The generic list row: an optional leading slot, a title, an optional subtitle, a trailing slot, and a chevron when it goes somewhere.
Light
In a flush card: title, subtitle, leading, trailing, chevron
On the page: no card, no hairlines for five rows or fewer
Dark
In a flush card: title, subtitle, leading, trailing, chevron
On the page: no card, no hairlines for five rows or fewer
Source:
Row.tsx. How lists are built from it: index-list.
When to use it
- Any list of things you open: techniques, students, classes, settings.
- A static fact with a label and a value, with
showChevron={false}.
Use FeedRow for the Social feed and SessionRow for training history — they are this row with
feature-specific content. Do not build a third.
Anatomy
| Part | Token |
|---|---|
| Row floor | controlHeight.row 64 — a floor, never a fixed height |
| Vertical padding | spacing.s12, on the content column |
| Gaps | spacing.s12 leading to text and text to trailing; spacing.s4 title to subtitle |
| Title | typography.rowTitle 16/600, text.primary |
| Subtitle | typography.caption 13/400, text.secondary |
| Chevron | caretRight at iconSize.inline 16, text.secondary |
| Divider | 1px border.hairline on surface.card, inset to the text edge, dropped on the last row (isLast) |
The hairline belongs to the content column, so it starts where the words start and never runs under the avatar. On the page, five rows or fewer get no hairlines at all — 12pt between groups.
States
Light
Default
Pressed
Focus
Dark
Default
Pressed
Focus
- Pressed:
opacity.pressed0.85 across the row. - Focus: a 2px
border.focusring. - Static: no press, no chevron, still one grouped element for a screen reader.
- Disabled: none. A row that cannot open says why in its subtitle.
- Loading: skeleton rows of the same shape, in the list that owns them.
On web the ring is the browser's. Row does not draw its own focus ring; on RN-web it relies on
the browser's default focus outline. Input, SearchField and SelectTrigger draw the 2px
border.focus themselves; the specimen shows the ring this component owes.
Content
- The subject is first and boldest. Metadata goes in the subtitle, never above the title.
- Neither line truncates. A long name wraps and the row grows.
- A count in the trailing slot is
captionintext.secondary; a status is a Badge.
Accessibility
- A pressable row is role
buttonby default and reads title and subtitle together. - A static row groups its lines by hand (
accessibleplus a label), so it is read once, not as three fragments. trailingInteractivesplits the row when the trailing slot holds something tappable — a ⋯ or a switch. On iOS a pressable fuses its children into one element, so a nested button is unreachable by VoiceOver. With the flag the text column becomes the pressable and the trailing control stays its own stop.- The subtitle is
text.secondary, nottext.tertiary, because a row sits directly onsurface.pageas often as on a card, andtext.tertiaryonsurface.pageis 4.23:1, under the 4.5:1 body text needs.
Do and don't
Do
The thing they are looking for is first and boldest.
Don’t
Metadata louder than its subject. The name is the row.
Code
import { Avatar, Card, Row, Text } from '../../components/ui';
<Card padded={false}>
<Row
title="Ana Torres"
subtitle="Green · 14 months"
leading={<Avatar name="Ana Torres" size={32} />}
onPress={() => openStudent(ana.id)}
testID="roster-row-ana"
/>
<Row title="Hand techniques" trailing={<Text variant="caption" tone="secondary">16</Text>} onPress={openHand} isLast />
</Card>