Card
Card surface with CardHeader, CardTitle, CardDescription, CardContent and CardFooter; onPress makes the whole card pressable.
Light
Dark
Your app is ready.
Inter body text, Inter headings.
a0: Vega, neutral, neutral accent, default radius, Inter.Change PresetInstallation
npx nativecn-cli@latest add cardAlso installs: theme, pressable, text.
Usage
Card demo
A Card with header, content and footer, and a pressable Card.
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 },
}));Card demo
A Card with header, content and footer, and a pressable Card.
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
The Vega Style's code, exactly as add copies it into your app.
{components}/card.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} />;
}{components}/card.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.lg,
padding: t.spacing[4],
gap: t.spacing[3],
borderWidth: t.borderWidth.default,
},
backgroundColor: t.colors.card,
borderColor: t.colors.border,
borderCurve: "continuous",
},
pressed: { filter: [{ brightness: 0.96 }], transform: [{ scale: 0.98 }] },
disabled: { opacity: t.opacity.disabled },
header: { gap: t.spacing[1] },
title: {
...t.type.h4,
fontSize: t.scaleValue(15),
lineHeight: t.scaleValue(20),
},
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} />;
}
