Expandable card

Free · MITReact Native · Expo

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

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/expandable-card.json

Then 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

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

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