Hone UI

Copy components with the CLI

Copy a component's source into your project with npx hone-ui add, and compare it with upstream.

The CLI copies components into your project so you can edit them. The npm package and the CLI come from the same source files, so a copied component matches the released one.

npx hone-ui init
npx hone-ui list
npx hone-ui add button
npx hone-ui diff button

Every hint the CLI prints names your package manager. It reads the lockfile in the project folder or a parent folder, so a pnpm project sees pnpm add @hone-ui/react react-aria-components and pnpm dlx hone-ui add button. Yarn 2 and newer get yarn dlx, Yarn 1 gets npx, Bun gets bunx, and a project without a lockfile gets npm and npx. This page writes npx throughout.

Run hone-ui <command> --help for the usage, the flags and an example of one command.

Find a component with list

list prints the name of every component you can add, in columns. The shared internal-* files stay out, because they come with the components that use them. add and diff suggest up to three close names for a typo and point at list:

npx hone-ui add buton
# Unknown component: buton.
# Did you mean button?

Set up with init

Run init once, from the folder with your package.json. It writes two files and copies no styles.

  • hone-ui.json records the folder for copied files and the look. add and diff read it, so you never repeat --dir.
  • src/components/ui/hone-ui/hone-ui.css imports @hone-ui/react/styles/material.core.css from the package. The core holds the theme variables, the page baseline and shared helpers but no component styles, so no copied component's CSS ships twice. Import hone-ui.css once at the root of your app, in place of @hone-ui/react/styles/material.css.
hone-ui.json
{
  "dir": "src/components/ui/hone-ui",
  "look": "material",
  "css": "src/components/ui/hone-ui/hone-ui.css"
}

Pass --dir to pick another folder and --look minimal for the Minimal look. Without --look, init uses the look your imports of @hone-ui/react/styles already name, and material when they name none. init also imports the styles of components you copied before it ran, and names any of @hone-ui/react and react-aria-components that your package.json lacks. It stops if hone-ui.json exists, and it keeps a hone-ui.css that exists. Earlier versions copied the core into a look folder. init leaves such a file alone and tells you it can go.

What add copies

Each command copies the component folder and everything it imports.

  • The component's .tsx file, its index.ts, its base style file (.base.css) plus the style file of the look in hone-ui.json (.material.css or .minimal.css, never both), and its AI guide. The style files are plain CSS, compiled from the library's Tailwind source, so your app needs no Tailwind.
  • Parts that belong with it. card brings CardContent, CardActions, CardHeader and CardMedia. dialog brings Backdrop and the four dialog parts. select brings MenuItem, and radio brings RadioGroup and FormControlLabel.
  • Shared files from internal, system and styles that only copied code uses, kept in the same folder layout, so every relative import still resolves.

The theme and the sx resolver are the exception. The copied files never include the theme context, its hooks or resolveSx. They import useTheme from @hone-ui/react/styles and resolveSx from @hone-ui/react/system, so a ThemeProvider from the package reaches every copied component, including a function sx. That also keeps the theme code out of your copy. The FormControl context stays a copy, because only copied components read it. A custom input that calls useFormControl from @hone-ui/react does not see a copied FormControl, so import useFormControl from the copy.

Files land in src/components/ui/hone-ui unless hone-ui.json or --dir names another folder. Names are not case sensitive, so IconButton and icon-button both work.

Options

OptionCommandDescription
-d, --dir <path>init, add, diffThe folder for copied files, relative to the folder that holds hone-ui.json. Wins over hone-ui.json.
-l, --look <name>initmaterial or minimal. Defaults to the look your imports of @hone-ui/react/styles use, and to material when they name none.
-o, --overwriteaddReplace files that you changed. Lists each file it replaced.

Each command accepts only its own options. Any other flag, such as add --look minimal or init --overwrite, stops with an error that names the flag and the command. A flag that no command knows stops with Unknown option --bogus.

Without --overwrite, add leaves a file alone when its content differs from the registry and lists it at the end. Files that already match are not touched.

Switch looks

Change look in hone-ui.json, then run add with no names:

npx hone-ui add

With no names, add re-syncs every component you already copied. For each one it copies the new look's style file, deletes the old look's style file when it still matches the registry, and updates the imports in hone-ui.css, including the core import.

add button after a switch touches only button and the files it imports. It lists the other components that still have the old look's styles, so you know to run add with no names.

A style file of the old look that you edited stays on disk, and its import stays in hone-ui.css. The component still gets the new look's file, so both looks apply to it until you delete the old file and its import. add names each file it kept and says why.

After copying

add and diff need a hone-ui.json. They look for it in the current folder, then in each parent folder up to the repository root, and stop with an error that tells you to run init when they find none. Paths in the config, and a relative --dir, start at the folder that holds hone-ui.json. For init, that is the current folder.

add appends an @import for each copied style file to hone-ui.css, after the package's core styles, so there is nothing to import by hand. It prints an install command for the packages that your package.json does not list yet, and nothing when it lists them all. If a write fails, add restores the files it had changed, so a failed run leaves no partial copy.

The copied components still need @hone-ui/react for the theme code that must stay shared, such as ThemeProvider and useTheme, and they import their types from it. The core style sheet replaces @hone-ui/react/styles/material.css for copied components only. Components you use from the npm package need that package's look import as well.

Recreate a deleted hone-ui.css

If hone-ui.css is gone, run add. It writes the file again with the look's core import and an @import for the style files of every component you copied, and says that it did. add with no names does the same and re-syncs your components, and it works before you copied any component. init still stops when hone-ui.json exists, and its error points at add when only the style sheet is missing.

Copy one component into an existing app

An app that already uses @hone-ui/react can copy a single component and keep the rest from the package. Import the package look first and hone-ui.css after it, so your copy wins:

app.css
@import '@hone-ui/react/styles/minimal.css';
@import './components/ui/hone-ui/hone-ui.css';

hone-ui.css loads the look's core a second time. The core's base rules, such as the ButtonBase reset, sit in the hone-ui.base sub-layer of hone-ui. Rules written directly in hone-ui always beat a sub-layer, so the reset cannot wipe the background of a package Button, whichever of the two files loads last.

That protection covers the resets only. Your edited copy beats the package's rules when hone-ui.css loads last. If hone-ui.css loads first, the package look comes later and its rules win over your copy, so keep the order shown above.

Use the package look import whenever the app renders components from @hone-ui/react. The core that hone-ui.css imports has no component styles.

See what changed upstream

Copied files are yours, so nothing updates them. diff compares your copy with the registry inside the CLI version you run, so use npx hone-ui@latest diff to compare with the newest release.

npx hone-ui diff          # lists the copied components that differ
npx hone-ui diff button   # prints the changes to Button and the shared files it imports

The output is a unified diff. Lines starting with - are in your copy and lines starting with + are upstream. Your own edits show up too, because the CLI keeps no record of the version you copied. diff also lists files that upstream has and your copy lacks. To take upstream, run add with --overwrite, which replaces your edits in those files.

diff exits with code 1 when any copy differs, like git diff --exit-code, so a CI step can fail on drift. In a project with no copied components it prints "Nothing copied yet" and exits with code 0, because there is nothing to drift.

The registry

The registry follows the shadcn registry format. The build writes a registry.json index and one JSON file per item into the CLI's dist/registry folder. Components are registry:ui items. Each shared source file is its own registry:lib item. The looks have no items, because the package serves their core style sheets. Every file carries a target under ~/components/ui/hone-ui, and registryDependencies hold full URLs, because bare names would resolve against shadcn's own registry and collide with its button and card.

Set HONE_REGISTRY_URL when you build to name the folder that serves the JSON, for example HONE_REGISTRY_URL=https://example.com/r pnpm --filter hone-ui build. Until the registry has a host, a placeholder address stands in, so npx shadcn add <url> does not work yet. The CLI itself reads the bundled registry and needs no host.

On this page