A Simple, Focused Component
Not every component needs to be complex. The PriceBadge is small but useful — it displays a formatted price with consistent styling. This pattern appears throughout BookIt: on service cards, in the booking summary, on the cart.
What You’ll Learn
- Create a focused utility component
- Use default prop values effectively
- Add component variants with props
- Format values within components
The Basic PriceBadge
Start simple — a component that displays a price:
<!-- filename: src/lib/components/PriceBadge.svelte -->
<script>
let { price } = $props();
</script>
<span class="price-badge">${price}</span>
<style>
.price-badge {
font-weight: bold;
color: #007bff;
}
</style> Usage:
<script>
import PriceBadge from '$lib/components/PriceBadge.svelte';
</script>
<PriceBadge price={50} />
<!-- Displays: $50 --> Add Currency Support
What if you need different currencies?
<script>
let {
price,
currency = 'USD',
locale = 'en-US'
} = $props();
function formatPrice(amount, curr, loc) {
return new Intl.NumberFormat(loc, {
style: 'currency',
currency: curr
}).format(amount);
}
let formatted = $derived(formatPrice(price, currency, locale));
</script>
<span class="price-badge">{formatted}</span> Now it handles formatting automatically:
<PriceBadge price={50} />
<!-- $50.00 -->
<PriceBadge price={50} currency="EUR" locale="de-DE" />
<!-- 50,00 € -->
<PriceBadge price={50} currency="GBP" locale="en-GB" />
<!-- £50.00 --> Add Size Variants
Different contexts need different sizes:
<script>
let {
price,
currency = 'USD',
locale = 'en-US',
size = 'medium' // 'small' | 'medium' | 'large'
} = $props();
let formatted = $derived(
new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(price)
);
</script>
<span class="price-badge size-{size}">{formatted}</span>
<style>
.price-badge {
font-weight: bold;
color: #007bff;
}
.size-small {
font-size: 0.875rem;
}
.size-medium {
font-size: 1rem;
}
.size-large {
font-size: 1.5rem;
}
</style> Usage:
<PriceBadge price={50} size="small" /> <!-- In a compact list -->
<PriceBadge price={50} /> <!-- Default medium -->
<PriceBadge price={50} size="large" /> <!-- Hero/featured price --> Add Visual Variants
Maybe you want different styles for different contexts:
<script>
let {
price,
currency = 'USD',
locale = 'en-US',
size = 'medium',
variant = 'default' // 'default' | 'highlight' | 'muted' | 'sale'
} = $props();
let formatted = $derived(
new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(price)
);
</script>
<span class="price-badge size-{size} variant-{variant}">
{formatted}
</span>
<style>
.price-badge {
font-weight: bold;
}
/* Size variants */
.size-small { font-size: 0.875rem; }
.size-medium { font-size: 1rem; }
.size-large { font-size: 1.5rem; }
/* Color variants */
.variant-default {
color: #007bff;
}
.variant-highlight {
color: #28a745;
}
.variant-muted {
color: #6c757d;
font-weight: normal;
}
.variant-sale {
color: #dc3545;
}
</style> Usage:
<PriceBadge price={50} /> <!-- Blue default -->
<PriceBadge price={50} variant="highlight" /> <!-- Green for savings -->
<PriceBadge price={50} variant="muted" /> <!-- Gray for original price -->
<PriceBadge price={35} variant="sale" /> <!-- Red for sale price --> Handle Edge Cases
What about zero, negative, or missing prices?
<script>
let {
price,
currency = 'USD',
locale = 'en-US',
size = 'medium',
variant = 'default',
freeLabel = 'Free',
showZero = false
} = $props();
function formatPrice(amount) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
}
let displayValue = $derived(() => {
if (price === 0 && !showZero) {
return freeLabel;
}
return formatPrice(price);
});
</script>
<span class="price-badge size-{size} variant-{variant}">
{displayValue()}
</span> Now:
<PriceBadge price={0} />
<!-- "Free" -->
<PriceBadge price={0} showZero />
<!-- "$0.00" -->
<PriceBadge price={0} freeLabel="No charge" />
<!-- "No charge" --> Use in ServiceCard
Update ServiceCard to use PriceBadge:
<!-- filename: src/lib/components/ServiceCard.svelte -->
<script>
import PriceBadge from './PriceBadge.svelte';
let { service, selected = false } = $props();
</script>
<article class="service-card" class:selected>
<h3>
<a href="/services/{service.slug}">{service.name}</a>
</h3>
<p>{service.description}</p>
<div class="meta">
<PriceBadge price={service.price} />
<span class="duration">{service.duration} min</span>
</div>
</article> The ServiceCard is now cleaner — price formatting is delegated to PriceBadge.
Create a Duration Component Too
While we’re at it, let’s make a DurationBadge:
<!-- filename: src/lib/components/DurationBadge.svelte -->
<script>
let {
minutes,
size = 'medium',
showIcon = true
} = $props();
function formatDuration(mins) {
if (mins < 60) {
return `${mins} min`;
}
const hours = Math.floor(mins / 60);
const remainingMins = mins % 60;
if (remainingMins === 0) {
return `${hours} hr`;
}
return `${hours} hr ${remainingMins} min`;
}
let formatted = $derived(formatDuration(minutes));
</script>
<span class="duration-badge size-{size}">
{#if showIcon}🕐{/if}
{formatted}
</span>
<style>
.duration-badge {
color: #666;
}
.size-small { font-size: 0.875rem; }
.size-medium { font-size: 1rem; }
.size-large { font-size: 1.25rem; }
</style> Usage:
<DurationBadge minutes={60} /> <!-- 🕐 1 hr -->
<DurationBadge minutes={90} /> <!-- 🕐 1 hr 30 min -->
<DurationBadge minutes={45} /> <!-- 🕐 45 min --> The Power of Small Components
Small components like PriceBadge and DurationBadge:
- Encapsulate formatting logic — Change price format once, updates everywhere
- Ensure consistency — Same styling across the app
- Simplify parent components — Less code in ServiceCard
- Are easy to test — Simple input/output
Common Mistakes
Over-Engineering
<!-- ❌ Too many props for a simple component -->
<PriceBadge
price={50}
fontFamily="Arial"
fontSize={16}
fontWeight={700}
color="#007bff"
padding="0.25rem"
borderRadius="4px"
/>
<!-- ✅ Use variants instead -->
<PriceBadge price={50} variant="highlight" size="large" /> Not Using Defaults
<!-- ❌ Requiring every prop every time -->
<PriceBadge price={50} currency="USD" locale="en-US" size="medium" variant="default" />
<!-- ✅ Sensible defaults -->
<PriceBadge price={50} /> Summary
Small utility components like PriceBadge encapsulate formatting and styling. Use defaults for common cases and props for variations. These components compose well and keep parent components clean.
Key takeaways:
- Small components are valuable
- Use defaults for common cases
- Variants handle different visual styles
- Encapsulate formatting logic in components
Next Steps
Components can accept many props. Continue with Spread Props to learn how to forward props elegantly.