Skip to content
All Taste components

Inputs

Time slot picker

Pick a day, then one free delivery or appointment slot from a grid.

day tabs · selected · available · full

Delivery slot1-hour windows, IST
Free delivery on slots after 2 pm.
"use client";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";

export type Slot = { id: string; label: string; full?: boolean };
export type Day = { id: string; label: string; slots: Slot[] };

/*
 * Days are tabs; slots are a native radio group (visually hidden inputs), so
 * arrow keys move between free slots and full ones are skipped.
 */
export function TimeSlotPicker({ days, value, onChange, label = "Delivery slot" }:
  { days: Day[]; value: { day: string; slot: string } | null; onChange: (v: { day: string; slot: string }) => void; label?: string }) {
  const id = useId();
  const [day, setDay] = useState(value?.day ?? days[0].id);
  const current = days.find((d) => d.id === day) ?? days[0];

  return (
    <div className="grid gap-3.5">
      <p id={id} className="text-sm font-medium text-fg">{label}</p>
      <div role="tablist" aria-label="Day" className="inline-flex w-fit gap-0.5 rounded-lg bg-surface-2 p-[3px]">
        {days.map((d) => (
          <button
            key={d.id}
            role="tab"
            type="button"
            aria-selected={d.id === day}
            onClick={() => setDay(d.id)}
            className="h-[30px] rounded px-3 text-[13px] font-medium text-fg-muted transition aria-selected:bg-bg aria-selected:text-fg aria-selected:shadow-sm"
          >
            {d.label}
          </button>
        ))}
      </div>
      <div role="radiogroup" aria-labelledby={id} className="grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-2">
        {current.slots.map((s) => (
          <label key={s.id} className={cn(s.full ? "cursor-not-allowed" : "cursor-pointer")}>
            <input
              type="radio"
              name={id}
              className="peer sr-only"
              disabled={s.full}
              checked={value?.day === day && value.slot === s.id}
              onChange={() => onChange({ day, slot: s.id })}
            />
            <span
              className={cn(
                "grid h-10 place-items-center whitespace-nowrap rounded border border-border bg-bg px-2 text-[13px] font-medium tabular-nums text-fg transition",
                "hover:border-border-strong peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2",
                "peer-checked:border-primary peer-checked:bg-primary-soft peer-checked:text-primary-soft-fg peer-checked:ring-1 peer-checked:ring-inset peer-checked:ring-primary",
                "peer-disabled:bg-surface peer-disabled:text-fg-subtle peer-disabled:line-through",
              )}
            >
              {s.label}
              {s.full && <span className="sr-only">, full</span>}
            </span>
          </label>
        ))}
      </div>
    </div>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.