# Owning the Source

> Source: https://design.nanatec.co.ke/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.

```bash
npx @nana-tec/ui init
npx @nana-tec/ui add button
```

## 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.

<details>
<summary>React</summary>

```
src/
├── components/ui/button/
│   ├── button.tsx
│   ├── index.ts
│   └── types.ts
└── lib/nana-ui/
    ├── styles/button.ts
    ├── utils/cn.ts
    └── utils/types.ts
```

```tsx
import { Button } from "@/components/ui/button";

<Button variant="primary" size="lg">Save</Button>
```

</details>

<details>
<summary>Svelte</summary>

```
src/lib/
├── components/ui/button/
│   ├── Button.svelte
│   ├── index.ts
│   └── types.ts
└── nana-ui/
    ├── styles/button.ts
    ├── utils/cn.ts
    └── utils/types.ts
```

```svelte
<script lang="ts">
  import { Button } from '$lib/components/ui/button';
</script>

<Button variant="primary" size="lg">Save</Button>
```

</details>

<details>
<summary>Web Components</summary>

```
src/
├── components/ui/button/
│   ├── index.ts
│   ├── nana-button.ts
│   └── nana-button.styles.ts
└── lib/nana-ui/utils/
    ├── custom-element.ts
    └── types.ts
```

```ts
import "@/components/ui/button/nana-button.js";
```

```html
<nana-button variant="primary">Save</nana-button>
```

</details>

### 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.

```bash
npx @nana-tec/ui list
```

lists 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

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:

```bash
npx @nana-tec/ui add editor
```

```
> resolving 1 component
  pulling in color-picker, icon, utils-custom-element, utils-types
```

Relative imports are rewritten to your project's aliases as the files are
written, so nothing points back at the library.

## 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:

```css
@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](/guides/reusability/).

```html
<nana-button style="--nana-button-bg:#2563eb">Buy now</nana-button>
```

## 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:

```json
{
  "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:

```ts
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

```bash
npx @nana-tec/ui update            # every installed component
npx @nana-tec/ui update button     # just this one
```

`update` 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

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.

```bash
npx @nana-tec/ui add login-form
```

```
> resolving 1 component
  pulling in alert, button, card, checkbox, input, utils-custom-element
```

Blocks 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.

```html
<nana-login-form heading="Welcome back" forgot-href="/reset"></nana-login-form>
```

```js
document.querySelector("nana-login-form")
  .addEventListener("nana-login", (e) => signIn(e.detail));
```

## 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:

```ts
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

`add` brings in `styles-nana-theme`, a `@theme` block mapping the `--nana-*`
tokens onto Tailwind's theme:

```css
@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:

```css
@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

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:

```tsx
<Button variant="primary" className="rounded-none px-8">Save</Button>
```

### 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:

```tsx
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

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:

```html
<nana-button style="--nana-button-bg: var(--color-primary)">Save</nana-button>
```

## Pinning a version

`add` takes the current release by default. Name an exact version to install
from a published snapshot instead:

```bash
npx @nana-tec/ui add button@0.0.25
```

Snapshots 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

```bash
npx @nana-tec/ui remove button
```

Shared 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.

```bash
npx @nana-tec/ui list --installed
```

shows what this project has, and which version each came from.

## 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 |
