# Service picker screen

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

- Type: Component (screens)
- Access: Pro
- Page: https://goodui.dev/native/components/service-picker-screen
- Markdown: https://goodui.dev/native/components/service-picker-screen.md
- Registry item: https://goodui.dev/native/r/service-picker-screen.json
- Dependencies: react-native-reanimated, react-native-svg
- Keywords: react native booking app, salon booking screen, service menu picker, appointment services list, expo booking flow, add-ons chips, fresha style booking, clinic services, spa menu

## When to use

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

## When not to use

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

## 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 @goodui-native-pro/service-picker-screen
npx expo install --fix
```

Pro: add `"@goodui-native-pro": { "url": "https://goodui.dev/native/r/pro/{name}.json", "headers": { "Authorization": "Bearer ${GOODUI_LICENSE_KEY}" } }` to the `registries` of components.json and put your license key in .env.local.

The component lands in `components/goodui-native/service-picker-screen/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs button, checkbox, chip, empty-state, icon-button, locale, rolling-number, sticky-header-list, time-slots.

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 { 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(",") },
          })
        }
      />
    </>
  );
}
```

## Examples

### Sample salon, no props

```tsx
<ServicePickerScreen />
```

### Controlled cart, one service per visit

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

### ServicePickerScreen

`import { ServicePickerScreen } from "@/components/goodui-native/service-picker-screen/service-picker-screen";`

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

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

## Motion

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

## Haptics

- pop or tick when a service is toggled (Checkbox), select when an add-on is (ChipGroup)
- tap on Continue (Button), then success or error when onContinue returns a promise

## Performance

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

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

## Related

- [Staff picker screen](https://goodui.dev/native/components/staff-picker-screen.md): 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.
- [Booking time screen](https://goodui.dev/native/components/booking-time-screen.md): 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.
- [Booking confirm screen](https://goodui.dev/native/components/booking-confirm-screen.md): 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.
- [Sticky header list](https://goodui.dev/native/components/sticky-header-list.md): A sectioned list whose sticky headers change as they stick: the title shrinks toward the leading edge and a surface with a hairline (frosted on the web) fades in under it. A contacts-style index on the side jumps between sections while a large letter follows the finger, with a tick per letter.
- [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.
- [Rolling number](https://goodui.dev/native/components/rolling-number.md): A number whose digits roll in the direction of change, with digits that open and close in place and currency signs that glide instead of jumping.

## Guidance for AI tools

Service picker screen: 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. 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
