Choose Your Service

The booking form needs service selection. When customers can only pick one service per booking, radio buttons work perfectly. Svelte’s bind:group connects multiple inputs to a single value.


What You’ll Learn

  • Use bind:group for radio buttons
  • Create service selection UI
  • Handle checkbox groups for multiple selections
  • Display selected service details

Radio Button Basics

Radio buttons let users select one option from a group. All radios in a group share the same name:

<script>
  let selectedService = $state('');
</script>

<fieldset>
  <legend>Select a Service</legend>
  
  <label>
    <input type="radio" name="service" value="lawn-mowing" bind:group={selectedService} />
    Lawn Mowing
  </label>
  
  <label>
    <input type="radio" name="service" value="hedge-trimming" bind:group={selectedService} />
    Hedge Trimming
  </label>
  
  <label>
    <input type="radio" name="service" value="consultation" bind:group={selectedService} />
    Garden Consultation
  </label>
</fieldset>

<p>Selected: {selectedService || 'None'}</p>

bind:group connects all three inputs to selectedService. Clicking any radio updates the bound value to that input’s value.


Generate from Data

With real services, generate the options:

<script>
  let selectedService = $state('');
  
  const services = [
    { id: '1', slug: 'lawn-mowing', name: 'Lawn Mowing', price: 50 },
    { id: '2', slug: 'hedge-trimming', name: 'Hedge Trimming', price: 75 },
    { id: '3', slug: 'garden-consultation', name: 'Garden Consultation', price: 100 },
    { id: '4', slug: 'tree-pruning', name: 'Tree Pruning', price: 150 }
  ];
</script>

<fieldset>
  <legend>Select a Service</legend>
  
  {#each services as service}
    <label class="service-option">
      <input 
        type="radio" 
        name="service" 
        value={service.slug}
        bind:group={selectedService}
      />
      <span class="service-name">{service.name}</span>
      <span class="service-price">${service.price}</span>
    </label>
  {/each}
</fieldset>

Rich Service Cards

Make the selection more visual:

<script>
  let selectedService = $state('');
  
  const services = [
    { 
      id: '1', 
      slug: 'lawn-mowing', 
      name: 'Lawn Mowing', 
      description: 'Professional lawn mowing service.',
      price: 50,
      duration: 60
    },
    { 
      id: '2', 
      slug: 'hedge-trimming', 
      name: 'Hedge Trimming', 
      description: 'Expert hedge and shrub trimming.',
      price: 75,
      duration: 90
    },
    { 
      id: '3', 
      slug: 'garden-consultation', 
      name: 'Garden Consultation', 
      description: 'One-on-one garden planning advice.',
      price: 100,
      duration: 60
    }
  ];
  
  let selectedDetails = $derived(
    services.find(s => s.slug === selectedService)
  );
</script>

<fieldset class="service-selection">
  <legend>Select a Service</legend>
  
  <div class="service-grid">
    {#each services as service}
      <label class="service-card" class:selected={selectedService === service.slug}>
        <input 
          type="radio" 
          name="service" 
          value={service.slug}
          bind:group={selectedService}
        />
        <div class="card-content">
          <h3>{service.name}</h3>
          <p>{service.description}</p>
          <div class="card-footer">
            <span class="price">${service.price}</span>
            <span class="duration">{service.duration} min</span>
          </div>
        </div>
      </label>
    {/each}
  </div>
</fieldset>

{#if selectedDetails}
  <div class="selection-summary">
    <h3>You selected: {selectedDetails.name}</h3>
    <p>Price: ${selectedDetails.price} · Duration: {selectedDetails.duration} minutes</p>
  </div>
{/if}

<style>
  .service-grid {
    display: grid;
    gap: 1rem;
  }
  
  .service-card {
    display: block;
    padding: 1rem;
    border: 2px solid #ddd;
    border-radius: 8px;
    cursor: pointer;
    transition: border-color 0.2s;
  }
  
  .service-card:hover {
    border-color: #007bff;
  }
  
  .service-card.selected {
    border-color: #007bff;
    background: #f0f7ff;
  }
  
  .service-card input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
  }
  
  .card-content h3 {
    margin: 0 0 0.5rem 0;
  }
  
  .card-content p {
    margin: 0 0 1rem 0;
    color: #666;
  }
  
  .card-footer {
    display: flex;
    justify-content: space-between;
  }
  
  .price {
    font-weight: bold;
    color: #007bff;
  }
  
  .duration {
    color: #666;
  }
</style>

The radio input is visually hidden — the entire card becomes clickable.


Checkbox Groups

What if customers can select multiple add-ons? Use checkboxes with bind:group:

<script>
  let selectedAddOns = $state([]);
  
  const addOns = [
    { id: 'cleanup', name: 'Garden Cleanup', price: 25 },
    { id: 'fertilizer', name: 'Lawn Fertilizer', price: 15 },
    { id: 'weed-control', name: 'Weed Control', price: 20 }
  ];
  
  let addOnTotal = $derived(
    addOns
      .filter(addon => selectedAddOns.includes(addon.id))
      .reduce((sum, addon) => sum + addon.price, 0)
  );
</script>

<fieldset>
  <legend>Add-Ons (Optional)</legend>
  
  {#each addOns as addon}
    <label class="addon-option">
      <input 
        type="checkbox" 
        value={addon.id}
        bind:group={selectedAddOns}
      />
      <span>{addon.name}</span>
      <span class="addon-price">+${addon.price}</span>
    </label>
  {/each}
</fieldset>

{#if selectedAddOns.length > 0}
  <p>Add-ons total: ${addOnTotal}</p>
{/if}

With checkboxes, bind:group uses an array. Selected values are added; deselected values are removed.


Bind:checked for Single Checkboxes

For a standalone checkbox (not a group), use bind:checked:

<script>
  let agreeToTerms = $state(false);
  let wantsNewsletter = $state(false);
</script>

<label>
  <input type="checkbox" bind:checked={agreeToTerms} required />
  I agree to the terms and conditions
</label>

<label>
  <input type="checkbox" bind:checked={wantsNewsletter} />
  Send me promotional emails
</label>

<button disabled={!agreeToTerms}>Book Now</button>

bind:checked binds to a boolean — true when checked, false when not.


Complete Service Selection

Integrate into the booking form:

<!-- filename: src/lib/components/BookingForm.svelte (partial) -->
<script>
  // Previous state...
  let customerName = $state('');
  let customerEmail = $state('');
  let bookingDate = $state('');
  let selectedTime = $state('');
  
  // Service selection
  let selectedService = $state('');
  let selectedAddOns = $state([]);
  let agreeToTerms = $state(false);
  
  const services = [
    { slug: 'lawn-mowing', name: 'Lawn Mowing', price: 50, duration: 60 },
    { slug: 'hedge-trimming', name: 'Hedge Trimming', price: 75, duration: 90 },
    { slug: 'garden-consultation', name: 'Garden Consultation', price: 100, duration: 60 }
  ];
  
  const addOns = [
    { id: 'cleanup', name: 'Garden Cleanup', price: 25 },
    { id: 'fertilizer', name: 'Lawn Fertilizer', price: 15 }
  ];
  
  let selectedServiceDetails = $derived(
    services.find(s => s.slug === selectedService)
  );
  
  let totalPrice = $derived(() => {
    if (!selectedServiceDetails) return 0;
    const addOnTotal = addOns
      .filter(a => selectedAddOns.includes(a.id))
      .reduce((sum, a) => sum + a.price, 0);
    return selectedServiceDetails.price + addOnTotal;
  });
</script>

<form>
  <!-- Contact and date fields... -->
  
  <fieldset>
    <legend>Select a Service</legend>
    
    {#each services as service}
      <label class="service-option" class:selected={selectedService === service.slug}>
        <input 
          type="radio" 
          name="service" 
          value={service.slug}
          bind:group={selectedService}
          required
        />
        <span class="name">{service.name}</span>
        <span class="details">${service.price} · {service.duration} min</span>
      </label>
    {/each}
  </fieldset>
  
  <fieldset>
    <legend>Add-Ons (Optional)</legend>
    
    {#each addOns as addon}
      <label class="addon-option">
        <input 
          type="checkbox" 
          value={addon.id}
          bind:group={selectedAddOns}
        />
        {addon.name} (+${addon.price})
      </label>
    {/each}
  </fieldset>
  
  <div class="total">
    <strong>Total: ${totalPrice()}</strong>
  </div>
  
  <label class="terms">
    <input type="checkbox" bind:checked={agreeToTerms} required />
    I agree to the terms and conditions
  </label>
  
  <button type="submit" disabled={!agreeToTerms}>
    Book Now
  </button>
</form>

Common Mistakes

Forgetting name Attribute

<!-- ❌ Without name, radios don't form a group natively -->
<input type="radio" value="a" bind:group={selected} />
<input type="radio" value="b" bind:group={selected} />

<!-- ✅ Include name for proper form behavior -->
<input type="radio" name="choice" value="a" bind:group={selected} />
<input type="radio" name="choice" value="b" bind:group={selected} />

Using Wrong Binding

<!-- ❌ bind:value doesn't work for radio groups -->
<input type="radio" bind:value={selected} />

<!-- ✅ Use bind:group for radios -->
<input type="radio" bind:group={selected} />

<!-- ❌ bind:group for single checkbox -->
<input type="checkbox" bind:group={agree} />

<!-- ✅ Use bind:checked for single checkbox -->
<input type="checkbox" bind:checked={agree} />

Summary

bind:group connects radio buttons and checkboxes to shared state. Radios bind to a single value; checkboxes bind to an array. Use bind:checked for standalone checkboxes.

Key takeaways:

  • bind:group for radio buttons → single value
  • bind:group for checkboxes → array of values
  • bind:checked for single checkbox → boolean
  • Include name attribute for form compatibility

Next Steps

The form captures service selection. Continue with Add Optional Notes Textarea to let customers provide additional instructions.