List row

Free · MITReact Native · Expo

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

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 https://goodui.dev/native/r/list-row.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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.
booleanfalse
disabled
Dims the control and ignores presses.
booleanfalse
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

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