Skip to main content

CLI

The RootNative CLI lets you add components directly into your project as source files — no npm package for components, full ownership of the code. Inspired by shadcn/ui.

npx rootnative init
npx rootnative add button card

Why use the CLI?​

With the standard @rootnative/components package, components live in node_modules and you import them as-is. The CLI takes a different approach:

  • Full ownership — component source files live in your project, not node_modules
  • Customizable — modify styles, adjust behavior, or remove what you don't need
  • No version lock-in — you decide when to pull updates
  • Tree-shake friendly — only the components you use exist in your codebase

The theme system (@rootnative/core) stays as an npm dependency so theme updates propagate automatically.

Prerequisites​

  • Node.js >= 18
  • Expo SDK 57: React Native >=0.83.0 <0.87.0 and React >=19.2.3 <20.0.0, the band the published packages declare
  • TypeScript project with path aliases configured (e.g. @/* → src/*)

Quick start​

1. Initialize​

Run init in your React Native or Expo project:

npx rootnative init

The CLI will:

  1. Detect your project type (Expo or bare React Native) — it stops here if it finds neither
  2. Detect your package manager (npm, yarn, pnpm, bun)
  3. Read path aliases from your tsconfig.json to seed the defaults
  4. Ask where to place components and utility files
  5. Create an rootnative.json config file, pinned to the current release tag
  6. Add a paths mapping to your tsconfig.json when your alias prefix has none
  7. Offer to point AI agents at the LLM docs from your CLAUDE.md
  8. Offer to install @rootnative/core

Steps 7 and 8 are confirmation prompts; -y accepts both.

2. Add components​

npx rootnative add button

This copies the Button source files into your project and installs any required dependencies.

3. Use them​

import { ThemeProvider } from '@rootnative/core'
import { Button } from '@/components/ui/button'

export default function App() {
return (
<ThemeProvider>
<Button variant="filled" onPress={() => {}}>
Press me
</Button>
</ThemeProvider>
)
}

A string icon name such as leadingIcon="check" needs an iconResolver on ThemeProvider. A copy-paste project does not install @rootnative/components, so it does not get mdiResolver from the npm package. Write the resolver in the project instead:

import { MaterialDesignIcons } from '@react-native-vector-icons/material-design-icons'
import type { IconResolver } from '@rootnative/core'
import type { ComponentProps } from 'react'

type MdiName = ComponentProps<typeof MaterialDesignIcons>['name']

const mdiResolver: IconResolver = (name, { size, color }) => (
<MaterialDesignIcons name={name as MdiName} size={size} color={color} />
)

<ThemeProvider iconResolver={mdiResolver}>{/* ... */}</ThemeProvider>

Install @react-native-vector-icons/material-design-icons, and expo-font on Expo, next to it. See the Icons guide for other icon sets.

In a static web export (web.output: 'static'), this resolver causes a hydration mismatch. The icon set draws the glyph on the server, but on the client it draws nothing until the font loads. Render the icon with no name while the page hydrates, as mdiResolver from @rootnative/components/mdi does.

Commands​

create​

Create a new project with RootNative UI pre-configured. See Quick Start for a full walkthrough.

npx rootnative create
npx rootnative create my-app
npx rootnative create .

The CLI prompts for template, project name, display name, and package manager, then scaffolds a ready-to-run Expo project with ThemeProvider and example components.

Scaffolding into the current directory: pass . as the name. The CLI writes the template into the directory you are in, and takes the project name from that directory's name. Your directory is never deleted. If the template would overwrite a file that is already there — an existing .gitignore or README.md, for example — the CLI lists each file and asks first. With -y it stops instead, and changes nothing.

A named project goes into a new subdirectory. If that subdirectory already exists, the CLI asks to delete it and all of its contents. With -y it stops instead.

Options:

FlagDescription
-y, --yesSkip prompts and use defaults
-t, --template <name>Template to use: blank (default) or with-router
--package-manager <pm>Package manager to use (npm, yarn, pnpm, bun)

init​

Initialize your project for RootNative UI.

npx rootnative init

If rootnative.json already exists, you'll be asked whether to overwrite it.

Options:

FlagDescription
-y, --yesSkip prompts and use defaults
--components-alias <alias>Components install path alias (e.g. @/components/ui)
--lib-alias <alias>Utility files path alias (e.g. @/lib)
--package-manager <pm>Package manager to use (npm, yarn, pnpm, bun)

What init writes:

  1. rootnative.json — your aliases, plus a registryVersion pinned to the release tag that matches the latest published @rootnative/core.
  2. A paths mapping in tsconfig.json for your alias prefix, when one is not already there. add rewrites every generated import to that alias, so without the mapping TypeScript cannot resolve any of them. An existing mapping is never overwritten.
  3. A pointer to the LLM docs in CLAUDE.md, if you accept the prompt.

If init cannot pin registryVersion to a release tag — npm unreachable, or the tag is not pushed yet — it warns and falls back to main. Take that warning seriously: main is the development branch, and component source copied from it can use @rootnative/core APIs that your installed release does not export yet.

add​

Add one or more components to your project.

npx rootnative add button
npx rootnative add card chip text-field
npx rootnative add appbar

Automatic dependency resolution — if a component depends on other components, they are added automatically. For example, appbar depends on button, icon-button and typography, so all four are added together.

Options:

FlagDescription
-y, --yesSkip prompts and use defaults
--force, -fOverwrite existing component files
--dry-run, -dPreview what would be installed without writing any files
--package-manager <pm>Package manager to use (npm, yarn, pnpm, bun)

What happens when you run add:

  1. The CLI fetches the component registry and validates the requested names
  2. It resolves the full dependency graph
  3. It shows a summary of what will be installed — components, utilities, and npm packages
  4. After confirmation, utility files are copied to your lib/ directory
  5. A barrel file (rootnative-utils.ts) is generated that re-exports only the utilities your installed components need
  6. It fetches the component source files from the registry
  7. Import paths are rewritten to match your project's alias configuration
  8. Any required npm packages are installed via your package manager

update​

Update installed components to the latest version from the registry.

npx rootnative update button
npx rootnative update --all

Options:

FlagDescription
-a, --allUpdate all installed components
-d, --dry-runShow diff without applying changes

upgrade​

Upgrade @rootnative/core to the latest version and install any new peer dependencies.

npx rootnative upgrade

Options:

FlagDescription
-y, --yesSkip confirmation prompt
-a, --allAlso update all installed component files
--package-manager <pm>Package manager to use (npm, yarn, pnpm, bun)

list​

Show all available components with their install status.

npx rootnative list

Output:

Available components (v<registry version>):

Name Status Description
----------------------------------------------------------------------
button installed MD3 button with 5 variants...
card - Surface container with 3...
...

The version is the registry's, which tracks the latest published release. installed means the component's directory exists under your configured components alias.

doctor​

Diagnose common issues in your project.

npx rootnative doctor

Checks performed:

CheckDescriptionOn failure
Configrootnative.json exists. Missing is not an error — a project that uses the published packages never needs oneinfo
Project typeAn Expo or bare React Native project was detectedfail
React Nativereact-native is present in your dependencies (the version is reported, not enforced)fail
Core package@rootnative/core is installedfail
TypeScripttsconfig.json presentwarn
Component integrityEvery installed component directory has an index.ts. Skipped without a configwarn
Utility barrelrootnative-utils.ts exists. Skipped without a configwarn
Animation engine@rootnative/inertia is installed — every animated component imports itfail
Required peersreact-native-svg is installed — the @rootnative/components barrel requires it at load timefail
Optional peersreact-native-safe-area-context status, and @react-native-vector-icons/material-design-icons status (needed only by mdiResolver)warn

Only fail rows count toward the issue total in the summary line. info rows never do, so doctor runs and exits zero on a project straight out of create.

Configuration​

rootnative init creates an rootnative.json file in your project root:

{
"$schema": "https://rootnative.github.io/ui/schema.json",
"aliases": {
"components": "@/components/ui",
"lib": "@/lib"
},
"registryUrl": "https://raw.githubusercontent.com/rootnative/ui",
"registryVersion": "v0.0.0-alpha.4"
}
FieldDefaultDescription
aliases.components@/components/uiDirectory where component folders are created
aliases.lib@/libDirectory where utility files are placed
registryUrlGitHub raw URLBase URL for fetching source files
registryVersionthe current release tagGit ref to fetch from (tag, branch name, or commit hash)

init pins registryVersion to the v<version> tag matching the latest published @rootnative/core, so add and update fetch reproducible source. It falls back to main only when npm is unreachable or the tag hasn't been pushed yet. A pinned project doesn't pick up new component source on its own — rootnative upgrade moves the pin forward. See API stability.

Path aliases​

The CLI uses your tsconfig.json path aliases to generate clean import paths. If your project has:

{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}

Then aliases.components of @/components/ui maps to src/components/ui/ on disk, and generated imports use @/components/ui/button instead of relative paths.

Note: If you use ~/* instead of @/*, the CLI detects that and adjusts the default aliases accordingly.

Project structure​

After running npx rootnative add appbar, your project looks like this — appbar pulls in three components, so all four are installed:

src/
├── components/
│ └── ui/
│ ├── appbar/
│ │ ├── AppBar.tsx
│ │ ├── types.ts
│ │ ├── styles.ts
│ │ ├── index.ts
│ │ └── safe-area.tsx ← shared file, flattened in
│ ├── button/ ← auto-added (appbar dependency)
│ │ ├── Button.tsx
│ │ ├── types.ts
│ │ ├── styles.ts
│ │ ├── index.ts
│ │ ├── elevationShadow.ts ← shared file, flattened in
│ │ ├── usePressMorph.ts ← shared file, flattened in
│ │ └── useStateLayer.ts ← shared file, flattened in
│ ├── icon-button/ ← auto-added (appbar dependency)
│ │ ├── IconButton.tsx
│ │ ├── types.ts
│ │ ├── styles.ts
│ │ ├── index.ts
│ │ ├── useBooleanProgress.ts
│ │ ├── usePressMorph.ts
│ │ └── useStateLayer.ts
│ └── typography/ ← auto-added (appbar dependency)
│ ├── Typography.tsx
│ ├── types.ts
│ ├── styles.ts
│ └── index.ts
└── lib/
├── color.ts ← alphaColor, blendColor
├── elevation.ts ← shadow/elevation helpers
├── pressable.ts ← Pressable style resolution
├── render-icon.tsx ← icon resolution
├── rtl.ts ← RTL layout helpers
└── rootnative-utils.ts ← generated barrel (re-exports used utilities)

What gets copied​

Component files — each component is a self-contained directory with the same structure as the library source: the component file, types, styles, and an index barrel.

Shared files — hooks and helpers that several components share (useStateLayer, usePressMorph, useBooleanProgress, elevationShadow, safe-area) are copied into each component's own directory rather than into lib/, and their imports are rewritten to be local. That keeps every component directory self-contained and independently deletable, at the cost of duplicating a file when two components need it — usePressMorph.ts above lands in both button/ and icon-button/. These are library internals, not API: see API stability.

Utility files — small helper functions that components depend on. These are copied from @rootnative/utils (which is not published to npm) into your lib/ directory. Only the utilities needed by your installed components are copied.

Barrel file — rootnative-utils.ts is auto-generated and re-exports only the functions your components use:

// Auto-generated by rootnative CLI. Do not edit.
export { alphaColor, blendColor } from './color'
export { elevationStyle } from './elevation'
export { resolvePressableStyle, resolveColorFromStyle } from './pressable'
export type { PressableState, PressableStyleProp } from './pressable'
export { renderIcon } from './render-icon'
export type { IconSource } from './render-icon'
export { transformOrigin, selectRTL } from './rtl'

Import rewriting​

The CLI rewrites imports in copied component files so they work in your project:

Original (library source)Rewritten to
from '@rootnative/core'Unchanged — npm package
from '@rootnative/inertia'Unchanged — npm package
from '@rootnative/utils'from '@/lib/rootnative-utils'
from '../icon-button'from '@/components/ui/icon-button'
from '../internal/useStateLayer'from './useStateLayer' — flattened into the component's own directory
from '../safe-area'from './safe-area' — same flattening
from './styles'Unchanged — same directory

Available components​

All 35, with the components each one pulls in automatically. Descriptions match what npx rootnative list prints, since both read the same registry.

ComponentDependenciesDescription
appbarbutton, icon-button, typographyTop app bar with 4 variants (small, center-aligned, medium, large) and SafeAreaView support
avatar—Circular avatar with image, icon, or text initials and 5 sizes (extraSmall to extraLarge)
badge—Small 6dp dot or 16dp count badge anchored to an icon, with max cap and NavigationBar support
bottom-sheetportalMD3 bottom sheet — modal (scrim) and standard variants, drag handle, velocity-based snap points, drag-to-dismiss
buttonprogressMD3 button with 5 variants (filled, elevated, outlined, text, tonal) and icon support
button-group—Standard and connected button groups with single or multi-select toggle behavior; replaces the deprecated MD3 segmented button
card—Surface container with 3 variants (elevated, filled, outlined) and optional press handler
checkbox—Binary selection control with checked/unchecked states and customizable colors
chip—Input chip with 4 variants (assist, filter, input, suggestion) and icon/avatar support
dialogicon-button, portalBasic and full-screen modal dialogs with Icon / Title / Content / Actions slots, scrim, and Android back handling
divider—Horizontal or vertical 1dp rule with optional leading/trailing insets and thickness/color overrides
fab—Floating action button with 4 color variants (primary, secondary, tertiary, surface), 3 sizes (small, medium, large), and optional extended label
icon—Any IconSource (name, element, or render function) rendered through the theme's iconResolver, decorative or labelled
icon-button—Icon-only button with 4 variants (filled, tonal, outlined, standard) and toggle support
keyboard-avoiding-wrapper—Zero-config keyboard-aware wrapper with platform-specific behavior for form layouts
layout—Layout primitives: Box, Row, Column, Container (a centred column with a maximum width), Grid (flexbox utilities), and Layout (SafeAreaView wrapper)
listdividerList container with interactive items, supporting headline/trailing text and dividers
loading-indicator—MD3 Expressive shape-morphing loading spinner (contained + uncontained, determinate + indeterminate)
menuportalAnchored dropdown menu (Menu + Menu.Item) that flips and shifts to stay on screen, with self-managing or controlled visibility
navigation-drawerdivider, portalMD3 navigation drawer — modal (scrim, slide from the start edge) and standard variants with sections, headlines, badges, and dividers
navigation-barbadgeMD3 navigation bar — 80dp bottom destination bar with an animated indicator pill
navigation-railnavigation-barMD3 navigation rail — 80dp side rail of destinations with an optional header, three alignments, and the NavigationBar indicator pill
portal—Render children into a host elsewhere in the tree (Portal + PortalHost) for overlays like dialogs, sheets, and tooltips
progress—Linear and circular progress indicators with determinate and indeterminate modes
radio—Single-choice selection control with selected/unselected states
search-baricon-buttonMD3 search bar — 56dp pill with a leading search or navigation icon, a clear button, and trailing icon actions or an avatar
search-viewdivider, icon-button, portal, search-barMD3 search view — the panel that opens from a search bar, full screen on a compact window and docked on a wider one, with a focus trap and Android back handling
skeleton—Pulsing loading placeholder block that matches the shape of the content it stands in for
slider—Single-thumb or range slider with continuous and discrete (stepped) modes and optional centered origin
snackbarbutton, icon-button, portalImperative snackbar queue — SnackbarProvider plus useSnackbar() with actions, durations, and safe-area aware placement
switch—Toggle control with optional icons and customizable thumb/track colors
tabsdividerPrimary and secondary tab rows, fixed or scrollable, with a sliding active indicator
text-field—Text input with animated floating label, 2 variants (filled, outlined), and icon support
tooltipportalPlain and rich tooltips anchored to a control, shown on hover or a long press
typography—Text component using MD3 type scale with 15 variants (displayLarge to labelSmall)

Comparison with npm packages​

CLI (npx rootnative add)npm (@rootnative/components)
Component code lives inYour project (src/)node_modules/
CustomizationEdit source directlyOverride via props/theme only
UpdatesRe-run add --force when you choosenpm update
Bundle sizeOnly what you addTree-shaking at build time
Setupnpx rootnative initpnpm add @rootnative/components

Both approaches use @rootnative/core for theming — they are fully compatible and you can even mix them in the same project.