Skip to content
  • Introduction
    nativecn is shadcn for Expo apps: Components you copy into your app and own, built so AI agents can build and verify whole mobile apps.
  • Installation
    Create a new Expo app with nativecn, or add nativecn to an existing one.
  • components.json
    The project file that records your Preset, Structure, aliases and agents.
  • CLI
    Every nativecn-cli command and flag: init, create, add, agents and mcp.
  • Changelog
    What changed in each nativecn release.
  • Theming
    Tokens, Colour Roles, Scale, Scheme, createStyles and fonts.
  • Presets & Styles
    Style, Base Colour, Accent Colour, Radius and fonts: chosen once, baked into your code.
  • Folder structure
    Flat and feature Structures, Destinations and the placement rule.
  • AI agents
    Rules, Skills, the nativecn MCP server, the Plugin and the Visual QA Loop.
  • Alert
    Alert, AlertTitle and AlertDescription: an inline message with Variants (default, destructive, success, warning, info), each with a default icon.
  • Avatar
    Round user image (expo-image) with fallback initials, in sizes sm, md and lg.
  • Badge
    Small status label with Variants default, secondary, outline, destructive, success and warning.
  • Button
    Button with Variants (primary, secondary, outline, ghost, destructive, link), Sizes, an icon slot, loading and status; icon-only requires aria-label.
  • Card
    Card surface with CardHeader, CardTitle, CardDescription, CardContent and CardFooter; onPress makes the whole card pressable.
  • Checkbox
    Checkbox with an optional label, controlled or uncontrolled, an indeterminate (mixed) state and a haptic tick.
  • Chip
    Chip (plain, toggle or removable, with an optional icon) and ChipGroup (single or multiple choice on the SelectionGroup Primitive), with a haptic tick.
  • Container
    Wraps a Screen: safe-area edges, scrolling, keyboard avoidance, Token padding and a max content width.
  • Drawer
    Drawer panel content for Expo Router's drawer (expo-router/drawer): DrawerContent, DrawerHeader, DrawerSection, DrawerItem (label, icon, href, badge; active from the current route) and DrawerFooter.
  • Empty State
    What a Screen or list shows when there is nothing to show yet: an icon, a title, a description and an action.
  • FormField
    Lays out a control with its Label, description and error, and links them for screen readers ("Email, text field, Enter a valid email").
  • Icon
    Every glyph: a Lucide icon at a Token size in a Colour Role, decorative unless labelled.
  • Input
    Single-line text field with Sizes, a leading icon, error/success status, a focus ring and an automatic Show/Hide password toggle.
  • InputOTP
    One-time code field: one hidden input drives N cells; numeric keyboard, SMS/Keychain autofill, paste, onComplete, error/success status and secure.
  • Label
    A field's visible label in the label type Variant, with a required marker read as "required".
  • List
    Settings-style rows: ListSection, ListSectionHeader, ListSectionFooter and ListItem (title, description, icon, trailing, chevron, destructive, onPress); FlashList-safe.
  • Progress
    A horizontal progress bar: a value 0–100 that animates, or indeterminate; role progressbar with its value.
  • RadioGroup
    RadioGroup and RadioGroupItem: a single choice from a list on the SelectionGroup Primitive, vertical or horizontal, with a haptic tick.
  • SchemeSwitcher
    Theme switcher / dark mode toggle: System, Light and Dark as SegmentedTabs, or one icon button cycling through them. Calls setScheme, which persists the choice.
  • SearchField
    Search box with a search icon, an automatic clear (×) button, a loading spinner and onSubmit from the keyboard's Search key.
  • SegmentedTabs
    In-screen tabs (not navigation): segmented or underline Variants, an animated indicator, and only the selected tab's content rendered.
  • Separator
    A hairline divider, horizontal or vertical, decorative by default.
  • Skeleton
    A placeholder shape shown while content loads; pulses unless Reduce Motion is on, hidden from screen readers.
  • Slider
    Pick a number in a range by dragging or tapping (Gesture Handler + Reanimated), snapped to steps with a haptic tick; adjustable for screen readers.
  • Spinner
    An indeterminate loading indicator in a Colour Role, announced as busy; still under Reduce Motion.
  • Switch
    A themed on/off Switch, identical on iOS and Android, with an optional label, a thumb that slides (instant under Reduce Motion) and a haptic tick.
  • TabNavigation
    The bottom tab bar: the custom tabBar of an Expo Router JS Tabs Layout, with classic and floating Variants, per-tab icons and badges from Tabs.Screen options.
  • Text
    All text: the type ramp as Variants, text Colour Roles, alignment and the font-scaling switch.
  • Textarea
    Multi-line text field that grows from minRows to maxRows, with a character counter when maxLength is set.
  • Toast
    Sonner-style Toasts: a <Toaster /> in the root Layout and toast(), toast.success/error/info/warning/dismiss. At most 3 stacked, 4s auto-dismiss, swipe away, announced, above native modals.
  • Profile header + grouped sections
    Drawer panel with a profile header (avatar, name, email, plan Badge), labelled sections (Main, Workspace, Support) and Log out pinned in the footer.
  • SaaS workspace
    Drawer panel for a SaaS workspace: logo and name with a close button, an Accent Colour onboarding item with a 5 Steps Badge, main items, a bottom group (Notifications with a count, Settings, Docs, Help) and a footer with a team switcher and the user's avatar.
  • Cover header
    Drawer panel with a dark cover (large avatar, name, View profile, a bell with a count, an overflow button and a round + action), icon items active in Accent Colour text, a divider and text-only secondary items.
  • Classic centred
    A centred sign-in Screen: logo, email and password, Forgot password, an "or" divider, then Apple and Google, and a Sign up link.
  • Social-first hero
    A social-first sign-in Screen: a dark brand hero (logo, headline, subtitle, centred) above a panel where Apple and Google lead and "Continue with email" reveals the email form.
  • Email-first, two steps
    An email-first sign-in Screen in two steps: the email, then the password, with "Email me a code instead" switching to a 6-digit code (InputOTP). Lucide chevron-left Back buttons.
  • Classic single form
    A classic sign-up Screen on one scroll: Apple and Google on top, "or", then name, email and password, a terms Checkbox with Terms and Privacy Policy links, Create account, and a Sign in link.
  • Step-by-step wizard
    A sign-up wizard, one question per step under a Progress bar: email, name, password, then a 6-digit code (InputOTP). Lucide chevron-left Back buttons; the Continue button rides above the keyboard.
  • Social-first + verify
    A brand-led, passwordless sign-up Screen: the logo and headline centred above Apple and Google; "Sign up with email" reveals the email field, and Send code moves on to a 6-digit code (InputOTP).
  • announce
    Reliable screen-reader announcements on iOS and Android (Button and Input status, Toast, error, success and warning Alerts).
  • FocusChain
    Automatic Next/Done between fields in on-screen order; Done submits the form.
  • FormField context
    Links a field to its label, description and error for screen readers; passes status, disabled and required down.
  • Keyboard
    KeyboardProvider setup, a scroll view that keeps the focused field above the keyboard, and a footer that keeps a form's main button above it.
  • Portal
    Renders content above every Screen (above native modals on iOS), layered and safe-area aware.
  • Pressable
    Pressed look from a Style Slot, tap area extended to 48, no handlers while disabled or loading, optional haptics, role and aria-* states.
  • SelectionGroup
    Single or multiple choice within a group, with radio, tab or checkbox roles and selected states.
  • useControllableState
    One implementation of controlled and uncontrolled values for form controls.
  • useMotion
    Enter/exit animations and timing/spring configs from the motion Tokens; instant when Reduce Motion is on.
  • Theme
    Tokens, Scale, Colour Roles, ThemeProvider, createStyles and the navigation theme.

Text

All text: the type ramp as Variants, text Colour Roles, alignment and the font-scaling switch.

View as Markdown
Componentcore
Style

Light

Dark

Your app is ready.

Inter body text, Inter headings.

smmdlgxl
you@example.com
Continue
Default Preset a0: Vega, neutral, neutral accent, default radius, Inter.Change Preset

Installation

$npx nativecn-cli@latest add text

Also installs: theme.

Usage

Text demo

Headings, body, small and caption text in Colour Roles.

import { View } from "react-native";

import { Text } from "@/components/text";
import { createStyles } from "@/theme";

/** The type ramp, text Colour Roles and alignment. */
export default function TextDemo() {
  const styles = useStyles();
  return (
    <View style={styles.stack}>
      <Text variant="h1">Welcome back</Text>
      <Text variant="lead" color="mutedForeground">
        Pick up where you left off.
      </Text>
      <Text>Body text is selectable by default, so people can copy it.</Text>
      <Text variant="small" color="mutedForeground">
        Last synced 2 minutes ago
      </Text>
      <Text variant="caption" color="destructive">
        Your trial ends tomorrow
      </Text>
      <Text variant="label" align="center">
        Centred label
      </Text>
    </View>
  );
}

const useStyles = createStyles((t) => ({
  stack: { gap: t.spacing[2] },
}));

Props

PropType and description
...TextPropsReact Native Text props pass through (numberOfLines, onPress, ...)
variant"display" | "h1" | "h2" | "h3" | "h4" | "lead" | "body" (default) | "label" | "small" | "caption" | "button"
colora text Colour Role: "foreground" (default), any "…Foreground" role, "primary", "destructive", "success", "warning", "info"
align"auto" | "left" | "right" | "center" | "justify"
selectableboolean (default true for body and small, false otherwise)
allowFontScalingboolean (default: config.fontScaling in theme/config.ts)
refRef<Text>
styleTextStyle, merged last

Variants

  • display, h1, h2, h3, h4, lead, body, label, small, caption, button

Accessibility

  • display and h1–h4 have role heading, so screen readers can jump between headings.
  • Font scaling follows the switch in theme/config.ts (off by default); allowFontScaling overrides it for one Text.
  • When font scaling is on, the chrome Variants (label, caption, button) cap the OS font scale at 1.5×.

Notes

The single typographic Component: use it for all text, never React Native's Text or a separate Heading. display and h1–h4 use the Heading Font; the rest use the Body Font.

Source

The Vega Style's code, exactly as add copies it into your app.

{components}/text.tsx

import type { Ref } from "react";
import { Text as RNText, type TextProps as RNTextProps } from "react-native";

import { createStyles, useTheme, type ColorRole, type tokens } from "@/theme";

/** The type ramp: one Variant per typographic role. */
export type TextVariant = keyof typeof tokens.typeRamp;

/** Colour Roles meant for text: `foreground`, every `…Foreground`, and the status colours. */
export type TextColor =
  | "foreground"
  | Extract<ColorRole, `${string}Foreground`>
  | "primary"
  | "destructive"
  | "success"
  | "warning"
  | "info";

export type TextAlign = "auto" | "left" | "right" | "center" | "justify";

/**
 * Per-Variant behaviour. Headings get the `heading` role; reading text is selectable; chrome
 * Variants (button, label, caption) cap the OS font scale at 1.5× when font scaling is on.
 */
const variants = {
  display: { heading: true, selectable: false, chrome: false },
  h1: { heading: true, selectable: false, chrome: false },
  h2: { heading: true, selectable: false, chrome: false },
  h3: { heading: true, selectable: false, chrome: false },
  h4: { heading: true, selectable: false, chrome: false },
  lead: { heading: false, selectable: false, chrome: false },
  body: { heading: false, selectable: true, chrome: false },
  label: { heading: false, selectable: false, chrome: true },
  small: { heading: false, selectable: true, chrome: false },
  caption: { heading: false, selectable: false, chrome: true },
  button: { heading: false, selectable: false, chrome: true },
} as const satisfies Record<
  TextVariant,
  { heading: boolean; selectable: boolean; chrome: boolean }
>;

/** The OS font-scale cap for chrome Variants (ADR 0004). */
const CHROME_MAX_FONT_SCALE = 1.5;

export type TextProps = RNTextProps & {
  /** The typographic role (default `body`). */
  variant?: TextVariant;
  /** A text Colour Role (default `foreground`). */
  color?: TextColor;
  align?: TextAlign;
  ref?: Ref<RNText>;
};

const useVariantStyles = createStyles((t) => ({
  display: { ...t.type.display },
  h1: { ...t.type.h1 },
  h2: { ...t.type.h2 },
  h3: { ...t.type.h3 },
  h4: { ...t.type.h4 },
  lead: { ...t.type.lead },
  body: { ...t.type.body },
  label: { ...t.type.label },
  small: { ...t.type.small },
  caption: { ...t.type.caption },
  button: { ...t.type.button },
}));

const useColorStyles = createStyles(
  (t) =>
    ({
      foreground: { color: t.colors.foreground },
      cardForeground: { color: t.colors.cardForeground },
      popoverForeground: { color: t.colors.popoverForeground },
      primaryForeground: { color: t.colors.primaryForeground },
      secondaryForeground: { color: t.colors.secondaryForeground },
      mutedForeground: { color: t.colors.mutedForeground },
      accentForeground: { color: t.colors.accentForeground },
      destructiveForeground: { color: t.colors.destructiveForeground },
      successForeground: { color: t.colors.successForeground },
      warningForeground: { color: t.colors.warningForeground },
      infoForeground: { color: t.colors.infoForeground },
      primary: { color: t.colors.primary },
      destructive: { color: t.colors.destructive },
      success: { color: t.colors.success },
      warning: { color: t.colors.warning },
      info: { color: t.colors.info },
    }) satisfies Record<TextColor, { color: string }>,
);

const useAlignStyles = createStyles(() => ({
  auto: { textAlign: "auto" },
  left: { textAlign: "left" },
  right: { textAlign: "right" },
  center: { textAlign: "center" },
  justify: { textAlign: "justify" },
}));

/**
 * All text in the app. Applies the type ramp and a text Colour Role, and honours the Theme's
 * font-scaling switch (`config.fontScaling`, off by default). React Native Text props pass
 * through; `style` is merged last.
 */
export function Text({
  variant = "body",
  color = "foreground",
  align,
  selectable,
  allowFontScaling,
  maxFontSizeMultiplier,
  role,
  style,
  ...props
}: TextProps) {
  const { config } = useTheme();
  const variantStyles = useVariantStyles();
  const colorStyles = useColorStyles();
  const alignStyles = useAlignStyles();
  const v = variants[variant];
  const scaling = allowFontScaling ?? config.fontScaling;

  return (
    <RNText
      role={role ?? (v.heading ? "heading" : undefined)}
      selectable={selectable ?? v.selectable}
      allowFontScaling={scaling}
      maxFontSizeMultiplier={
        maxFontSizeMultiplier ?? (scaling && v.chrome ? CHROME_MAX_FONT_SCALE : undefined)
      }
      style={[variantStyles[variant], colorStyles[color], align ? alignStyles[align] : null, style]}
      {...props}
    />
  );
}