Expokit
AboutDocsComponentsChartsBlocksPricing
Sign inGet started
All components

Inputs

CalendarNewDate PickerNewFloating InputOTP InputPassword InputSearch BarSelectSliderStepperTag InputTextareaTime PickerNew

Selection

CheckboxChip GroupColor SwatchProRadio GroupRatingProSegmented ControlSwitch

Buttons

ButtonFloating Action ButtonProLike ButtonProMorph ButtonProShimmer ButtonProSlide to ConfirmProStateful Button

Feedback

AlertProgressSkeletonSpinnerToast

Overlays

Action SheetNewBottom SheetDialogDrawerNew

Display

AccordionAvatarBadgeCardCarouselProList ItemNewPagination DotsNewSwipeable RowProTimelineNew

Navigation

Bottom NavNewProgress StepsNewTabs

Motion

Number TickerProText Animation
Card
Sponsor ExpoKit

Free stays free. Tip once — keep shipping.

Support
About
Docs
IntroductionWhat ExpoKit is and how it worksInstallationAdd ExpoKit to an Expo appThemingColours, radius and dark modeCLIinit, add, login and updateAI assistants (MCP)Cursor, Claude, Antigravity and moreRun on your phoneTry components in Expo GoChangelogWhat shipped recently
npx @expokit/cli@latest initQuick start
Components
Liquid Glass8 iOS 26 glass pieces3DNew14 tilt, flip and stack
Inputs 12
  • CalendarNew
  • Date PickerNew
  • Floating Input
  • OTP Input
  • Password Input
  • +7 more
Selection 7
  • Checkbox
  • Chip Group
  • Color SwatchPro
  • Radio Group
  • RatingPro
  • +2 more
Buttons 7
  • Button
  • Floating Action ButtonPro
  • Like ButtonPro
  • Morph ButtonPro
  • Shimmer ButtonPro
  • +2 more
Feedback 5
  • Alert
  • Progress
  • Skeleton
  • Spinner
  • Toast
Overlays 4
  • Action SheetNew
  • Bottom Sheet
  • Dialog
  • DrawerNew
Display 9
  • Accordion
  • Avatar
  • Badge
  • Card
  • CarouselPro
  • +4 more
Navigation 3
  • Bottom NavNew
  • Progress StepsNew
  • Tabs
Motion 2
  • Number TickerPro
  • Text Animation
70 components · 347 live variantsAll components
Charts

Cartesian

  • Area Chart
  • Bar Chart
  • Line Chart
  • Combo Chart

Circular

  • Pie & Donut
  • Radial Chart
  • Radar Chart
  • Polar AreaPro

3D & Pixel

  • Isometric BarsPro
  • Pixel BarsPro

Specialty

  • TreemapPro
  • WaterfallPro
  • BulletPro
  • TimelinePro
  • Gauge
  • Candlestick
  • Heatmap
  • Funnel

Classic

  • Sparkline
  • Progress Rings
  • MultiLine (classic)
  • StackedBar (classic)
  • Ranking Bars (classic)

Guides

  • Usage & config
  • Patterns & effects
  • Show / hide anything
  • Chart theming
  • Interactions
23 charts, built on the open-source expo-charts engineAll charts
Blocks

Screens

ChatProAn AI assistant chat screenCheckoutProA premium checkoutDashboardProA finance / analytics home screenInboxProA messaging inboxOnboardingProA minimal swipeable welcome flowProfileProAccount profile for any appSearch ResultsProA clean, monochrome search screenSettingsProAn iOS-style grouped settings screenSign InProOne configurable sign-in flow modelled on top mobile apps
9 complete screens — drop one in, wire your dataSee them on devices All blocks
Pricing
DocsComponentsDisplayCard

Card

Scan with Expo Go

Card with all its variants, native in Expo Go. No account needed.

No Expo Go? Open in your browser

A composable card that springs down on press, with a spotlight that follows your finger.

<Card />4 variantsiOS · Android · Expo Go
9:41

Sunday 18 October

9:41

Installation#

One command copies the source into your app — you own the code and can change anything.

Usage#

Colours, radius and light / dark come from useKit(). Wrap your app in <KitProvider accent="#6366f1" radius={14}> to re-skin every component at once.

Props#

PropTypeDefault
variant

Surface style. Glass is translucent with a light edge — place it over colour or imagery.

"default" | "outline" | "elevated""default"
onPress / onLongPress

Makes the card a button with a spring press.

(e) => void—
pressable

Spring press without an onPress.

booleanfalse
highlight

Radial glow that tracks the finger, or an accent border that glows in.

"none" | "spotlight" | "border""none"
accent

Highlight colour.

stringtheme accent
padding

Inner padding.

number18
gap

Space between sections.

number14
disabled

Dims and blocks presses.

booleanfalse
Card.Header action

Element pinned to the header's top-right.

ReactNode—
Card.Title / Description numberOfLines

Clamp the text to this many lines with an ellipsis.

number—
Card.Content / Footer

Plain section view / bottom row of actions (row, 10pt gap); `style` overrides either.

{ children, style? }—
Card.Media source / height

Full-bleed image; place it first.

ImageSource / number— / 168
style

Style for the card surface (merged after the variant background, border and radius).

StyleProp<ViewStyle>—

Behaviour & accessibility#

Touch
With onPress, onLongPress or pressable it scales down with a spring and bounces back on release.
Motion
highlight="spotlight" springs in a soft glow under your finger; highlight="border" fades in a glowing accent ring.
Screen readers
Becomes a button with disabled state when it has onPress or onLongPress. Card.Title is a header.
Web
Shows a pointer cursor when it has onPress.

Source#

PreviousBadgeNextCarousel

Did you like the content?

Coding agents

Chat apps

Other

9:41

Default

Card

Manual installation
  1. Install dependencies

    terminal
    npx expo install react-native-svg
  2. Copy the files

    Create each file at the same path and paste its code. @/ points at your src/ folder (the default in new Expo apps). Skip any file you already have, like lib/kit.tsx.

    • src/components/ui/card.tsx301 lines
    • src/lib/kit.tsx220 lines

    Want to read it first? Every file is in Source below.

  3. Use it

    App.tsx
    import { Card } from "@/components/ui/card";

    See Usage for a full example.

Example.tsx
import { Card } from '@/components/ui/card'; <Card variant="elevated" highlight="spotlight" onPress={openGuide}>  <Card.Header>    <Card.Title>Kyoto in autumn</Card.Title>    <Card.Description>Temples, maple-lined paths and the best matcha spots.</Card.Description>  </Card.Header></Card>

Every variant above has a complete, runnable example in its Code tab.

src/components/ui/card.tsx
import React, { createContext, useContext, useId, useRef, useState, type ComponentRef } from 'react';import {  Animated,  Image,  Platform,  Pressable,  StyleSheet,  Text,  View,  type GestureResponderEvent,  type ImageSourcePropType,  type LayoutChangeEvent,  type StyleProp,  type TextStyle,  type ViewStyle,} from 'react-native';import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg'; import { alpha, motion, useKit } from '@/lib/kit'; export type CardVariant = 'default' | 'outline' | 'elevated';export type CardHighlight = 'none' | 'spotlight' | 'border'; export interface CardProps {  children?: React.ReactNode;  variant?: CardVariant;  /** Inner padding in points (default 18). Sections are spaced by `gap`. */  padding?: number;  /** Space between Header, Content and Footer (default 14). */  gap?: number;  /** Makes the whole card a button with spring press feedback. */  onPress?: (e: GestureResponderEvent) => void;  onLongPress?: (e: GestureResponderEvent) => void;  /** Spring press feedback without an onPress (e.g. the card wraps its own buttons). */  pressable?: boolean;  /** Light up on press: a soft spotlight that follows the finger, or a glowing accent border. */  highlight?: CardHighlight;  /** Colour of the highlight (default: theme accent). */  accent?: string;  disabled?: boolean;  accessibilityLabel?: string;  style?: StyleProp<ViewStyle>;} const CardContext = createContext({ padding: 18 }); /** * Surface for grouping content. Compose with Card.Header, Card.Title, Card.Description, * Card.Content, Card.Media and Card.Footer. Optional spring press with a finger-following spotlight. */export function Card({  children,  variant = 'default',  padding = 18,  gap = 14,  onPress,  onLongPress,  pressable,  highlight = 'none',  accent,  disabled,  accessibilityLabel,  style,}: CardProps) {  const { colors, radius, dark } = useKit();  const tint = accent ?? colors.accent;  const interactive = !!(onPress || onLongPress || pressable);  const corner = radius.scale(18);  const gid = 'card' + useId().replace(/[^a-zA-Z0-9]/g, '');   const scale = useRef(new Animated.Value(1)).current;  const glow = useRef(new Animated.Value(0)).current;  const spot = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;  const [size, setSize] = useState({ w: 0, h: 0 });  const spotD = Math.max(size.w, size.h) * 1.1;   const surfaces: Record<CardVariant, ViewStyle> = {    default: {      backgroundColor: colors.card,      borderWidth: StyleSheet.hairlineWidth * 2,      borderColor: colors.border,      ...shadow(dark ? 0 : 0.04, 2, 6),    },    outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.border },    elevated: { backgroundColor: dark ? colors.popover : colors.card, ...shadow(dark ? 0.5 : 0.1, 10, 28, 8) },  };   const border = (surfaces[variant].borderWidth as number | undefined) ?? 0;   const pressIn = (e: GestureResponderEvent) => {    Animated.spring(scale, { toValue: 0.975, useNativeDriver: true, ...motion.snappy }).start();    if (highlight === 'none') return;    moveSpot(e);    Animated.spring(glow, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start();  };  const pressOut = () => {    Animated.spring(scale, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();    if (highlight !== 'none') Animated.timing(glow, { toValue: 0, duration: 420, useNativeDriver: true }).start();  };  // Touch coordinates are relative to whichever child was hit, so work from page coordinates.  const clipRef = useRef<ComponentRef<typeof View>>(null);  const origin = useRef({ x: 0, y: 0 });  const placeSpot = (pageX: number, pageY: number) =>    spot.setValue({ x: pageX - origin.current.x - spotD / 2, y: pageY - origin.current.y - spotD / 2 });  const moveSpot = (e: GestureResponderEvent) => {    const { pageX, pageY } = e.nativeEvent;    clipRef.current?.measure((_x, _y, _w, _h, px, py) => {      origin.current = { x: px, y: py };      placeSpot(pageX, pageY);    });  };  const followSpot = (e: GestureResponderEvent) => placeSpot(e.nativeEvent.pageX, e.nativeEvent.pageY);   const content = (    <View ref={clipRef} style={[styles.clip, { borderRadius: Math.max(0, corner - border) }]}>      {highlight === 'spotlight' && spotD > 0 ? (        <Animated.View          pointerEvents="none"          style={[            styles.spot,            {              width: spotD,              height: spotD,              opacity: glow,              transform: [                { translateX: spot.x },                { translateY: spot.y },                { scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.4, 1] }) },              ],            },          ]}        >          <Svg width={spotD} height={spotD}>            <Defs>              <RadialGradient id={gid} cx="50%" cy="50%" r="50%">                <Stop offset="0" stopColor={tint} stopOpacity={dark ? 0.32 : 0.2} />                <Stop offset="0.55" stopColor={tint} stopOpacity={dark ? 0.1 : 0.06} />                <Stop offset="1" stopColor={tint} stopOpacity={0} />              </RadialGradient>            </Defs>            <Rect width={spotD} height={spotD} fill={`url(#${gid})`} />          </Svg>        </Animated.View>      ) : null}      <View style={{ padding, gap }}>{children}</View>      {highlight !== 'none' ? (        <Animated.View          pointerEvents="none"          style={[styles.ring, { borderRadius: Math.max(0, corner - border), borderColor: alpha(tint, 0.85), opacity: glow }]}        />      ) : null}    </View>  );   const onLayout = (e: LayoutChangeEvent) => {    const { width: w, height: h } = e.nativeEvent.layout;    if (w !== size.w || h !== size.h) setSize({ w, h });  };   return (    <CardContext.Provider value={{ padding }}>      <Animated.View        onLayout={onLayout}        style={[          { borderRadius: corner, opacity: disabled ? 0.55 : 1 },          surfaces[variant],          interactive && { transform: [{ scale }] },          style,        ]}      >        {interactive ? (          <Pressable            accessibilityRole={onPress || onLongPress ? 'button' : undefined}            accessibilityLabel={accessibilityLabel}            accessibilityState={{ disabled: !!disabled }}            disabled={disabled}            onPress={onPress}            onLongPress={onLongPress}            onPressIn={pressIn}            onPressOut={pressOut}            onTouchMove={highlight === 'spotlight' ? followSpot : undefined}            style={Platform.OS === 'web' ? ({ cursor: onPress ? 'pointer' : 'auto' } as ViewStyle) : undefined}          >            {content}          </Pressable>        ) : (          content        )}      </Animated.View>    </CardContext.Provider>  );} export interface CardHeaderProps {  children?: React.ReactNode;  /** Element pinned to the top-right (menu button, badge, avatar). */  action?: React.ReactNode;  style?: StyleProp<ViewStyle>;} /** Title + description stack, with an optional action on the right. */export function CardHeader({ children, action, style }: CardHeaderProps) {  return (    <View style={[styles.header, style]}>      <View style={styles.headerText}>{children}</View>      {action ? <View>{action}</View> : null}    </View>  );} export interface CardTextProps {  children?: React.ReactNode;  numberOfLines?: number;  style?: StyleProp<TextStyle>;} export function CardTitle({ children, numberOfLines, style }: CardTextProps) {  const { colors } = useKit();  return (    <Text accessibilityRole="header" numberOfLines={numberOfLines} style={[styles.title, { color: colors.cardForeground }, style]}>      {children}    </Text>  );} export function CardDescription({ children, numberOfLines, style }: CardTextProps) {  const { colors } = useKit();  return (    <Text numberOfLines={numberOfLines} style={[styles.description, { color: colors.mutedForeground }, style]}>      {children}    </Text>  );} export interface CardSectionProps {  children?: React.ReactNode;  style?: StyleProp<ViewStyle>;} export function CardContent({ children, style }: CardSectionProps) {  return <View style={style}>{children}</View>;} /** Row of actions at the bottom. */export function CardFooter({ children, style }: CardSectionProps) {  return <View style={[styles.footer, style]}>{children}</View>;} export interface CardMediaProps {  source: ImageSourcePropType;  /** Image height (default 168). */  height?: number;  /** Optional overlay content (e.g. a badge) placed on top of the image. */  children?: React.ReactNode;  accessibilityLabel?: string;  style?: StyleProp<ViewStyle>;} /** Full-bleed image. Put it first inside the Card — it cancels the card padding. */export function CardMedia({ source, height = 168, children, accessibilityLabel, style }: CardMediaProps) {  const { padding } = useContext(CardContext);  const { colors } = useKit();  return (    <View style={[{ height, marginTop: -padding, marginHorizontal: -padding, backgroundColor: colors.muted }, style]}>      <Image        source={source}        accessibilityLabel={accessibilityLabel}        accessibilityIgnoresInvertColors        resizeMode="cover"        style={styles.media}      />      {children ? <View style={[styles.mediaOverlay, { padding: padding - 4 }]}>{children}</View> : null}    </View>  );} Card.Header = CardHeader;Card.Title = CardTitle;Card.Description = CardDescription;Card.Content = CardContent;Card.Footer = CardFooter;Card.Media = CardMedia; function shadow(opacity: number, y: number, blur: number, elevation = 2): ViewStyle {  if (!opacity) return {};  return { shadowColor: '#000', shadowOpacity: opacity, shadowRadius: blur / 2, shadowOffset: { width: 0, height: y }, elevation };} const styles = StyleSheet.create({  clip: { overflow: 'hidden' },  spot: { position: 'absolute', top: 0, left: 0 },  ring: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderWidth: 1.5 },  header: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },  headerText: { flex: 1, gap: 4 },  title: { fontSize: 17, lineHeight: 22, fontWeight: '400', letterSpacing: -0.2 },  description: { fontSize: 14, lineHeight: 20 },  footer: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 2 },  media: { width: '100%', height: '100%' },  mediaOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'space-between' },});