List row
Free · MITReact Native · ExpoSettings and navigation cells with a token-colored icon tile, title, subtitle and a trailing value, switch, badge, check or chevron, grouped in inset sections with headers and footers. On touch down the highlight floods in, the tile sinks and the chevron leans forward; on release the highlight lingers and fades.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/list-row.jsonThen run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/list-row.
Usage
import { ListRow, ListSection } from "@/components/goodui-native/list-row/list-row";
import { router } from "expo-router";
import { useState } from "react";
import Svg, { Path } from "react-native-svg";
const bell = (color: string, size: number) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path d="M6 16v-5a6 6 0 0 1 12 0v5l1.5 2h-15ZM10 20.5a2 2 0 0 0 4 0" stroke={color} strokeWidth={2} strokeLinejoin="round" />
</Svg>
);
export function SettingsScreen() {
const [push, setPush] = useState(true);
return (
<ListSection header="Notifications" footer="We only send booking reminders and receipts.">
<ListRow title="Push notifications" icon={bell} iconTone="accent" switchValue={push} onSwitchChange={setPush} />
<ListRow title="Reminder time" value="1 hour before" onPress={() => router.push("/settings/reminders")} />
</ListSection>
);
}When to use
- Settings screens: account, notifications, privacy, each row leading to a detail screen or holding a switch.
- Profile and account menus with a value on the right: plan, language, payment method.
- Short pickers where one row is checked: language, currency, appearance.
- A destructive row at the end of a group: sign out, delete account.
- Rows that reveal content in place: use accordion.
- Rows with swipe gestures: wrap the row content in swipe-actions.
- Long, sectioned, scrolling data such as contacts: use sticky-header-list.
- Rows the person sorts by dragging: use reorderable-list.
- Two to five mutually exclusive views: use segmented-control.
Examples
A picker with a drawn check
<ListSection header="Currency">
{(["EUR", "USD", "GBP"] as const).map((c) => (
<ListRow key={c} title={c} checked={currency === c} onPress={() => setCurrency(c)} />
))}
</ListSection>Async row and destructive row
<ListSection>
<ListRow title="Restore purchases" onPress={() => api.restore()} chevron={false} />
<ListRow title="Sign out" destructive onPress={signOut} />
</ListSection>Badge and soft tile
<ListRow title="Messages" icon={chat} iconTone="accent" iconVariant="soft" badge={3} onPress={openMessages} />API reference
ListRow
| Prop | Type | Default |
|---|---|---|
| title* Row title. Sentence case; wraps to two lines before it truncates. | string | — |
| subtitle Second line under the title. | string | — |
| icon Icon in a colored tile at the leading edge. A function gets the tile's foreground color and icon size. | ListRowIcon | — |
| iconTone Tile color. `neutral` gray by default; the accent or a status only when it means something. | "success" | "warning" | "danger" | "accent" | "neutral" | — |
| iconVariant `solid` tile with an on-color icon (iOS Settings), or `soft` tinted tile with a colored icon. | "solid" | "soft" | "solid" |
| value Trailing value text, for example "Wi-Fi name", "English" or "€12.00". | string | — |
| badge Trailing badge: a count (unread, updates) or a short status label such as "New". | number | string | — |
| chevron Trailing chevron. Default: shown when the row has `onPress` and is not destructive, checkable or a switch. | boolean | — |
| checked Makes the row a radio choice with a check that draws in when selected. Use for pickers like language. | boolean | — |
| switchValue Puts a Switch in the trailing slot (controlled). The switch carries the title as its label. | boolean | — |
| defaultSwitchValue Switch start state when uncontrolled. | boolean | — |
| onSwitchChange Called with the new switch state. Return a promise for an optimistic switch with rollback. | (value: boolean) => unknown | — |
| trailing Any trailing element instead of value, badge and chevron. Keep it non-interactive unless the row has no `onPress`. | ReactNode | — |
| onPress Tap on the row. Return a promise and the trailing slot shows a spinner until it settles; a rejection shows an error mark with an error haptic. | () => unknown | — |
| onLongPress Long press on the row, for example to copy the value. | () => void | — |
| destructive Red title (and danger tile) for an action that removes or signs out. No chevron. | boolean | false |
| disabled Dims the control and ignores presses. | boolean | false |
| size `sm` 44 pt rows with 28 pt tiles, `md` 52 pt rows with 32 pt tiles. | "sm" | "md" | "md" |
| haptic Haptic on press. Default: `select` for checkable rows, none for navigation rows. | HapticName | false | — |
| accessibilityLabel Screen reader label on native. Defaults to title, subtitle, value and badge. The web reads the visible text: a pressable row there is a button named by its text, so a custom label applies to static rows only. | string | — |
| accessibilityHint Describes what happens on activation, read after the label. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<ListRowLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
ListSection
| Prop | Type | Default |
|---|---|---|
| children* Rows. Each ListRow inside draws its own hairline, inset to line up with the title. | ReactNode | — |
| header Short heading above the group, sentence case. | string | — |
| footer Note under the group: a string, or a node such as a text with a link. | ReactNode | — |
| variant `inset` is a rounded card inside the screen gutter (iOS inset grouped); `plain` runs edge to edge. | "inset" | "plain" | "inset" |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Touch down: the highlight fades in within 80 ms, the icon tile scales to 0.9 and the chevron moves 3 pt forward, both on the snappy spring.
- Release: the highlight fades out over 320 ms so a quick tap still reads; tile and chevron spring back.
- Checking a row draws the check in; the first render shows it complete.
- Reduced motion: no tile scale or chevron travel, the check appears without drawing; the highlight still fades.
- Haptic: select on press for checkable rows by default, none for navigation rows (set haptic to change it)
- Haptic: error when an async onPress rejects
- Haptic: the Switch in a switch row plays its own toggle-on or toggle-off cue
- Press feedback runs on the UI thread with shared values; React state only changes for async status.
- Rows are plain views: for hundreds of rows render them in a FlatList or use sticky-header-list.
Accessibility
- A pressable row is a button; with `checked` it is a radio with checked in accessibilityState and aria-checked on the web. Disabled and busy (while an async onPress runs) are set in both.
- On native the row reads one label built from title, subtitle, value and badge. On the web the visible text names it, so the name always starts with the title; a custom accessibilityLabel applies there to static rows only, never to a pressable one.
- A switch row is not a button: the Switch inside is the control, labelled with the title (and subtitle). The row's text stays readable.
- Section headers are headers. Rows are at least 44 pt tall.
- A failed async press is announced ('<title> failed') and shown as an error mark.
Notes for AI
- Always put rows inside ListSection when they form a group; it draws the surface, rounded corners and the inset hairlines. A ListRow alone has no background.
- Pass icons as functions `(color, size) => <Icon />` so the tile picks the right color for solid and soft variants.
- A switch row does not take onPress; use switchValue + onSwitchChange (return a promise for a server setting).
- chevron defaults on for pressable rows; pass chevron={false} for actions that do not navigate.
- Do not nest Pressables in `trailing` of a pressable row.
- Tile tones: neutral or accent for most rows; success, warning and danger only when the row means that status (danger for destructive rows).
Related
An iOS-style switch with squash and stretch: the thumb stretches toward where it is going on touch down, then travels on a spring while the track color follows. Settings-row layout, tones, sizes and optimistic async changes.
A count, dot or status badge that springs in when it appears, pops when its value changes, caps at 99+ and shrinks away at zero; BadgeAnchor pins it to an icon or avatar.
Collapsible rows whose panels open from their measured height on the UI thread, with content that drops into place and a chevron that turns on the same spring.
Row actions that unfold with the finger; a long swipe lets the edge action take over and run, and destructive actions collapse the row before it leaves.
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.