Proper UI

CLI tool

The Proper UI CLI copies component source into your project and configures Tailwind, straight from the terminal. No copy-pasting required.

The properui CLI copies component source into an existing project, shadcn-style: you get the .tsx files, not a runtime dependency. It detects your framework, TypeScript setup, and Tailwind version automatically, then writes files and reports the npm packages an install needs. This guide covers every command: init, add, remove, why, check, icons, list, search, diff, and login.

Installation

There's nothing to install ahead of time; every command runs through npx (or your package manager's equivalent), which always fetches the latest version:

npx @properui/cli@latest init

Or with your preferred package manager: pnpm dlx @properui/cli@latest init, yarn dlx @properui/cli@latest init, bunx @properui/cli@latest init.

Requires Node 20+.

Init command

init configures an existing project: it does not scaffold a new one, so run it from the directory that already holds your package.json. It reads your tsconfig.json for an existing path alias, detects Next.js App Router vs Pages Router vs Vite vs Remix vs plain React, checks your Tailwind version (v3 projects are stopped with upgrade instructions; Proper UI requires Tailwind v4), and confirms your package manager from the lockfile. It then writes components.json, copies the theme token file (from the registry when reachable, a placeholder otherwise), creates utils/cx.ts, and writes the full stylesheet block into your global stylesheet: not just an @source scan line, but the theme import, plugin registrations, custom variants, and utility layer the components need, inserted right after your existing @import "tailwindcss" line. Unless you pass --no-providers, it also copies the registry's ThemeProvider (next-themes-based, with forcedTheme/enableSystem props available) and wraps your app entry point (app/layout.tsx, pages/_app.tsx, or main.tsx) in it; on a Next.js App Router project it wires up RouterProvider there too, since that provider needs no framework-specific setup beyond what init already knows. Unless you pass --no-tooling-ignores, it adds ESLint and Prettier ignore entries for the vendored component directories, so a strict consumer's lint/format config doesn't fail on copied-in source it doesn't own the style of. For Vite specifically, it also writes the @ alias into both halves Vite needs: a paths entry in tsconfig.app.json/tsconfig.json and a matching resolve.alias in vite.config, since Vite has no built-in tsconfig-paths support and vite build/tsc -b fail on @/... imports without both.

init does not install any npm packages itself unless you pass --install. Either way, the install command is the last thing it prints, after every file it wrote, not buried above a wall of file paths, so the final lines of a run look like:

✔ Wrote components.json
✔ Wrote src/styles/theme.css
✔ Wrote src/utils/cx.ts
✔ Wired ThemeProvider into src/main.tsx

Next steps:
  npx @properui/cli@latest add buttons badges

That suggested next command names real registry entries (buttons, badges), not placeholders, so it's safe to copy and run as-is.

npx @properui/cli@latest init

Command options

OptionDescription
--nextjsTreat the project as Next.js instead of auto-detecting.
--viteTreat the project as Vite instead of auto-detecting.
--manualWrite the files but leave the app entry point (and, for Vite, the @ alias) alone. Print the snippet instead.
--installRun the install for you instead of just printing the command.
--no-providersSkip copying and wiring ThemeProvider (and, on Next App Router, RouterProvider) entirely.
--no-tooling-ignoresSkip adding the ESLint/Prettier ignore entries for vendored component directories.
--overwriteReplace components.json and any files that already exist.
--platform <p>react or html: override the detected platform (see Non-React projects).
--registry <source>Registry directory or base URL for this run (see below).
-y, --yesAccept every default; never prompt.
--cwd <path>Run against a directory other than the current one.

Add command

add resolves a component and its registryDependencies recursively, copies the source files into your project, rewrites @/ imports to your configured alias, and records what it installed in the installed manifest inside components.json. It does not run the install for you unless you pass --install: either way, the install block prints last, after every file it wrote, with per-file attribution so you can see which package a given .tsx file actually needs:

✔ Wrote src/components/ui/button.tsx
✔ Wrote src/components/ui/input.tsx

Install:
  npm install react-aria-components   # button.tsx, input.tsx
  npm install tailwind-variants        # button.tsx
npx @properui/cli@latest add button

Add multiple components in one call:

npx @properui/cli@latest add button input select

If a component depends on another component internally (badge-groups depends on badges, for example), the CLI pulls in the whole dependency chain, so you never end up with a broken import.

npx @properui/cli@latest add badge-groups

The dependency tree above installs badges and dot-icon alongside badge-groups because those are its registryDependencies.

Install every component at once:

npx @properui/cli@latest add --all

Customize the installation directory:

npx @properui/cli@latest add button --path src/components/ui

Typing a name that doesn't exist gets a suggestion rather than a bare failure: the CLI runs a fuzzy match against the registry index and prints the closest names.

Command options

OptionDescription
--allAdd every component in the registry.
--installRun the install for you instead of just printing the command.
--no-optionalSkip an entry's optionalRegistryDependencies, the pieces it works better with but doesn't strictly need.
--with-demosAlso copy the entry's demo file(s), for reference or as a starting point.
--overwriteReplace files that already exist instead of skipping them: how you pull upstream fixes.
--path <dir>Put component files in this directory instead of the configured alias.
--dry-runPrint what would change without writing anything.
--registry <source>Registry directory or base URL for this run (see below).
-y, --yesSkip the "install these dependencies?" confirmation.

--dry-run before --all is a good habit:

npx @properui/cli@latest add --all --dry-run

Every add (and init) records what it installed in an installed manifest inside components.json, keyed by entry name, so remove and why (below) have something to consult, and re-running add on an entry that's already listed there is the no-op described above.

Remove command

remove deletes an installed entry's files from your project and drops it from the installed manifest. If another installed entry still lists it as a registryDependency, remove warns you instead of leaving a broken import behind:

npx @properui/cli@latest remove badges
✖ badges is still a dependency of badge-groups (installed). Remove badge-groups first, or pass --force.

Command options

OptionDescription
--forceRemove the entry even if something else installed still depends on it.
--cwd <path>Run against a directory other than the current one.

Why command

why answers "what pulled this in" for anything in the installed manifest, whether you added it directly or it arrived as someone else's registryDependency:

npx @properui/cli@latest why dot-icon
dot-icon
  └─ badges (registryDependency)
       └─ badge-groups (you ran: add badge-groups)

Reach for it before deleting a file you don't remember installing yourself, and before deciding an entry is unused.

Check command

check is the token guard: it scans your installed component directories (or a path you give it) for raw palette classes and arbitrary values that should have been semantic tokens (bg-purple-600 instead of bg-brand-solid, p-[13px] instead of a spacing token), the convention described in Theming, which nothing in Tailwind itself enforces.

npx @properui/cli@latest check
npx @properui/cli@latest check src/components/ui

Run it as part of your normal verification pass, alongside type-check and build; see the AI agents guide for why an agent should run it before reporting UI work as done. If your project already runs ESLint, @properui/eslint-plugin catches the same things (plus dark: variants and physical directional properties) inline as you type, with an autofix for the last one.

Command options

OptionDescription
--path <dir>Check this directory instead of the configured alias.
--cwd <path>Run against a directory other than the current one.

Icons command

icons lists and installs icon components the same way add handles the rest of the registry, useful because the main search/list index is built for components and examples, not for browsing an icon set:

npx @properui/cli@latest icons                    # list available icons
npx @properui/cli@latest icons arrow-right check   # install specific icons

Command options

OptionDescription
--path <dir>Put icon files in this directory instead of the configured alias.
--overwriteReplace icon files that already exist.
--registry <source>Registry directory or base URL for this run (see below).

List command

list prints the registry’s index, every component and example with its layer and description:

npx @properui/cli@latest list
npx @properui/cli@latest list --layer base
npx @properui/cli@latest list --type component
npx @properui/cli@latest list --json

Command options

OptionDescription
--layer <l>Filter by layer, e.g. base, application, marketing, app-examples, marketing-examples, foundations, shared-assets, hooks, utils, styles, html.
--type <t>Filter by type: component, example, util, hook, style, html.
--platform <p>Only entries that run on react, next, html, vue, angular, svelte, astro or vanilla.
--jsonPrint the raw index rows, for scripting.

Search command

search runs a local fuzzy match over component names, descriptions, and example names: a scored subsequence match, not a network call or an AI model. It rewards contiguous, early, and whole-word matches, so "date picker" still finds date-picker, but it only ranks results that are actually a subsequence of the query; it can't reason about what a component looks like or means.

npx @properui/cli@latest search "pricing"
npx @properui/cli@latest search "empty state" --limit 5

Each line of output shows the matched name, its platform (react or html), its layer, its title, and how many docs pages have a worked example using it, sorted by score. The match runs over export names as well as entry names and titles, so searching for a named export (ComboBox, say) finds the entry it lives in even when the entry itself is named something else. When nothing scores, it prints No match plainly rather than a low-confidence guess; treat that as a reason to check the registry index directly, not as proof the component doesn't exist (see the AI agents guide).

Command options

OptionDescription
--limit <n>Maximum results (default 20).
--platform <p>Only entries for this platform (react, html, vue, …).
--registry <source>Registry directory or base URL for this run (see below).

Diff command

diff compares the files already in your project against the current registry version, so you can see what you've changed (or what's out of date) before pulling an update:

npx @properui/cli@latest diff              # every installed component
npx @properui/cli@latest diff button       # just one

Requires components.json, so run init first.

Adding example pages

add example installs a complete, functional page (a dashboard, a settings screen, a marketing landing page) along with every component and dependency it uses.

npx @properui/cli@latest add example settings-01
npx @properui/cli@latest add example hero-split-image-01

Once an example is installed, it's your code: modify, delete, or extend it however your project needs. The CLI never re-writes an example file after it's on disk unless you pass --overwrite.

Not sure of the exact name? List them first:

npx @properui/cli@latest list --type example

add example accepts the same options as add (--install, --overwrite, --path, --dry-run, --no-optional, --with-demos, --registry, -y/--yes).

Login command

login stores a registry token at ~/.properui/auth.json. It's only needed for a private registry: the public one is anonymous, and add works without ever logging in.

npx @properui/cli@latest login
npx @properui/cli@latest login --token <token>

Run with no --token in an interactive terminal and it prompts you to paste one (after pointing you at <registry>/account/tokens to create it). The token is reused by every later command on the same machine. You only need to do this once.

Command options

OptionDescription
--token <t>Use this token instead of prompting for one.
--registry <source>Registry directory or base URL to store the token against.
-y, --yesNever prompt: fails with an error if no token is given.

Non-React projects

The TSX components are the primary, fullest layer. For Vue, Nuxt, Angular, Svelte, SvelteKit, Astro and plain HTML, the registry also carries a curated set of html entries: @properui/html snippets named <component>-html (buttons-html, modal-html, ...), type: "html", layer: "html", with platforms: ["html", "vue", "angular", "svelte", "astro", "vanilla"]. The frameworks page lists which components exist there.

init detects the platform from package.json and config files (nuxt.config.*, angular.json, svelte.config.js, astro.config.*, a vite.config.* using @vitejs/plugin-vue or @sveltejs/vite-plugin-svelte). No package.json, or no known framework, means plain HTML. On the html platform it writes "platform": "html" into components.json, skips utils/cx.ts, the providers and the TSX @source line, and either wires @import "@properui/tokens/theme.css"; @import "@properui/html/css"; plus an @source for the snippets into a Tailwind v4 stylesheet or, with no such stylesheet, prints the no-build lines:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@properui/tokens/dist/properui.min.css" />
<script src="https://cdn.jsdelivr.net/npm/@properui/html/dist/properui-html.iife.js" data-auto-init defer></script>

The last line it prints is the install: npm i @properui/tokens @properui/html @properui/elements. Pass --platform react or --platform html to override detection.

On an html project, add buttons installs buttons-html: the .html snippets land in <components alias>/buttons/ and are recorded in components.json like any other entry. A React-only entry fails with one line that names the HTML alternative when there is one:

npx @properui/cli@latest add data-table
# error "data-table" is React-only and this project's components.json platform is "html".
#       Use the HTML equivalent instead: properui add table (installs table-html).

info --json reports platform, framework and installedHtml, which is what an agent should read before deciding between TSX and HTML.

Pointing at another registry

A registry source is either an HTTP(S) base URL or a directory on disk. Both expose the same shape: index.json plus one <name>.json per entry. Every command accepts --registry <source>, and the source is resolved in this order:

  1. --registry <source> on the command line
  2. the REGISTRY_URL environment variable
  3. the registry field in components.json
  4. the built-in default, https://properui.dev/r

The hosted registry at https://properui.dev/r goes live when the docs site deploys. Until then, working from a clone of this repo, build the registry locally and point the CLI at the output directory:

pnpm registry:build
npx @properui/cli@latest add button --registry ./packages/registry/dist
# or: REGISTRY_URL=./packages/registry/dist npx @properui/cli@latest add button

Usage examples

Configure an existing Next.js project and add a handful of base components:

npx @properui/cli@latest init --nextjs
npx @properui/cli@latest add button input select badges avatar

Configure an existing Vite app, then preview what an add --all would do before committing to it:

npx @properui/cli@latest init --vite
npx @properui/cli@latest add --all --dry-run

Search for a layout, install it, and overwrite it later once you've pulled in an update:

npx @properui/cli@latest search "hero split image"
npx @properui/cli@latest add example hero-split-image-01
npx @properui/cli@latest add example hero-split-image-01 --overwrite

Troubleshooting

"No components.json found." Run init first, from the directory that holds your package.json.

Imports point at the wrong place. The alias in components.json no longer matches tsconfig.json. Fix the tsconfig path and re-run init --overwrite.

A component installed but renders unstyled. Your stylesheet is missing the @source line for the directory the components landed in. See Installation.

Debugging. Set PROPERUI_DEBUG=1 to get a full stack trace instead of a single error line.

FAQs