Time-slot picker
A booking picker with a row of days and a grid of half-hour slots, disabling taken times and writing the choice to a hidden field.
Tailwind CSSFormsbookingtime-slotspickerradiogroup
Live preview
Code
<fieldset class="mx-auto max-w-md rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<legend class="px-1 text-base font-semibold text-slate-900">Pick a time</legend>
<div id="days" role="radiogroup" aria-label="Day" class="mt-3 flex gap-2 overflow-x-auto pb-1"></div>
<div id="slots" role="radiogroup" aria-label="Available times" class="mt-4 grid grid-cols-3 gap-2"></div>
<p id="picked" class="mt-4 min-h-6 text-sm text-slate-600" aria-live="polite"></p>
<input type="hidden" name="slot" id="slot-value">
</fieldset>
// Availability per ISO date; in a real app this comes from your API
const taken = new Set(['10:00', '11:30', '15:00']);
const times = [];
for (let m = 9 * 60; m < 17 * 60; m += 30) {
times.push(String(Math.floor(m / 60)).padStart(2, '0') + ':' + String(m % 60).padStart(2, '0'));
}
const days = Array.from({ length: 5 }, (_, i) => { const d = new Date(); d.setDate(d.getDate() + i); return d; });
let day = days[0], time = null;
const base = 'rounded-lg border px-3 py-2 text-sm transition focus-visible:outline-2 focus-visible:outline-indigo-500';
const on = ' border-indigo-600 bg-indigo-600 text-white';
const off = ' border-slate-300 bg-white text-slate-700 hover:border-indigo-400';
const dis = ' cursor-not-allowed border-slate-200 bg-slate-50 text-slate-300 line-through';
function make(label, cls, checked, handler, disabled = false) {
const b = document.createElement('button');
b.type = 'button';
b.setAttribute('role', 'radio');
b.setAttribute('aria-checked', String(checked));
b.disabled = disabled;
b.className = base + cls;
b.textContent = label;
if (!disabled) b.addEventListener('click', handler);
return b;
}
function draw() {
const dayBox = document.getElementById('days');
const slotBox = document.getElementById('slots');
dayBox.replaceChildren(...days.map((d) =>
make(d.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric' }), d === day ? on : off, d === day,
() => { day = d; time = null; draw(); })));
slotBox.replaceChildren(...times.map((t) =>
make(t, taken.has(t) ? dis : t === time ? on : off, t === time, () => { time = t; draw(); }, taken.has(t))));
const label = time ? day.toDateString() + ' at ' + time : '';
document.getElementById('picked').textContent = time ? 'Selected: ' + label : 'Choose a time to continue.';
document.getElementById('slot-value').value = time ? day.toISOString().slice(0, 10) + 'T' + time : '';
}
draw();
How it works
What it solves
Booking flows need a clear way to choose a day and a time while blocking slots that are already taken. A native date input is poor on desktops, and calendar libraries are overkill when only the next few days are offered.
How it works
Days and times are two groups of buttons marked role="radio" inside radiogroup containers, with aria-checked carrying the selection. A single draw function rebuilds both groups from state each time something changes, which is easy to reason about for lists this small. Slots are generated in a loop from minutes since midnight in 30 minute steps, then formatted with padStart. A slot found in the taken set is rendered disabled with a muted strike-through style and never receives a click handler.
Changing the day clears the chosen time, because availability is different per day. The selection is written to a hidden slot field as YYYY-MM-DDTHH:mm, so a normal form submit carries it to your controller, and an aria-live paragraph speaks the result.
Adapting it
- Fetch the taken slots for the selected day from your API inside the day handler.
- Change the step, opening hours or number of days at the top.
- Add
Arrowkey handling for a fully roving radio group.
Pitfalls
- Treat times as local to the business and convert on the server; the browser zone may differ.
- Re-check availability when the booking is saved, since two people can pick the same slot.
toISOStringuses UTC and can shift the date near midnight; format the date manually if your zone is far from UTC.
How to use it
- Put the fieldset inside your booking form and load the script after it.
- Replace the sample
takenset with real data for each selected day. - Adjust the opening hours loop and the number of days.
- Read the hidden
slotfield in your controller and validate it again against availability.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.