Time slots
Free · MITReact Native · ExpoA booking grid of time slots grouped into morning, afternoon and evening: the picked slot pops into the accent, taken slots are crossed out, low availability shows "2 left", and a summary with the end time and the confirm button rides under the grid or pinned to the bottom of the screen.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/time-slots.jsonThen run npx expo install --fix so react-native-reanimated match your Expo SDK. Files land in components/goodui-native/time-slots.
Usage
import { useState } from "react";
import { TimeSlots } from "@/components/goodui-native/time-slots/time-slots";
export function PickTime({ day, slots }: { day: Date; slots: { time: string; available: boolean }[] }) {
const [time, setTime] = useState<string | null>(null);
return (
<TimeSlots
slots={slots}
value={time}
onValueChange={setTime}
date={day}
duration={45}
confirmLabel="Book"
onConfirm={(t) => api.book(day, t)}
successLabel="Booked"
/>
);
}When to use
- The time step of a booking flow: appointments, classes, tables, rentals, consultations.
- Any list of discrete start times with availability, after the day is chosen (pair it with week-strip or date-picker).
- Any time of day without fixed slots: use wheel-picker.
- A handful of options that are not times: use a ChipGroup from chip.
- Choosing the day: use week-strip or date-picker.
Examples
Floating summary pinned above the safe area
<TimeSlots slots={slots} summary="floating" duration={60} date={day} onConfirm={reserve} confirmLabel="Reserve" />Classes with places left and prices, 24-hour clock
<TimeSlots
hour12={false}
columns={2}
slots={[
{ time: "07:00", remaining: 2, detail: "€18" },
{ time: "12:15", detail: "€18" },
{ time: "18:30", available: false },
]}
/>Loading and an empty day
<TimeSlots slots={slots} loading={!slots.length && fetching} emptyLabel="Fully booked. Try Thursday." />API reference
| Prop | Type | Default |
|---|---|---|
| slots* The day's slots, any order. They are sorted by time. | TimeSlot[] | — |
| value Selected slot time (controlled). `null` for none. | string | null | — |
| defaultValue Starting slot when uncontrolled. | string | null | — |
| onValueChange Called with the chosen time, or `null` when the selected slot is tapped again with `allowDeselect`. | (time: string | null) => void | — |
| allowDeselect Tapping the selected slot clears it. Default `false`. | boolean | false |
| duration Appointment length in minutes. The summary then shows the end time and the length. | number | — |
| date Day the slots are on, shown in the summary. | Date | — |
| locale BCP 47 locale for time and date formats. Defaults to the device locale. | string | — |
| hour12 Force a 12 or 24 hour clock. Defaults to the locale's. | boolean | — |
| timeZone IANA time zone to show the times in, for example a business's "Europe/Amsterdam". Labels, the morning, afternoon and evening groups and the summary then show each slot's moment in that zone, and a slot that falls on another day there is grouped under that day (or names its weekday in one grid). Values stay the device-clock "HH:mm" strings. Default: the device's zone. | string | — |
| columns Slots per row. Default 3. | 2 | 3 | 4 | 3 |
| groupBy `period` groups slots under morning, afternoon and evening; `none` is one grid. | "period" | "none" | "period" |
| periodLabels Localized group titles. Same as `labels.morning`, `labels.afternoon` and `labels.evening`. | Record<TimeSlotsPeriod, string> | — |
| summary The selected slot's summary with the confirm button. `inline` sits under the grid, `floating` is pinned to the bottom of the screen above the safe area while a slot is selected, `none` hides it. | "inline" | "floating" | "none" | "inline" |
| confirmLabel Confirm button label. Sentence case, starts with a verb. Same as `labels.confirm`. | string | — |
| onConfirm Called with the selected time from the confirm button. Return a promise to show pending, success and error. | (time: string) => unknown | — |
| successLabel Text on the confirm button after the promise resolves. | string | — |
| loading Shows placeholders in place of the grid while slots load. | boolean | false |
| emptyLabel Shown when there are no available slots at all. Same as `labels.empty`. | string | — |
| haptic Haptic when a slot is picked. `false` turns it off. | HapticName | false | "select" |
| accessibilityLabel Names the slot group for screen readers. Same as `labels.availableTimes`. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<TimeSlotsLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Press: the slot scales to 95 % on the snappy spring from touch down.
- Pick: the accent fills in with a short timing while the slot squeezes to 90 % and springs back past its size on the bouncy spring (the one delight of the flow). The previous slot fades back.
- Summary: the floating bar rises from below on the smooth spring when a slot is picked and leaves the same way, keeping the last time while it goes; the inline summary brightens from 45 % opacity. A new time rolls up into place on the snappy spring.
- Reduced motion: no press scale or pop, the fill and the summary fade, the time changes in place with a fade.
- Haptic: select when a slot is picked (configurable with haptic, false turns it off)
- Haptic: The confirm Button plays its own tap, success and error haptics.
- Each slot animates only its own fill, scale and text color on the UI thread; picking re-renders the grid once.
- Slots are plain views in rows, not a virtualized list: keep a day to a few dozen slots.
Accessibility
- Slots are radios in a radiogroup (accessibilityLabel, default "Available times"), with checked and disabled in accessibilityState and aria-checked and aria-disabled on the web.
- Each slot's label starts with its formatted time, then "unavailable", "2 left" or its detail.
- Picking a slot announces it ("10:30 AM selected"); the summary text is a polite live region.
- Group titles are headers and show how many slots are available, or "Fully booked".
- Slots are at least 44 pt tall (52 with a note line). The confirm button is the kit's Button, so it reports its pending, success and error states.
- While loading, the placeholder grid is one busy element named "Loading times".
Notes for AI
- Slot times are 24-hour "HH:mm" strings and are also the values; display follows locale and hour12 through Intl.
- Pass timeZone (an IANA zone such as the business's) to show the times in that zone: each "HH:mm" is read as a moment on the device clock of date, then labels, period groups and the summary's time range and day are formatted in the zone. Slots that land on another day there get a group named after that day (with groupBy none, a weekday: "Wed 23:30"). Values stay unchanged.
- Return the booking promise from onConfirm; do not add a loading flag. The button shows pending, success (successLabel) and error itself.
- summary="floating" renders through the Portal above everything while a slot is selected: give the screen bottom padding so the bar never covers the last row. Use only one floating TimeSlots per screen.
- Slots with available: false stay in the grid, crossed out. Drop slots instead if they should not appear at all.
Related
A horizontal week of days: one accent pill slides between days and stretches toward the new one, weeks page with the finger 1:1, dots mark busy days, and a header shows the month with a Today button.
Inline month calendar: months slide with the finger, the selection circle travels from day to day and stretches toward the new one, a range sweeps its band in from the start day, and booked days are crossed out. A compact wheel variant picks day, month and year in the locale's order.
Filter, choice and input chips: selecting one fills it with the accent while a check draws itself in, removing one shrinks it away, and a ChipGroup in single mode moves one elastic highlight from chip to chip.
A press-responsive button with haptics that runs async actions on its own: it gathers into a spinner, then opens into a success or error state.