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.

Alert

Alert, AlertTitle and AlertDescription: an inline message with Variants (default, destructive, success, warning, info), each with a default icon.

View as Markdown
Componentfeedback
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 alert

Also installs: theme, text, icon, form-field-context, announce.

Packages: lucide-react-native@^1.51.0, react-native-svg.

Usage

Alert demo

Alerts in every Variant, and one with a custom icon.

import { Terminal } from "lucide-react-native";
import { View } from "react-native";

import { Alert, AlertDescription, AlertTitle } from "@/components/alert";
import { createStyles } from "@/theme";

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

/** Every Variant with its default icon, and one with a custom icon. */
export default function AlertDemo() {
  const styles = useStyles();
  return (
    <View style={styles.stack}>
      <Alert icon={Terminal}>
        <AlertTitle>Heads up!</AlertTitle>
        <AlertDescription>You can add Components to your app with the CLI.</AlertDescription>
      </Alert>
      <Alert variant="destructive">
        <AlertTitle>Payment failed</AlertTitle>
        <AlertDescription>Check your card details and try again.</AlertDescription>
      </Alert>
      <Alert variant="success">
        <AlertTitle>Profile saved</AlertTitle>
      </Alert>
      <Alert variant="warning">
        <AlertTitle>Storage almost full</AlertTitle>
        <AlertDescription>You have used 90% of your space.</AlertDescription>
      </Alert>
      <Alert variant="info">
        <AlertDescription>A new version is available.</AlertDescription>
      </Alert>
    </View>
  );
}

Props

PropType and description
variantdefault | destructive | success | warning | info (default default)
iconLucide icon component; each Variant has a default (Info, CircleAlert, CircleCheck, TriangleAlert, Info); null hides it
AlertTitle, AlertDescriptionText props (children); the colour follows the Variant

Variants

  • default, destructive, success, warning, info

Accessibility

  • role alert, read as one element: the title and description together.
  • A destructive, success or warning Alert is announced once when it appears and again when its text changes ("Error: Wrong password. Check it and try again.", "Success: Saved", "Warning: Your session expires soon"); unrelated re-renders stay silent.
  • default and info Alerts are not announced; screen readers read them when the user reaches them.
  • aria-label, if given, replaces the announced text; without it the announcement is built from the text written inside the Alert.
  • Not announced when it sits inside another Alert (the outer one speaks) or inside a FormField that is announcing its error.
  • The Variant's icon is decorative.

Notes

Never use Alert.alert or any platform dialog: use this inline Alert, or toast() for transient and server feedback. The border, icon and title take the Variant's Colour Role on a card background.

Source

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

{components}/alert.tsx

import {
  CircleAlert,
  CircleCheck,
  Info,
  TriangleAlert,
  type LucideIcon,
} from "lucide-react-native";
import {
  createContext,
  isValidElement,
  use,
  useEffect,
  useRef,
  type ReactNode,
  type Ref,
} from "react";
import { View, type ViewProps } from "react-native";

import { Icon } from "@/components/icon";
import { useFormField } from "@/components/primitives/form-field-context";
import { Text, type TextColor, type TextProps } from "@/components/text";
import { createStyles, type ColorRole } from "@/theme";
import { announce } from "@/utils/announce";

/**
 * Each Variant's default icon, its Colour Roles (border, icon and title, description) and the
 * prefix it is announced with. Error (destructive), success and warning are announced; default
 * and info are read when the user reaches them.
 */
const variants = {
  default: {
    icon: Info,
    border: "border",
    accent: "foreground",
    description: "mutedForeground",
    spoken: null,
  },
  destructive: {
    icon: CircleAlert,
    border: "destructive",
    accent: "destructive",
    description: "destructive",
    spoken: "Error",
  },
  success: {
    icon: CircleCheck,
    border: "success",
    accent: "success",
    description: "success",
    spoken: "Success",
  },
  warning: {
    icon: TriangleAlert,
    border: "warning",
    accent: "warning",
    description: "warning",
    spoken: "Warning",
  },
  info: { icon: Info, border: "info", accent: "info", description: "info", spoken: null },
} as const satisfies Record<
  string,
  {
    icon: LucideIcon;
    border: ColorRole;
    accent: TextColor & ColorRole;
    description: TextColor;
    spoken: string | null;
  }
>;

export type AlertVariant = keyof typeof variants;

/** The enclosing Alert's Variant, or `null` outside an Alert. */
const AlertContext = createContext<AlertVariant | null>(null);

export type AlertProps = ViewProps & {
  /** Default `default`. */
  variant?: AlertVariant;
  /**
   * A Lucide icon component shown before the text. Each Variant has a default (Info,
   * CircleAlert, CircleCheck, TriangleAlert, Info); pass `null` for no icon.
   */
  icon?: LucideIcon | null;
  /** AlertTitle and AlertDescription, or any content. */
  children?: ReactNode;
  ref?: Ref<View>;
};

export type AlertTitleProps = Omit<TextProps, "variant" | "color">;
export type AlertDescriptionProps = Omit<TextProps, "variant" | "color">;

const useStyles = createStyles((t) => ({
  root: {
    ...{
    borderRadius: t.radius.lg,
    padding: t.spacing[4],
    gap: t.spacing[3],
    borderWidth: t.borderWidth.default,
  },
    flexDirection: "row",
    alignItems: "flex-start",
    borderCurve: "continuous",
    backgroundColor: t.colors.card,
  },
  // Centres the icon on the title's first line.
  icon: { height: t.type.label.lineHeight, justifyContent: "center" },
  content: { flex: 1, gap: t.spacing[1] },
}));

const useBorderStyles = createStyles((t) => ({
  default: { borderColor: t.colors.border },
  destructive: { borderColor: t.colors.destructive },
  success: { borderColor: t.colors.success },
  warning: { borderColor: t.colors.warning },
  info: { borderColor: t.colors.info },
}));

/**
 * The text written inside `node`, one entry per element: "Saved" and "Your changes are live."
 * for an AlertTitle and an AlertDescription. Adjacent strings and numbers stay one entry.
 */
function textParts(node: ReactNode): string[] {
  if (typeof node === "string" || typeof node === "number") return [String(node)];
  if (Array.isArray(node)) {
    const parts: string[] = [];
    let inline = "";
    for (const child of node as ReactNode[]) {
      if (typeof child === "string" || typeof child === "number") {
        inline += String(child);
        continue;
      }
      if (inline) parts.push(inline);
      inline = "";
      parts.push(...textParts(child));
    }
    if (inline) parts.push(inline);
    return parts;
  }
  if (isValidElement<{ children?: ReactNode }>(node)) return textParts(node.props.children);
  return [];
}

/** "Error: Wrong password. Check it and try again.", the same shape as a Toast's. */
function spokenText(prefix: string, label: string | undefined, children: ReactNode): string {
  const parts = (label?.trim() ? [label] : textParts(children))
    .map((part) => part.trim())
    .filter(Boolean);
  const text = parts.reduce(
    (said, part) => (said ? `${said}${/[.!?:]$/.test(said) ? " " : ". "}${part}` : part),
    "",
  );
  return text ? `${prefix}: ${text}` : "";
}

/**
 * An inline message that calls attention to something on the Screen. It has the `alert` role
 * and is read as one element. A destructive, success or warning Alert is also announced to
 * screen readers once when it appears and again when its text changes ("Error: …",
 * "Success: …", "Warning: …"), unless it sits inside another Alert or inside a FormField that
 * is announcing its error.
 * Never a platform dialog: use it (or `toast()`) instead of `Alert.alert`. Compose with
 * AlertTitle and AlertDescription. `style` is merged last.
 */
export function Alert({ variant = "default", icon, style, children, ...props }: AlertProps) {
  const styles = useStyles();
  const borderStyles = useBorderStyles();
  const v = variants[variant];
  const glyph = icon === undefined ? v.icon : icon;

  // Announced once when it appears and again when the spoken text changes, never on unrelated
  // re-renders. An enclosing Alert already speaks this text; a FormField already speaks its error.
  const nested = use(AlertContext) !== null;
  const fieldError = useFormField()?.error;
  const spoken =
    v.spoken && !nested && !fieldError ? spokenText(v.spoken, props["aria-label"], children) : "";
  const lastAnnounced = useRef("");
  useEffect(() => {
    if (spoken && spoken !== lastAnnounced.current) announce(spoken);
    lastAnnounced.current = spoken;
  }, [spoken]);

  return (
    <AlertContext value={variant}>
      <View role="alert" accessible style={[styles.root, borderStyles[variant], style]} {...props}>
        {glyph ? (
          <View style={styles.icon}>
            <Icon icon={glyph} size="sm" color={v.accent} />
          </View>
        ) : null}
        <View style={styles.content}>{children}</View>
      </View>
    </AlertContext>
  );
}

/** The Alert's heading line, in the Variant's colour. */
export function AlertTitle(props: AlertTitleProps) {
  const variant = use(AlertContext) ?? "default";
  return <Text variant="label" color={variants[variant].accent} {...props} />;
}

/** The Alert's body text. */
export function AlertDescription(props: AlertDescriptionProps) {
  const variant = use(AlertContext) ?? "default";
  return <Text variant="small" color={variants[variant].description} {...props} />;
}