Time slots

Free · MITReact Native · Expo

A 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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/time-slots.json

Then 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

Use it for
  • 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).
Reach for something else
  • 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

PropTypeDefault
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`.
booleanfalse
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 | 43
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.
booleanfalse
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.