Booking time screen
ProReact Native · ExpoStep 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.
Installation
$ npx shadcn@latest add @goodui-native-pro/booking-time-screenThen 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
- 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.
- 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
| Prop | Type | Default |
|---|---|---|
| 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. | BookingBusiness | sampleBusiness |
| 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 | null | sampleStaff[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. | BookingAvailability | sampleAvailability |
| value Picked start (controlled). `null` for none. Pair it with `onValueChange`. | Date | null | — |
| defaultValue Starting pick when uncontrolled. Its day opens first. | Date | null | null |
| 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`. | boolean | true |
| 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.
Related
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.
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.
Step two of a booking flow: an Any available card and a card per professional with photo, rating and next free time. One accent ring springs from card to card, its edges on different springs so it stretches toward the pick.
The last step of a booking flow: a summary card, client details, notes and the cancellation policy, booked with a slide (or a button). The slider spins while the request runs, then the screen gives way to a success check with confetti, Add to calendar and Done.
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.