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:valueon selects works like text inputs- Generate options with
{#each}loops - Use
disabledon 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.