Skip to content
Nana UI

Reusability & Composition

Nana components are designed to be consumed through one stable contract — attributes, slots, and events — and themed through design tokens, CSS variables, and ::part(). Stay on that surface and the same component drops into any screen, in any framework, with no forking and no shadow-DOM hacks.

Tokens flow in, the component exposes a public API, your app composes it — and you theme once at the edges.

design tokens component (public API) your app
┌──────────────┐ ┌───────────────────────────┐ ┌──────────────┐
│ --nana-color │ │ attributes variant/size │ │ <nana-card> │
│ --nana-space │ ───▶ │ slots header/body │ ───▶ │ <nana-…/> │
│ --nana-radius│ │ events nana-change │ │ </nana-card>│
└──────────────┘ └───────────────────────────┘ └──────────────┘
▲ ▲ style via ::part / CSS vars │
└────────────────────┴─── theme once, reuse everywhere ──┘

1 · One component, many uses (attributes)

Section titled “1 · One component, many uses (attributes)”

Behaviour and appearance are props. No variant means no new component, no copy-paste, no custom CSS.

<nana-button variant="primary">Primary</nana-button>
<nana-button variant="secondary">Secondary</nana-button>
<nana-button variant="ghost">Ghost</nana-button>
<nana-button variant="danger" size="sm">Delete</nana-button>
<nana-button variant="primary" loading>Saving…</nana-button>

Structure comes from slotted children, so you assemble layouts from primitives instead of reaching inside them.

<nana-card>
<nana-card-header>
<nana-card-title>Monthly statement</nana-card-title>
<nana-card-description>June 2026</nana-card-description>
</nana-card-header>
<nana-card-content>
Your balance increased by 4.2%.
</nana-card-content>
</nana-card>

Restyle through the three supported layers below — never by targeting shadow-DOM internals directly. A token change re-skins every instance at once.

:root {
--nana-color-primary-600: #0f1f1d;
--nana-radius-md: 8px;
}
nana-table { --nana-table-pad-x: 1rem; --nana-table-pad-y: .5rem; }
nana-dialog::part(panel) { box-shadow: 0 20px 60px rgb(0 0 0 / .2); }

The icon set is a shared registry: register a glyph once and every <nana-icon> can use it by name. The bundled glyphs are code-split — they load on demand, so apps that register only their own icons never ship the full set.

import { registerIcons } from "@nana-tec/ui-components";
registerIcons({
github: { viewBox: "0 0 24 24", paths: ["M12 .5C5.37.5 …"] },
});

If you find yourself writing !important or piercing the shadow DOM, there’s almost always a prop, slot, or part for it.

<nana-icon name="settings" size="sm"></nana-icon>
<nana-button variant="ghost" size="sm" icon-only>
<nana-icon name="more_vert"></nana-icon>
</nana-button>
<nana-icon name="settings" class="!w-[16px] !h-[16px]"></nana-icon>
NeedReach forNot
Different look / statevariant, size, loading attributesCloning the component
Custom structureSlots (header / body / footer)Wrapper divs around internals
Brand colours / spacingDesign tokens + documented CSS vars!important overrides
Style an internal part::part()Piercing the shadow DOM
A new glyphregisterIcon() once, reuse by nameInline one-off SVGs everywhere