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.

Pressable

Pressed look from a Style Slot, tap area extended to 48, no handlers while disabled or loading, optional haptics, role and aria-* states.

View as Markdown
Primitiveinteraction
Style

Installation

$npx nativecn-cli@latest add pressable

Also installs: theme.

Packages: expo-haptics.

Props

Pressable

PropType and description
...PressablePropsReact Native Pressable props pass through (onPress, role, aria-*, ...)
pressedStyleViewStyle: the pressed look, usually a Style Slot such as button.pressed
disabledboolean: blocks every press handler, announced disabled
loadingboolean: blocks every press handler, announced busy
haptic"selection" | "light": fires only when config.haptics is on
size{ width?, height? }: the visual size used to extend the tap area to 48; missing sides are measured
hitSlopInsets | number: an explicit tap-area extension; overrides the computed one
styleViewStyle or (state) => ViewStyle, merged last
refRef<View>

touchTargetHitSlop

PropType and description
touchTargetHitSlop(width, height, min)Insets | undefined: the extra area on each side so the element reaches min on both axes

Accessibility

  • The tap area reaches theme.minTouchTarget (48) through hitSlop.
  • Use the role / aria-* props, not accessibility*.

Notes

The press foundation of every pressable Component. Press feedback is a Style Slot applied instantly on press, identical on iOS and Android (no ripple).

Source

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

{components}/primitives/pressable.tsx

import * as Haptics from "expo-haptics";
import { useCallback, useState, type ReactNode, type Ref } from "react";
import {
  Pressable as RNPressable,
  type GestureResponderEvent,
  type Insets,
  type LayoutChangeEvent,
  type PressableProps as RNPressableProps,
  type PressableStateCallbackType,
  type StyleProp,
  type View,
  type ViewStyle,
} from "react-native";

import { useTheme } from "@/theme";

/** Haptic feedback on press: `selection` is the light tick for selection controls. */
export type PressableHaptic = "selection" | "light";

/** A declared size; any dimension left out is measured with onLayout. */
export type PressableSize = { width?: number; height?: number };

export type PressableProps = Omit<
  RNPressableProps,
  "disabled" | "hitSlop" | "style" | "children"
> & {
  /** Root style, or a function of the press state. */
  style?: StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>);
  /**
   * The pressed look, applied instantly while pressed and merged after `style`. Pass the
   * Component's Style Slot result, e.g. `styles.pressed` from its `button.pressed` Style Slot.
   */
  pressedStyle?: StyleProp<ViewStyle>;
  /** Blocks every press handler and is announced as disabled. */
  disabled?: boolean;
  /** Blocks every press handler and is announced as busy (not disabled). */
  loading?: boolean;
  /** Haptic feedback on press. Fires only when `config.haptics` is on in the Theme. */
  haptic?: PressableHaptic;
  /**
   * The visual size used to extend the tap area to the 48 minimum (`theme.minTouchTarget`).
   * Dimensions given here are used as-is (no layout pass needed, e.g. a Variant's fixed
   * height); missing dimensions are measured with onLayout. Ignored when `hitSlop` is set.
   */
  size?: PressableSize;
  /** Explicit tap-area extension. Overrides the computed one. */
  hitSlop?: Insets | number;
  ref?: Ref<View>;
  children?: ReactNode | ((state: PressableStateCallbackType) => ReactNode);
};

/** Extra tap area on each side so a `width` × `height` element reaches `min` on both axes. */
export function touchTargetHitSlop(
  width: number | undefined,
  height: number | undefined,
  min: number,
): Insets | undefined {
  const x = width == null ? 0 : Math.max(0, (min - width) / 2);
  const y = height == null ? 0 : Math.max(0, (min - height) / 2);
  if (x === 0 && y === 0) return undefined;
  return { top: y, bottom: y, left: x, right: x };
}

// Haptics are best effort: an unsupported device must never break a press.
function fireHaptic(haptic: PressableHaptic) {
  try {
    const done =
      haptic === "selection"
        ? Haptics.selectionAsync()
        : Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    done.catch(() => {});
  } catch {
    // Ignored, see above.
  }
}

/**
 * The press foundation of every pressable Component: pressed look from a Style Slot, a tap
 * area of at least 48, no handlers while disabled or loading, optional haptics and
 * `role`/`aria-*`. It draws nothing of its own beyond the styles it is given.
 */
export function Pressable({
  style,
  pressedStyle,
  disabled = false,
  loading = false,
  haptic,
  size,
  hitSlop,
  role = "button",
  onPress,
  onLongPress,
  onPressIn,
  onPressOut,
  onLayout,
  ref,
  ...props
}: PressableProps) {
  const theme = useTheme();
  const blocked = disabled || loading;
  const [measured, setMeasured] = useState<{ width: number; height: number }>();

  const needsLayout = hitSlop == null && (size?.width == null || size?.height == null);

  const handleLayout = useCallback(
    (event: LayoutChangeEvent) => {
      onLayout?.(event);
      if (!needsLayout) return;
      const { width, height } = event.nativeEvent.layout;
      setMeasured((prev) =>
        prev && prev.width === width && prev.height === height ? prev : { width, height },
      );
    },
    [onLayout, needsLayout],
  );

  const computedHitSlop =
    hitSlop ??
    touchTargetHitSlop(
      size?.width ?? measured?.width,
      size?.height ?? measured?.height,
      theme.minTouchTarget,
    );

  const handlePress = (event: GestureResponderEvent) => {
    if (blocked) return;
    if (haptic && theme.config.haptics) fireHaptic(haptic);
    onPress?.(event);
  };
  const guard =
    (handler: ((event: GestureResponderEvent) => void) | null | undefined) =>
    (event: GestureResponderEvent) => {
      if (!blocked) handler?.(event);
    };

  return (
    <RNPressable
      ref={ref}
      role={role}
      aria-disabled={disabled}
      aria-busy={loading}
      disabled={disabled}
      hitSlop={computedHitSlop}
      onPress={handlePress}
      onLongPress={onLongPress ? guard(onLongPress) : undefined}
      onPressIn={guard(onPressIn)}
      onPressOut={guard(onPressOut)}
      onLayout={onLayout || needsLayout ? handleLayout : undefined}
      style={(state) => [
        typeof style === "function" ? style(state) : style,
        state.pressed && !blocked ? pressedStyle : null,
      ]}
      {...props}
    />
  );
}