# Card

> Card surface with CardHeader, CardTitle, CardDescription, CardContent and CardFooter; onPress makes the whole card pressable.

Component · display. Registry item `card`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/card.json

## Installation

```sh
npx nativecn-cli@latest add card
```

Also installs: [theme](https://nativecn.dev/docs/theme/theme), [pressable](https://nativecn.dev/docs/primitives/pressable), [text](https://nativecn.dev/docs/components/text).

## Usage

### Card demo

A Card with header, content and footer, and a pressable Card.

```tsx
import { View } from "react-native";

import { Badge } from "@/components/badge";
import { Button } from "@/components/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/card";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";

export default function CardDemo() {
  const styles = useStyles();
  return (
    <View style={styles.stack}>
      <Card>
        <CardHeader>
          <CardTitle>Pro plan</CardTitle>
          <CardDescription>Everything in Free, plus unlimited projects.</CardDescription>
        </CardHeader>
        <CardContent>
          <Badge label="Most popular" variant="secondary" />
          <Text>$12 per month, billed yearly.</Text>
        </CardContent>
        <CardFooter>
          <Button label="Upgrade" style={styles.grow} />
        </CardFooter>
      </Card>

      {/* With onPress the whole card is one pressable target. */}
      <Card onPress={() => {}}>
        <CardHeader>
          <CardTitle>Order #1042</CardTitle>
          <CardDescription>Arrives Thursday</CardDescription>
        </CardHeader>
      </Card>
    </View>
  );
}

const useStyles = createStyles((t) => ({
  stack: { gap: t.spacing[4] },
  grow: { flex: 1 },
}));
```

## Props

### Card

| Prop | Type and description |
|---|---|
| `children` | CardHeader, CardContent, CardFooter or anything else |
| `onPress` | (event) => void: makes the whole card one pressable target (role button) |
| `onLongPress` | (event) => void |
| `disabled` | boolean (default false): pressable cards only; dims and blocks presses |
| `haptic` | "selection" \| "light": pressable cards only |
| `style` | layout only, merged last onto the root |

### CardHeader

| Prop | Type and description |
|---|---|
| `children` | CardTitle and CardDescription |

### CardTitle

| Prop | Type and description |
|---|---|
| `children` | the title text (role heading); Text props except variant |

### CardDescription

| Prop | Type and description |
|---|---|
| `children` | muted supporting text; Text props except variant |

### CardContent

| Prop | Type and description |
|---|---|
| `children` | the main content |

### CardFooter

| Prop | Type and description |
|---|---|
| `children` | actions, laid out in a row |

## Accessibility

- With onPress or onLongPress the whole card is one button, named by its text.
- A pressable card is taller than the 48 touch target, so its tap area is not extended.
- disabled is announced as disabled and blocks presses.
- CardTitle has role heading (the h4 text Variant).

## Notes

Compose shadcn-style from named exports (no dot syntax): <Card><CardHeader><CardTitle>…</CardTitle><CardDescription>…</CardDescription></CardHeader><CardContent>…</CardContent><CardFooter>…</CardFooter></Card>. With onPress the whole card is a single pressable, so don't nest other pressables inside a pressable Card. Padding, gap, radius, border and shadow come from the card.root Style Slot; the pressed look from card.pressed.

## Source

### {components}/card.tsx

```tsx
import type { ReactNode, Ref } from "react";
import {
  View,
  type GestureResponderEvent,
  type StyleProp,
  type ViewProps,
  type ViewStyle,
} from "react-native";

import { Pressable, type PressableHaptic } from "@/components/primitives/pressable";
import { Text, type TextProps } from "@/components/text";
import { createStyles } from "@/theme";

export type CardProps = Omit<ViewProps, "style" | "hitSlop"> & {
  children?: ReactNode;
  /** Makes the whole card one pressable target (role `button`, pressed look from `card.pressed`). */
  onPress?: (event: GestureResponderEvent) => void;
  onLongPress?: (event: GestureResponderEvent) => void;
  /** Only for a pressable card: blocks presses, dims it and announces it as disabled. */
  disabled?: boolean;
  /** Only for a pressable card: haptic feedback on press (when `config.haptics` is on). */
  haptic?: PressableHaptic;
  /** Layout only, merged last onto the root. */
  style?: StyleProp<ViewStyle>;
  ref?: Ref<View>;
};

export type CardSectionProps = ViewProps & { children?: ReactNode; ref?: Ref<View> };

export type CardTitleProps = Omit<TextProps, "variant">;
export type CardDescriptionProps = Omit<TextProps, "variant">;

const useStyles = createStyles((t) => ({
  root: {
    ...{
    borderRadius: t.radius.xl,
    padding: t.spacing[6],
    gap: t.spacing[4],
    borderWidth: t.borderWidth.default,
    boxShadow: t.elevation.sm,
  },
    backgroundColor: t.colors.card,
    borderColor: t.colors.border,
    borderCurve: "continuous",
  },
  pressed: { filter: [{ brightness: 0.96 }] },
  disabled: { opacity: t.opacity.disabled },
  header: { gap: t.scaleValue(6) },
  title: { ...t.type.h4 },
  content: { gap: t.spacing[2] },
  footer: { flexDirection: "row", alignItems: "center", gap: t.spacing[2] },
}));

/**
 * A surface that groups related content. Compose it from CardHeader, CardTitle, CardDescription,
 * CardContent and CardFooter. With `onPress` the whole card becomes one pressable target, built
 * on the Pressable Primitive. `style` is merged last onto the root.
 */
export function Card({
  children,
  onPress,
  onLongPress,
  disabled = false,
  haptic,
  style,
  ...props
}: CardProps) {
  const styles = useStyles();

  if (onPress || onLongPress) {
    return (
      <Pressable
        onPress={onPress}
        onLongPress={onLongPress}
        disabled={disabled}
        haptic={haptic}
        // A card is taller than the 48 touch target: no tap-area extension, no layout pass.
        hitSlop={0}
        pressedStyle={styles.pressed}
        style={[styles.root, disabled ? styles.disabled : null, style]}
        {...props}
      >
        {children}
      </Pressable>
    );
  }

  return (
    <View style={[styles.root, style]} {...props}>
      {children}
    </View>
  );
}

/** Holds the CardTitle and CardDescription. */
export function CardHeader({ style, ...props }: CardSectionProps) {
  const styles = useStyles();
  return <View style={[styles.header, style]} {...props} />;
}

/** The card's heading (role `heading`), from the `card.title` Slot. */
export function CardTitle({ style, ...props }: CardTitleProps) {
  const styles = useStyles();
  return <Text variant="h4" style={[styles.title, style]} {...props} />;
}

/** Supporting text under the CardTitle, in the muted foreground. */
export function CardDescription({ color = "mutedForeground", ...props }: CardDescriptionProps) {
  return <Text variant="small" color={color} {...props} />;
}

/** The card's main content. */
export function CardContent({ style, ...props }: CardSectionProps) {
  const styles = useStyles();
  return <View style={[styles.content, style]} {...props} />;
}

/** A row of actions at the bottom of the card. */
export function CardFooter({ style, ...props }: CardSectionProps) {
  const styles = useStyles();
  return <View style={[styles.footer, style]} {...props} />;
}
```
