Guides
Owning the Source
Copy Nana UI components into your project with the CLI, and change them however you like.
There are two ways to use Nana UI, and they solve different problems.
Installing @nana-tec/ui-components gives you a versioned dependency: upgrades
arrive with npm update, and the component internals stay ours. That is the
right default, and most projects should stop there.
The CLI gives you the other trade. It copies component source into your repository, where you can read it, change it, and take it in a direction the design system never planned for — at the cost of no longer receiving updates automatically.
npx @nana-tec/ui initnpx @nana-tec/ui add buttonComponents native to your framework
Section titled “Components native to your framework”What lands on disk depends on the framework you are using. init detects it and
records it in nana-ui.json, and add serves the matching implementation:
| Framework | Detected from | You get | Components |
|---|---|---|---|
| React | react or next in dependencies | button.tsx, styled with Tailwind | 14 |
| Svelte | svelte.config.js, or svelte in dependencies | Button.svelte, styled with Tailwind | 14 |
| Web Components | anything else | nana-button.ts + Lit styles | 65 |
React and Svelte ship the same core set:
alert, badge, button, card, checkbox, dialog, dropdown, input,
radio, select, switch, table, tabs, tooltip.
Web Components remain the widest surface at 65, including the pieces the core
set does not cover yet — editor, carousel, date range, colour picker, QR code
and the rest. Run nana-ui list to see what your framework actually offers.
These are not one component wrapped three ways. Each is written natively, so a
React button is a real React component with forwardRef and typed props, and a
Svelte one uses runes and snippets. What they share is the design — the same
tokens, variants, sizes, focus behaviour and reduced-motion handling — not the
implementation.
React
src/├── components/ui/button/│ ├── button.tsx│ ├── index.ts│ └── types.ts└── lib/nana-ui/ ├── styles/button.ts ├── utils/cn.ts └── utils/types.tsimport { Button } from "@/components/ui/button";
<Button variant="primary" size="lg">Save</Button>Svelte
src/lib/├── components/ui/button/│ ├── Button.svelte│ ├── index.ts│ └── types.ts└── nana-ui/ ├── styles/button.ts ├── utils/cn.ts └── utils/types.ts<script lang="ts"> import { Button } from '$lib/components/ui/button';</script>
<Button variant="primary" size="lg">Save</Button>Web Components
src/├── components/ui/button/│ ├── index.ts│ ├── nana-button.ts│ └── nana-button.styles.ts└── lib/nana-ui/utils/ ├── custom-element.ts └── types.tsimport "@/components/ui/button/nana-button.js";<nana-button variant="primary">Save</nana-button>What ships today
Section titled “What ships today”Web Components has the full library — 72 components and a block. React and Svelte carry the core set: button, input, select, dialog, dropdown, tabs, table, pagination, checkbox, radio, switch, tooltip, card, badge, divider, alert, empty state, meter input and bulk progress, plus five blocks.
npx @nana-tec/ui listlists what your framework’s registry actually offers, so it is always accurate rather than whatever this page last said.
Paths and aliases follow each framework’s conventions — $lib/components/ui on
Svelte, @/components/ui on React — and init reuses whatever alias your
tsconfig already declares.
Dependencies come with it
Section titled “Dependencies come with it”Components are not standalone files. nana-editor uses nana-icon and
nana-color-picker; almost everything uses the shared customElement helper and
type definitions. The registry records those relationships, so one command
installs the whole set:
npx @nana-tec/ui add editor> resolving 1 component pulling in color-picker, icon, utils-custom-element, utils-typesRelative imports are rewritten to your project’s aliases as the files are written, so nothing points back at the library.
What stays on npm
Section titled “What stays on npm”Design tokens do. Colours, spacing, radii and typography live in
@nana-tec/ui-tokens and @nana-tec/ui-themes, which init installs for you:
@import "@nana-tec/ui-themes/base";Ejecting tokens as well would freeze every project’s palette at the version it installed on, and the point of a design system is that this cannot happen. You own the components; the system owns the variables they read.
You do not have to eject a component to restyle it. Every component exposes CSS
custom properties and ::part hooks first — see
Reusability & Composition.
<nana-button style="--nana-button-bg:#2563eb">Buy now</nana-button>Requirements
Section titled “Requirements”The written files import each other through the aliases recorded in
nana-ui.json, so your bundler and tsconfig both have to resolve them. init
reads compilerOptions.paths and reuses whatever prefix it finds; if there is
none, it tells you what to declare.
This means the CLI targets bundler-based projects — Vite, Next, Astro, Rspack. If you load Nana UI from a CDN, install the npm package instead.
The project also has to compile TypeScript.
Web Components only: that target uses Lit, whose decorators most templates —
create-next-app among them — do not enable. init checks and offers to add:
{ "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false }}Without both, the copied files are written correctly but fail to typecheck.
React and Svelte need none of this — decorators are a Lit concern, so init
skips the check entirely on those targets.
init records TypeScript support by looking for a tsconfig.json or
jsconfig.json, and add refuses on a project without one rather than writing
files it cannot build. There is no JavaScript output on
purpose: the components use decorators, so a compiled copy would be
__decorate([property({ type: Boolean })], …) rather than source worth owning —
which defeats the point of copying it in at all. On a JavaScript project, install
@nana-tec/ui-components from npm; the components are standards-based custom
elements and work the same.
There is no autoloader for copied components, and there cannot be: the autoloader resolves a tag name to a module inside the package, and your copies live at paths only your project knows. Each file registers its own tag when it is imported, so importing it once anywhere — an entry point, or the module that renders it — is what makes the tag work:
import "@/components/ui/button/nana-button.js";The two models mix, so a project can autoload most of the library from npm and copy in only the components it needs to change.
Updating
Section titled “Updating”npx @nana-tec/ui update # every installed componentnpx @nana-tec/ui update button # just this oneupdate can tell an untouched file from one you have changed, because add
records what it wrote in nana-ui.lock.json. Files still matching that record
are refreshed in place; files you have edited are reported and left alone unless
you pass --force.
That distinction is the reason the lock file exists. Once source lives in your repository, a file that differs from the registry is ambiguous on its own — it could be an upstream improvement you have not taken yet, or the change you made on purpose. Only a record of what was installed can tell the two apart.
Commit nana-ui.lock.json. It is how the next person, and the next machine, get
the same answer.
Blocks
Section titled “Blocks”A block is a composed starting point rather than a primitive: it arrives already assembled from several components, and you are expected to rewrite it. Blocks currently exist for the Web Components target only.
npx @nana-tec/ui add login-form> resolving 1 component pulling in alert, button, card, checkbox, input, utils-custom-elementBlocks are the clearest case for owning source. A login form is nine tenths decisions your product has already made — which fields, what the copy says, where it posts — so a configurable component would need a prop for each of them. Copied in, it is just a file you edit.
add --all installs every component but no blocks, because a block is a choice
rather than a building block.
<nana-login-form heading="Welcome back" forgot-href="/reset"></nana-login-form>document.querySelector("nana-login-form") .addEventListener("nana-login", (e) => signIn(e.detail));Using Tailwind
Section titled “Using Tailwind”The React and Svelte components are Tailwind. They ship as utility classes rather than a stylesheet, so changing one is editing a string rather than hunting through CSS.
Every component’s variants, sizes and shapes live in one file of its own, next to
the component but apart from it — styles/button.ts for the button:
export const variants: Record<ButtonVariant, string> = { primary: 'bg-primary text-primary-foreground hover:brightness-90', ghost: 'bg-transparent text-foreground border-line hover:bg-surface-muted',};That file is where you restyle a component. Editing it changes every button in the project at once, and the component itself stays about rendering — it reads the map, it does not contain it.
Two reasons it is split out rather than sitting in the component. Restyling should not mean reading past a component’s logic to find a string. And React and Svelte are separate implementations of one design: keeping the classes in a file they both read is what stops the same variant looking different in the two frameworks, which is not hypothetical — it had already happened twice before the split.
Note what those class names are: bg-primary, not bg-neutral-900. They are
semantic names that resolve to design tokens, which is what keeps the components
following the design system instead of drifting from it.
The token bridge
Section titled “The token bridge”add brings in styles-nana-theme, a @theme block mapping the --nana-*
tokens onto Tailwind’s theme:
@theme inline { --color-primary: var(--nana-surface-accent); --color-primary-foreground: var(--nana-surface-accent-contrast); --color-line: var(--nana-surface-border); --radius-md: var(--nana-radius-md); --text-sm: var(--nana-typography-font-size-sm);}Import it once, after Tailwind:
@import "tailwindcss";@import "@nana-tec/ui-themes/base";@import "@/lib/nana-ui/styles/nana-theme.css";Now bg-primary in a component and bg-primary in your own markup are the same
colour, and re-theming a token moves both. That is the whole point of the bridge:
you get editable utility classes without the design system losing control of the
values.
Requirements
Section titled “Requirements”Tailwind v4 is required for the React and Svelte components, along with clsx
and tailwind-merge — the CLI installs the latter two for you. Classes you pass
are merged with tailwind-merge, so your override wins rather than fighting:
<Button variant="primary" className="rounded-none px-8">Save</Button>Using your own tokens instead
Section titled “Using your own tokens instead”If your project already has its own design tokens mapped into Tailwind, you do
not need the bridge at all. Skip styles-nana-theme and rewrite the variant
strings against your own names:
const variants = { primary: 'bg-foreground text-background', accent: 'bg-accent text-background',};That is the model working as intended — you own the file, and the component now follows your system rather than ours.
Web Components are different
Section titled “Web Components are different”That target keeps its shadow-DOM CSS, because Tailwind cannot reach inside a
shadow root. Utilities on a nana-* element apply to the host, so layout classes
(mt-4, w-full) work while appearance ones do not. Change appearance through
the CSS variables instead:
<nana-button style="--nana-button-bg: var(--color-primary)">Save</nana-button>Pinning a version
Section titled “Pinning a version”add takes the current release by default. Name an exact version to install
from a published snapshot instead:
npx @nana-tec/ui add button@0.0.25Snapshots are immutable and stay online after later releases, so a project can
reproduce an install months later. They are per framework, at
/r/<framework>/v/<version>/. One command installs from one version —
components share helpers, and mixing versions across a dependency graph is how
you get a component calling into a helper that has since changed shape. Ranges
are rejected rather than resolved, because a source copy is a one-time decision
and ^1 would mean something different every time you ran it.
Only releases from 0.0.25 onward have snapshots; earlier versions predate them.
Removing
Section titled “Removing”npx @nana-tec/ui remove buttonShared helpers are kept if another installed component still needs them, and a
file you have edited is never deleted without --force. Empty directories are
cleaned up afterwards.
npx @nana-tec/ui list --installedshows what this project has, and which version each came from.
Command reference
Section titled “Command reference”| Command | What it does |
|---|---|
init | Writes nana-ui.json and installs the theme packages |
add <name…> | Copies components in, with every dependency they need |
add --all | Copies in every component |
add <name>@<version> | Copies in from a pinned release |
update [name…] | Refreshes installed components, skipping ones you edited |
remove <name…> | Deletes components, keeping helpers others still need |
list | Shows what the registry offers |
list --installed | Shows what this project has |
| Flag | Effect |
|---|---|
--dry-run | Reports what would change, writes nothing |
--overwrite | add: replaces files that differ from the registry copy |
--force | update / remove: act on files you have edited |
--skip-install | Writes files without touching package.json |
--cwd <dir> | Runs as if started in another directory |
--registry <url> | Reads from a different registry |