Expandable card
Free · MITReact Native · ExpoA card that opens into its detail: inline it grows from its own measured height while the preview cross-fades into the detail under a header that never moves; fullscreen the card itself lifts out and stretches to fill the screen, and a swipe down shrinks it back into its place.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/expandable-card.jsonThen run npx expo install --fix so react-native-gesture-handler, react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/expandable-card.
Usage
import { Text, View } from "react-native";
import { ExpandableCard } from "@/components/goodui-native/expandable-card/expandable-card";
export function BookingCard() {
return (
<ExpandableCard
accessibilityLabel="Massage, Thursday 14:30"
header={
<View>
<Text>Deep tissue massage</Text>
<Text>Thu 14:30 · Studio Alma</Text>
</View>
}
preview={<Text>60 min with Rita · €70, paid</Text>}
detail={
<View style={{ gap: 12 }}>
<Text>Therapist: Rita Sousa</Text>
<Text>Address: Rua da Rosa 12, Lisbon</Text>
</View>
}
/>
);
}When to use
- Summaries in a list that hold more detail: a booking, a transaction, an order receipt (inline).
- Feature or content cards that open into a full page of their own: a workout plan, a story, a trip (fullscreen).
- When the detail should feel like the same object, not a new screen pushed on top.
- A stack of plain text rows that open and close: use accordion.
- Actions or options for an item: use action-sheet or context-menu.
- Detail that needs its own navigation history or deep links: push a screen.
- Content that rises from the bottom with detents: use bottom-sheet.
Examples
Fullscreen with swipe down to close
<ExpandableCard
mode="fullscreen"
accessibilityLabel="Leg day, 55 minutes"
header={<WorkoutHero workout={w} />}
preview={<Text>Tap for the plan</Text>}
detail={<WorkoutPlan workout={w} />}
/>Controlled
const [open, setOpen] = useState(false);
<ExpandableCard expanded={open} onExpandedChange={setOpen} header={<OrderHeader />} detail={<Receipt />} />
<Button onPress={() => setOpen((o) => !o)}>{open ? "Hide receipt" : "Show receipt"}</Button>API reference
| Prop | Type | Default |
|---|---|---|
| header* Shown in both states and never moves: title, price, avatar. Keep it non-interactive. | ReactNode | — |
| preview Collapsed body under the header, such as a summary line. Keep it non-interactive: the card handles the tap. | ReactNode | — |
| detail* Expanded body. It may hold buttons and inputs. Mounted on open, unmounted after it closes. | ReactNode | — |
| mode `inline` grows in place and pushes the content below; `fullscreen` lifts out and fills the screen. | "inline" | "fullscreen" | "inline" |
| expanded Expanded state (controlled). Pair it with `onExpandedChange`. | boolean | — |
| defaultExpanded Expanded state on first render (uncontrolled). | boolean | false |
| onExpandedChange Called when a tap, the close button, a swipe down, Escape or the back button changes it. | (expanded: boolean) => void | — |
| variant `elevated` surface with a soft shadow, `outline` with a hairline, `filled` quiet fill. | "filled" | "outline" | "elevated" | — |
| haptic Haptic when the card opens or closes. `false` turns it off. | HapticName | false | "tap" |
| closeLabel Label of the fullscreen close button. Same as `labels.close`. | string | — |
| accessibilityLabel Name of the card for screen readers on native. The web reads the header's text. | string | — |
| disabled Dims the control and ignores presses. | boolean | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<ExpandableCardLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Inline: the body's height springs on the smooth spring from the measured preview height to the measured detail height; the preview fades out over the first half while the detail fades in and drops 8 pt into place; the chevron turns. Detail that changes size while open eases to the new height.
- Fullscreen: the card's own frame (position, size and corner radius) animates on the smooth spring from its place in the list to the full screen while the scrim fades in; the header moves down under the status bar, the preview fades out, the detail rises 24 pt and fades in, the close button fades in last.
- Swipe down (with the detail scrolled to the top): the card follows at half the finger's distance, shrinks up to 12 % and rounds its corners; past 120 pt or a fast flick it shrinks back into the list, otherwise it springs back with the release velocity.
- Press: the card scales down slightly on touch down on the snappy spring.
- Reduced motion: inline height changes at once and the bodies cross-fade; fullscreen opens and closes as a fade with no travel; no press scale.
- Haptic: tap when the card opens or closes (configurable with haptic)
- Heights, frames and fades run on the UI thread; React state changes when the card opens, when the detail first mounts and when it unmounts.
- Inline detail mounts on open and unmounts after it closes; the fullscreen layer exists only while open or animating.
Accessibility
- Inline: the header is a button with expanded in accessibilityState and aria-expanded on the web; the preview is a second tap target hidden from screen readers. The collapsed detail is unmounted.
- Fullscreen: the whole card is a button with expanded state. The open card is a modal dialog (accessibilityViewIsModal, role dialog with aria-modal on the web) with a labelled close button, and opening is announced.
- Escape closes on the web and the back button on Android. The detail scroll area takes keyboard focus on the web so it can be scrolled with arrow keys.
- accessibilityLabel names the card on native; on the web the header's visible text names it.
Notes for AI
- header and preview must be non-interactive (the card handles the tap); buttons and inputs go in detail.
- Use mode='fullscreen' for rich detail pages and the default inline mode for receipts and summaries in a list.
- In fullscreen the detail renders inside a ScrollView with padding; do not wrap it in another ScrollView.
- Fullscreen measures the card on screen; it works inside ScrollViews and lists, and the card hides in place while it is open.
- Do not nest an ExpandableCard inside another card surface; place it on the screen background.
Related
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.
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.
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.
A snapping card carousel that follows the finger 1:1, shrinks and dims the neighbours, stretches its pagination pill with the drag, and turns that pill into a timer when it autoplays.