Make It Look Good

The ServiceCard works, but it’s plain. Users judge quality by appearance. A well-styled card communicates professionalism and makes the service offerings more appealing.


What You’ll Learn

  • Structure CSS in Svelte components
  • Create visual hierarchy with typography
  • Add spacing and layout
  • Design an attractive card component

Start with Structure

First, ensure the HTML is semantic:

<!-- filename: src/lib/components/ServiceCard.svelte -->
<script>
	let { service, selected = false } = $props()
</script>

<article class="card" class:selected>
	<div class="card-image">
		<img src={service.image} alt={service.name} />
	</div>

	<div class="card-content">
		<h3 class="card-title">
			<a href="/services/{service.slug}">{service.name}</a>
		</h3>

		<p class="card-description">{service.description}</p>

		<div class="card-meta">
			<span class="price">${service.price}</span>
			<span class="duration">{service.duration} min</span>
		</div>
	</div>
</article>

Clear class names make styling straightforward.


Base Card Styling

The Card is just raw HTML now. Let’s add base styles for the card container:

<!-- filename: src/lib/components/ServiceCard.svelte -->
<!-- Script ... -->
<!-- HTML ... -->

<style>
	.card {
		background: white;
		border: 1px solid #e5e7eb;
		border-radius: 12px;
		overflow: hidden;
		transition: all 0.2s ease;
	}

	.card:hover {
		border-color: #3b82f6;
		box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
	}
</style>

This creates:

  • Clean white background
  • Subtle border
  • Rounded corners
  • Smooth hover effect

Style the Image

next add styles for the image section:

<!-- filename: src/lib/components/ServiceCard.svelte -->
<!-- Script ... -->
<!-- HTML ... -->

<style>
	/* ... previous styles */

	.card-image {
		aspect-ratio: 16 / 9;
		overflow: hidden;
		background: #f3f4f6;
	}

	.card-image img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		transition: transform 0.3s ease;
	}

	.card:hover .card-image img {
		transform: scale(1.05);
	}
</style>

Features:

  • Consistent aspect ratio
  • Images scale on hover
  • Fallback background color while loading

Content Area Styling

next step will be to add style for the text content:

<!-- filename: src/lib/components/ServiceCard.svelte -->
<!-- Script ... -->
<!-- HTML ... -->

<style>
	/* ... previous styles */

	.card-content {
		padding: 1.25rem;
	}

	.card-title {
		margin: 0 0 0.5rem 0;
		font-size: 1.125rem;
		font-weight: 600;
		line-height: 1.4;
	}

	.card-title a {
		color: #1f2937;
		text-decoration: none;
	}

	.card-title a:hover {
		color: #3b82f6;
	}

	.card-description {
		margin: 0 0 1rem 0;
		color: #6b7280;
		font-size: 0.9375rem;
		line-height: 1.6;
	}
</style>

Visual hierarchy:

  • Title is larger and bolder
  • Description is smaller and gray
  • Comfortable line height for readability

Meta Information

THan add style for the price and duration:

<!-- filename: src/lib/components/ServiceCard.svelte -->
<!-- Script ... -->
<!-- HTML ... -->

<style>
	/* ... previous styles */
	.card-meta {
		display: flex;
		justify-content: space-between;
		align-items: center;
		padding-top: 1rem;
		border-top: 1px solid #f3f4f6;
	}

	.price {
		font-size: 1.25rem;
		font-weight: 700;
		color: #3b82f6;
	}

	.duration {
		font-size: 0.875rem;
		color: #9ca3af;
	}
</style>

Selected State

Finally add style when the card is selected (like in a form):

<!-- filename: src/lib/components/ServiceCard.svelte -->
<!-- Script ... -->
<!-- HTML ... -->

<style>
	/* ... previous styles */
	.card.selected {
		border-color: #3b82f6;
		background: #eff6ff;
	}

	.card.selected .card-title a {
		color: #1d4ed8;
	}
</style>

The blue tint shows selection clearly.


The Complete Styled Component

Here is the full styled ServiceCard component:

<!-- filename: src/lib/components/ServiceCard.svelte -->
<script>
	let { service, selected = false } = $props()
</script>

<article class="card" class:selected>
	{#if service.image}
		<div class="card-image">
			<img src={service.image} alt={service.name} />
		</div>
	{/if}

	<div class="card-content">
		<h3 class="card-title">
			<a href="/services/{service.slug}">{service.name}</a>
		</h3>

		<p class="card-description">{service.description}</p>

		<div class="card-meta">
			<span class="price">${service.price}</span>
			<span class="duration">{service.duration} min</span>
		</div>
	</div>
</article>

<style>
	.card {
		background: white;
		border: 1px solid #e5e7eb;
		border-radius: 12px;
		overflow: hidden;
		transition: all 0.2s ease;
	}

	.card:hover {
		border-color: #3b82f6;
		box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
	}

	.card.selected {
		border-color: #3b82f6;
		background: #eff6ff;
	}

	.card-image {
		aspect-ratio: 16 / 9;
		overflow: hidden;
		background: #f3f4f6;
	}

	.card-image img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		transition: transform 0.3s ease;
	}

	.card:hover .card-image img {
		transform: scale(1.05);
	}

	.card-content {
		padding: 1.25rem;
	}

	.card-title {
		margin: 0 0 0.5rem 0;
		font-size: 1.125rem;
		font-weight: 600;
		line-height: 1.4;
	}

	.card-title a {
		color: #1f2937;
		text-decoration: none;
	}

	.card-title a:hover {
		color: #3b82f6;
	}

	.card.selected .card-title a {
		color: #1d4ed8;
	}

	.card-description {
		margin: 0 0 1rem 0;
		color: #6b7280;
		font-size: 0.9375rem;
		line-height: 1.6;
	}

	.card-meta {
		display: flex;
		justify-content: space-between;
		align-items: center;
		padding-top: 1rem;
		border-top: 1px solid #f3f4f6;
	}

	.price {
		font-size: 1.25rem;
		font-weight: 700;
		color: #3b82f6;
	}

	.duration {
		font-size: 0.875rem;
		color: #9ca3af;
	}
</style>

Display in a Grid

On the services page, arrange cards in a grid:

<!-- filename: src/routes/services/+page.svelte -->
<script>
	import ServiceCard from '$lib/components/ServiceCard.svelte'

	const services = [
		/* ... */
	]
</script>

<h1>Our Services</h1>

<div class="services-grid">
	{#each services as service (service.id)}
		<ServiceCard {service} />
	{/each}
</div>

<style>
	.services-grid {
		display: grid;
		grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
		gap: 1.5rem;
	}
</style>

This creates a responsive grid that:

  • Shows multiple cards per row on wide screens
  • Stacks to single column on mobile
  • Maintains consistent spacing

Design Principles Applied

Visual Hierarchy:

  • Title is prominent (size, weight)
  • Description is secondary (smaller, gray)
  • Price catches the eye (color, weight)

Whitespace:

  • Padding separates content from edges
  • Margins separate sections
  • Border-top creates visual division

Feedback:

  • Hover state shows interactivity
  • Selected state confirms selection
  • Transitions make changes smooth

Common Mistakes

Too Many Colors

/* ❌ Too chaotic */
.price {
	color: #ff6b6b;
}
.duration {
	color: #4ecdc4;
}
.title {
	color: #45b7d1;
}

/* ✅ Cohesive palette */
.price {
	color: #3b82f6;
} /* Primary blue */
.duration {
	color: #9ca3af;
} /* Neutral gray */
.title a {
	color: #1f2937;
} /* Dark text */

Inconsistent Spacing

/* ❌ Random values */
.card-content {
	padding: 17px;
}
.card-meta {
	margin-top: 13px;
}

/* ✅ Consistent scale */
.card-content {
	padding: 1.25rem;
} /* 20px */
.card-meta {
	margin-top: 1rem;
} /* 16px */

Summary

A well-styled ServiceCard uses visual hierarchy, consistent spacing, and thoughtful colors. Svelte’s scoped styles keep everything organized. The result is a professional-looking component that enhances user trust.

Key takeaways:

  • Use semantic HTML for clear styling targets
  • Create visual hierarchy with size and weight
  • Add hover/selected states for interactivity
  • Maintain consistent spacing scale

Next Steps

Static styles are good, but sometimes styles need to change dynamically. Continue with Dynamic Classes to learn conditional styling.