# Time slots

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

- Type: Component (inputs)
- Access: Free, open source (MIT)
- Page: https://goodui.dev/native/components/time-slots
- Markdown: https://goodui.dev/native/components/time-slots.md
- Registry item: https://goodui.dev/native/r/time-slots.json
- Dependencies: react-native-reanimated
- Keywords: react native time slot picker, booking time slots, appointment scheduler, available times grid, salon booking app, calendly style picker, sticky booking summary

## 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).

## When not to use

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

## Installation

With the shadcn CLI (needs a components.json in the Expo app), then `npx expo install --fix` so native dependencies match your Expo SDK. The foundation comes along the first time.

```bash
npx shadcn@latest add https://goodui.dev/native/r/time-slots.json
npx expo install --fix
```

The component lands in `components/goodui-native/time-slots/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs button, locale, skeleton.

Built-in strings follow `<GooduiLocaleProvider>` (one [language pack](https://goodui.dev/native/r/locale-tr.json) translates the whole kit); the `labels` prop overrides them for one instance.

## Usage

```tsx
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"
    />
  );
}
```

## Examples

### Floating summary pinned above the safe area

```tsx
<TimeSlots slots={slots} summary="floating" duration={60} date={day} onConfirm={reserve} confirmLabel="Reserve" />
```

### Classes with places left and prices, 24-hour clock

```tsx
<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

```tsx
<TimeSlots slots={slots} loading={!slots.length && fetching} emptyLabel="Fully booked. Try Thursday." />
```

## API reference

### TimeSlots

`import { TimeSlots } from "@/components/goodui-native/time-slots/time-slots";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `slots` (required) | `TimeSlot[]` | – | The day's slots, any order. They are sorted by time. |
| `value` | `string \| null` | – | Selected slot time (controlled). `null` for none. |
| `defaultValue` | `string \| null` | – | Starting slot when uncontrolled. |
| `onValueChange` | `(time: string \| null) => void` | – | Called with the chosen time, or `null` when the selected slot is tapped again with `allowDeselect`. |
| `allowDeselect` | `boolean` | `false` | Tapping the selected slot clears it. Default `false`. |
| `duration` | `number` | – | Appointment length in minutes. The summary then shows the end time and the length. |
| `date` | `Date` | – | Day the slots are on, shown in the summary. |
| `locale` | `string` | – | BCP 47 locale for time and date formats. Defaults to the device locale. |
| `hour12` | `boolean` | – | Force a 12 or 24 hour clock. Defaults to the locale's. |
| `timeZone` | `string` | – | 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. |
| `columns` | `2 \| 3 \| 4` | `3` | Slots per row. Default 3. |
| `groupBy` | `"period" \| "none"` | `"period"` | `period` groups slots under morning, afternoon and evening; `none` is one grid. |
| `periodLabels` | `Record<TimeSlotsPeriod, string>` | – | Localized group titles. Same as `labels.morning`, `labels.afternoon` and `labels.evening`. |
| `summary` | `"inline" \| "floating" \| "none"` | `"inline"` | 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. |
| `confirmLabel` | `string` | – | Confirm button label. Sentence case, starts with a verb. Same as `labels.confirm`. |
| `onConfirm` | `(time: string) => unknown` | – | Called with the selected time from the confirm button. Return a promise to show pending, success and error. |
| `successLabel` | `string` | – | Text on the confirm button after the promise resolves. |
| `loading` | `boolean` | `false` | Shows placeholders in place of the grid while slots load. |
| `emptyLabel` | `string` | – | Shown when there are no available slots at all. Same as `labels.empty`. |
| `haptic` | `HapticName \| false` | `"select"` | Haptic when a slot is picked. `false` turns it off. |
| `accessibilityLabel` | `string` | – | Names the slot group for screen readers. Same as `labels.availableTimes`. |
| `labels` | `Partial<TimeSlotsLabels>` | – | Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. |
| `style` | `StyleProp<ViewStyle>` | – | Style for the outer container. |
| `testID` | `string` | – | Identifier for end-to-end tests. |

## 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".

## Motion

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

## Haptics

- select when a slot is picked (configurable with haptic, false turns it off)
- The confirm Button plays its own tap, success and error haptics.

## Performance

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

## 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

- [Week strip](https://goodui.dev/native/components/week-strip.md): 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.
- [Date picker](https://goodui.dev/native/components/date-picker.md): 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.
- [Chip](https://goodui.dev/native/components/chip.md): 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.
- [Button](https://goodui.dev/native/components/button.md): 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.

## Guidance for AI tools

Time slots: 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. Use only the documented props. Keep accessibility, reduced motion and both color schemes intact when adapting it.

Full library index: https://goodui.dev/native/llms.txt
