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.