Service picker screen

ProReact Native · Expo

Step one of a booking flow: the menu in sticky category sections with duration and price, add-ons that spring up as chips once a matching service is picked, and a total that rolls with every change above the Continue button. Also ships the booking data model shared by every booking 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 @goodui-native-pro/service-picker-screen

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/service-picker-screen.

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

Usage

import { router, Stack } from "expo-router";
import {
  ServicePickerScreen,
  type BookingAddOn,
  type BookingBusiness,
  type BookingService,
} from "@/components/goodui-native/service-picker-screen/service-picker-screen";
import { useMenu } from "@/lib/booking"; // your data, shaped as the types above

export default function ServicesRoute() {
  const { business, services, addOns } = useMenu() as {
    business: BookingBusiness;
    services: BookingService[];
    addOns: BookingAddOn[];
  };
  return (
    <>
      <Stack.Screen options={{ headerShown: false }} />
      <ServicePickerScreen
        business={business}
        services={services}
        addOns={addOns}
        onBack={() => router.back()}
        onContinue={(cart) =>
          router.push({
            pathname: "/book/staff",
            params: { services: cart.serviceIds.join(","), addOns: cart.addOnIds.join(",") },
          })
        }
      />
    </>
  );
}

When to use

Use it for
  • The first step of booking a salon, clinic, studio, spa or tutor: picking what the visit is for.
  • Menus grouped by category where each item has a length and a price and extras depend on what was picked.
  • Any flow where a running total should stay in view while people add and remove items.
Reach for something else
  • Picking a time: use booking-time-screen (or time-slots on your own screen).
  • A product catalog with images and quantities: use product-screen and cart-screen.
  • A single choice between a few plans: use segmented-control or a radio group.

Examples

Sample salon, no props

<ServicePickerScreen />

Controlled cart, one service per visit

const [cart, setCart] = useState<BookingCart>({ serviceIds: [], addOnIds: [] });

<ServicePickerScreen
  value={cart}
  onValueChange={setCart}
  maxServices={1}
  step={{ current: 1, total: 3 }}
  onContinue={(cart, { duration, price }) => saveDraft({ ...cart, duration, price })}
/>

API reference

PropTypeDefault
business
The business being booked. Its name, area and currency show on the screen.
BookingBusinesssampleBusiness
services
The menu. Grouped into sticky sections by `category`, in the order categories first appear.
BookingService[]sampleServices
addOns
Extras offered once a matching service is picked (see `serviceIds` and `categories` on each add-on).
BookingAddOn[]sampleAddOns
value
Picked services and add-ons (controlled). Pair it with `onValueChange`.
BookingCart—
defaultValue
Starting pick when uncontrolled.
BookingCartEMPTY
onValueChange
Called with the new cart on every change. Add-ons that no longer fit the services are dropped.
(cart: BookingCart) => void—
onContinue
Continue button. Return a promise to show pending, success and error on the button.
(cart: BookingCart, summary: ServicePickerSummary) => unknown—
onBack
Shows a back button in the top bar.
() => void—
maxServices
Most services in one visit; the other rows dim once it is reached. Default 3.
number3
step
Step of the booking flow shown in the top bar. `null` hides the step and its progress line. Default step 1 of 4.
BookingStep | null{ current: 1, total: 4 }
title
Headline. Default "Choose services".
string"Choose services"
locale
BCP 47 locale for prices. Defaults to the device locale.
string—
rowHeight
Height of one service row in points. Default 72; raise it for long names or large type.
number72
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • The total is a RollingNumber: digits roll up when something is added and down when it is removed.
  • The add-on row springs up out of the bar when the first matching service is picked and fades out when none fit; the summary row slides to make room.
  • Section headers shrink as they stick (StickyHeaderList), checks draw themselves in (Checkbox), the current step segment fills on a spring when the screen appears.
  • Reduced motion: the add-on row fades instead of springing, the summary row does not slide, the step segment shows full; the components inside follow their own reduced-motion rules.
  • Haptic: pop or tick when a service is toggled (Checkbox), select when an add-on is (ChipGroup)
  • Haptic: tap on Continue (Button), then success or error when onContinue returns a promise
  • The menu is a virtualized SectionList with fixed row heights (rowHeight), so long menus scroll and jump without measuring.
  • Rows read the selection from context, so a pick re-renders only what changed; totals are derived with useMemo.

Accessibility

  • Each service is a checkbox row (role checkbox with checked and disabled state): name, then length and price.
  • On native a service's description is added to its spoken label; the web names the row from its visible text.
  • Category headers have the header role; the step line in the top bar is a progressbar with min, max and current step (aria-valuenow on the web).
  • Add-ons are a ChipGroup in multiple mode, named Add-ons. The Continue button carries the count, length and total as its hint.
  • Rows past maxServices are disabled rather than hidden, so the menu never jumps.

Notes for AI

  • Pass business, services and addOns in the shapes exported from booking-types.ts; every booking screen reads the same types.
  • onContinue(cart, summary) gets { serviceIds, addOnIds } and { items, duration, price, priceFrom }. Navigate to the staff step there; return a promise only if you save the draft remotely.
  • Add-ons show only when they fit a picked service (serviceIds or categories on the add-on). Unfitting add-ons are dropped from the cart automatically.
  • Keep service names short enough for one line at the default rowHeight of 72; raise rowHeight for long names or large type.
  • Hide the navigator header (headerShown: false); the screen draws its own top bar with back button and step progress.
  • booking-types.ts also exports formatPrice, formatDuration, lineItemsFor, totalsOf, slotStart, slotTime, zonedSlotStart, zoneOffset and a sample salon (sampleBusiness, sampleServices, sampleAddOns, sampleStaff, sampleAvailability, sampleAppointments) for prototypes.