Pick a Time Slot

Dates aren’t enough — customers need to choose a specific time. A dropdown with available time slots is cleaner than a free-form time input. Let’s build it with Svelte’s select binding.


What You’ll Learn

  • Bind select elements with bind:value
  • Generate time slot options dynamically
  • Handle empty/placeholder selections
  • Format times for display

Basic Select Binding

Selects work just like text inputs:

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

<label for="booking-time">Preferred Time</label>
<select id="booking-time" bind:value={selectedTime}>
  <option value="">Choose a time...</option>
  <option value="09:00">9:00 AM</option>
  <option value="10:00">10:00 AM</option>
  <option value="11:00">11:00 AM</option>
  <option value="14:00">2:00 PM</option>
  <option value="15:00">3:00 PM</option>
</select>

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

The bound value matches the value attribute of the selected option.


Generate Time Slots

Hardcoding options gets tedious. Generate them:

<script>
  let selectedTime = $state('');
  
  // Generate time slots from 9 AM to 5 PM
  function generateTimeSlots() {
    const slots = [];
    for (let hour = 9; hour < 17; hour++) {
      slots.push({
        value: `${hour.toString().padStart(2, '0')}:00`,
        label: formatTime(hour, 0)
      });
      // Add 30-minute slots
      slots.push({
        value: `${hour.toString().padStart(2, '0')}:30`,
        label: formatTime(hour, 30)
      });
    }
    return slots;
  }
  
  function formatTime(hour, minute) {
    const period = hour >= 12 ? 'PM' : 'AM';
    const displayHour = hour > 12 ? hour - 12 : hour === 0 ? 12 : hour;
    const displayMinute = minute.toString().padStart(2, '0');
    return `${displayHour}:${displayMinute} ${period}`;
  }
  
  const timeSlots = generateTimeSlots();
</script>

<label for="booking-time">Preferred Time</label>
<select id="booking-time" bind:value={selectedTime} required>
  <option value="">Choose a time...</option>
  {#each timeSlots as slot}
    <option value={slot.value}>{slot.label}</option>
  {/each}
</select>

Now you have 16 time slots (9 AM to 4:30 PM, every 30 minutes) without manual HTML.


Disable Unavailable Slots

Some times might be booked. Mark them as unavailable:

<script>
  let selectedTime = $state('');
  
  // Simulated booked times (would come from server)
  const bookedTimes = ['10:00', '14:00', '14:30'];
  
  const timeSlots = generateTimeSlots().map(slot => ({
    ...slot,
    disabled: bookedTimes.includes(slot.value)
  }));
</script>

<select id="booking-time" bind:value={selectedTime} required>
  <option value="">Choose a time...</option>
  {#each timeSlots as slot}
    <option value={slot.value} disabled={slot.disabled}>
      {slot.label}{slot.disabled ? ' (Unavailable)' : ''}
    </option>
  {/each}
</select>

Disabled options can’t be selected and appear greyed out.


Show Remaining Slots

Help customers see availability at a glance:

<script>
  let selectedTime = $state('');
  const bookedTimes = ['10:00', '14:00', '14:30'];
  
  const timeSlots = generateTimeSlots();
  
  let availableCount = $derived(
    timeSlots.filter(slot => !bookedTimes.includes(slot.value)).length
  );
</script>

<label for="booking-time">
  Preferred Time 
  <span class="availability">({availableCount} slots available)</span>
</label>
<select id="booking-time" bind:value={selectedTime} required>
  <!-- options... -->
</select>

<style>
  .availability {
    font-weight: normal;
    color: #666;
    font-size: 0.875rem;
  }
</style>

Integrate with Date Selection

Time availability depends on the selected date:

<script>
  let bookingDate = $state('');
  let selectedTime = $state('');
  
  // Reset time when date changes
  $effect(() => {
    if (bookingDate) {
      selectedTime = ''; // Clear time selection
    }
  });
  
  // In a real app, fetch availability from server
  function getBookedTimesForDate(date) {
    // Simulated — different days have different bookings
    const bookings = {
      '2025-06-15': ['10:00', '11:00'],
      '2025-06-16': ['14:00', '15:00', '15:30'],
    };
    return bookings[date] || [];
  }
  
  let bookedTimes = $derived(getBookedTimesForDate(bookingDate));
</script>

<div class="field">
  <label for="booking-date">Date</label>
  <input type="date" id="booking-date" bind:value={bookingDate} required />
</div>

<div class="field">
  <label for="booking-time">Time</label>
  <select 
    id="booking-time" 
    bind:value={selectedTime} 
    disabled={!bookingDate}
    required
  >
    <option value="">
      {bookingDate ? 'Choose a time...' : 'Select a date first'}
    </option>
    {#each timeSlots as slot}
      <option 
        value={slot.value} 
        disabled={bookedTimes.includes(slot.value)}
      >
        {slot.label}
      </option>
    {/each}
  </select>
</div>

The time dropdown is disabled until a date is selected, then shows availability for that specific day.


Complete Time Selection

Here’s the polished time selection field:

<!-- filename: src/lib/components/BookingForm.svelte (partial) -->
<script>
  let bookingDate = $state('');
  let selectedTime = $state('');
  
  function generateTimeSlots() {
    const slots = [];
    for (let hour = 9; hour < 17; hour++) {
      for (let minute of [0, 30]) {
        const value = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
        const period = hour >= 12 ? 'PM' : 'AM';
        const displayHour = hour > 12 ? hour - 12 : hour;
        const label = `${displayHour}:${minute.toString().padStart(2, '0')} ${period}`;
        slots.push({ value, label });
      }
    }
    return slots;
  }
  
  const timeSlots = generateTimeSlots();
  
  // Simulated availability
  const bookedTimes = ['10:00', '14:00'];
</script>

<fieldset>
  <legend>Appointment Details</legend>
  
  <div class="field">
    <label for="booking-date">Preferred Date</label>
    <input 
      type="date" 
      id="booking-date"
      bind:value={bookingDate}
      required
    />
  </div>
  
  <div class="field">
    <label for="booking-time">Preferred Time</label>
    <select 
      id="booking-time" 
      name="bookingTime"
      bind:value={selectedTime}
      disabled={!bookingDate}
      required
    >
      <option value="">
        {bookingDate ? 'Choose a time...' : 'Select a date first'}
      </option>
      {#each timeSlots as slot}
        <option 
          value={slot.value} 
          disabled={bookedTimes.includes(slot.value)}
        >
          {slot.label}{bookedTimes.includes(slot.value) ? ' (Booked)' : ''}
        </option>
      {/each}
    </select>
  </div>
</fieldset>

<style>
  select {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 1rem;
    background: white;
  }
  
  select:disabled {
    background: #f5f5f5;
    cursor: not-allowed;
  }
  
  option:disabled {
    color: #999;
  }
</style>

Common Mistakes

Forgetting the Empty Option

<!-- ❌ No way to show "nothing selected" -->
<select bind:value={time}>
  <option value="09:00">9:00 AM</option>
</select>

<!-- ✅ Include placeholder option -->
<select bind:value={time}>
  <option value="">Choose a time...</option>
  <option value="09:00">9:00 AM</option>
</select>

Value Type Mismatch

<script>
  let selectedHour = $state(0); // Number
</script>

<!-- ❌ Option values are strings -->
<select bind:value={selectedHour}>
  <option value="9">9 AM</option>
</select>

<!-- ✅ Match the type -->
<select bind:value={selectedHour}>
  <option value={9}>9 AM</option>
</select>

Option values are strings by default. Use value={number} for numeric binding.


Summary

Select elements bind with bind:value just like text inputs. Generate options dynamically with {#each}, disable unavailable choices, and coordinate with other form fields using effects.

Key takeaways:

  • bind:value on selects works like text inputs
  • Generate options with {#each} loops
  • Use disabled on options for unavailable slots
  • Coordinate dependent dropdowns with $effect

Next Steps

Date and time are set. Continue with Service Selection with bind:group to let customers choose which service they want.