Booking time screen

ProReact Native · Expo

Step three of a booking flow: a week strip, the day's times grouped by morning, afternoon and evening for the chosen professional, a time zone note, and a summary that springs up from the bottom with the time rolling in on every pick.

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 @goodui-native-pro/booking-time-screen

Then run npx expo install --fix so react-native-reanimated match your Expo SDK. Files land in components/goodui-native/booking-time-screen.

Pro screens install from the license-gated @goodui-native-pro registry. Setup: Pro access.

Usage

import { router, Stack, useIsFocused } from "expo-router";
import { BookingTimeScreen } from "@/components/goodui-native/booking-time-screen/booking-time-screen";
import type { BookingTimeScreenProps } from "@/components/goodui-native/booking-time-screen/booking-time-screen";
import { api } from "@/lib/api";
import { useBookingDraft } from "@/lib/booking"; // your draft store

export default function TimeRoute() {
  const draft = useBookingDraft();
  const focused = useIsFocused();
  const availability: BookingTimeScreenProps["availability"] = (day, staffId) =>
    api.slots({ day: day.toISOString().slice(0, 10), staffId, minutes: draft.duration });
  return (
    <>
      <Stack.Screen options={{ headerShown: false }} />
      <BookingTimeScreen
        business={draft.business}
        services={draft.services}
        duration={draft.duration}
        staff={draft.staff}
        availability={availability}
        focused={focused}
        onBack={() => router.back()}
        onChangeStaff={() => router.back()}
        onConfirm={(start) => {
          draft.setStart(start);
          router.push("/book/confirm");
        }}
      />
    </>
  );
}

When to use

Use it for
  • Picking a start time for a service of known length with one professional or anyone available.
  • Availability that changes per day and comes from an API (return a promise and placeholders show while it loads).
  • Businesses whose clients may be in another time zone: times are shown in the business's zone, and the note names that zone and how far it is from the phone.
Reach for something else
  • Picking a date range (hotel nights): use date-picker in range mode.
  • Events with fixed sessions and seats: use time-slots with remaining counts on your own screen.
  • Recurring schedules: use a form with wheel-picker.

Examples

Sample salon, no props

<BookingTimeScreen />

Anyone available, 12 hour clock

<BookingTimeScreen staff={null} hour12 locale="en-US" confirmLabel="Book this time" onConfirm={book} />

API reference

PropTypeDefault
business
The business. Times are shown in its `timeZone` (with a note naming that zone and how far it is from the phone), and its closed weekdays dim days in the strip. An empty or unknown zone shows the phone's clock.
BookingBusinesssampleBusiness
services
The picked services, for the summary line and the appointment length.
BookingService[][sampleServices[0]]
duration
Appointment length in minutes. Defaults to the sum of `services`; pass it when add-ons add time.
number—
staff
Who it is with. `null` is "Any available".
BookingStaff | nullsampleStaff[0]
availability
Slots for a day and a staff id (`null` = anyone). Return a promise to show placeholders while it loads. Each "HH:mm" is a moment on the device clock of that day (`slotTime(date)` of your API's start); it is shown in the business's zone. `onConfirm` gets `slotStart(day, time)` back.
BookingAvailabilitysampleAvailability
value
Picked start (controlled). `null` for none. Pair it with `onValueChange`.
Date | null—
defaultValue
Starting pick when uncontrolled. Its day opens first.
Date | nullnull
onValueChange
Called with the picked start, or `null` when the day changes and the pick is cleared.
(start: Date | null) => void—
onConfirm
The button in the floating summary, with the picked start. Return a promise to show pending, success and error.
(start: Date) => unknown—
confirmLabel
Label of that button. Default "Continue". Same as `labels.confirm`.
string—
onChangeStaff
Shows a "Change" button beside the professional, for going back to the staff step.
() => void—
onBack
Shows a back button in the top bar.
() => void—
minDate
First bookable day. Default today.
Date—
maxDate
Last bookable day. Default 60 days from today.
Date—
markedDates
Dots under days in the week strip, for example days with few times left.
(date: Date) => boolean | number—
focused
Whether this screen is the one on top. The floating summary renders above everything, so pass `false` while another screen covers this one (`useIsFocused()` from expo-router or React Navigation) and it leaves with it. Default `true`.
booleantrue
step
Step of the booking flow in the top bar. `null` hides it. Default step 3 of 4.
BookingStep | null{ current: 3, total: 4 }
title
Headline. Default "Pick a time". Same as `labels.title`.
string—
locale
BCP 47 locale for days and times. Defaults to the device locale.
string—
hour12
Force a 12 or 24 hour clock. Defaults to the locale's.
boolean—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<BookingTimeScreenLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • The summary springs up from the bottom with the first pick, and each new time rolls in from below (TimeSlots floating summary).
  • The picked slot pops, the week strip pill stretches between days, placeholders shimmer while a day loads.
  • Reduced motion: the summary fades instead of sliding, the time crossfades, slots do not pop.
  • Haptic: select when a time or a day is picked
  • Haptic: tap on the summary button, then success or error when onConfirm returns a promise
  • availability runs once per day and staff change (an inline function is fine, it does not refetch on re-render); stale answers from a slow request are ignored when a newer day is picked.
  • The jump to the next free day only searches synchronous availability, so it never fires a request per day.

Accessibility

  • The week strip and the time grid are the kit's WeekStrip and TimeSlots: days are tabs, times are radios with checked and disabled state; taken times read as unavailable.
  • The time grid is named after the day ("Times on Thursday, 6 November").
  • The floating summary is a polite live region, so the picked time and its end are read when they change.
  • A full day says so in text and offers a button to the next day with free times.

Notes for AI

  • availability(day, staffId) returns TimeSlot[] ({ time: 'HH:mm', available, remaining }) or a promise of it. staffId is null for Any available.
  • Slot "HH:mm" strings are moments on the device clock of the day (build them with slotTime(start) from your API's start times; zonedSlotStart(day, '09:00', zone) turns business hours into moments). The grid, its morning/afternoon/evening groups and the summary show them in business.timeZone through Intl, matching the note; set timeZoneCity for the note's city name.
  • An empty or unknown business.timeZone shows the phone's clock and the note says so.
  • onConfirm(start) receives slotStart(day, time), the exact moment. Return a promise only if you hold the slot remotely.
  • Pass duration when add-ons add minutes; otherwise it is the sum of the services.
  • business.closedWeekdays dims closed days in the strip.
  • Hide the navigator header (headerShown: false). The floating summary renders in ThemeProvider's overlay layer, above every screen: pass focused={useIsFocused()} so it leaves when the next screen is pushed.