Kanzo UI
Getting started

Installation

The packages, the peers you may not need, one Tailwind build and one provider.

Install the packages

@kanzo-tech/ui depends on @kanzo-tech/theme, so installing the first brings the second.

npm install @kanzo-tech/ui

Install the peers

React 19, lucide-react for icons, and Tailwind CSS v4 — the library ships Tailwind source, and your build compiles it.

npm install react react-dom lucide-react tailwindcss

Everything else is optional, and only if you import the matching subpath. Each subpath statically re-exports its engine, so installing its peers is what buys you that subpath and nothing else:

SubpathWhat it isOptional peers
@kanzo-tech/ui/tableThe connected data table@tanstack/react-table
@kanzo-tech/ui/analyticsThe charts, over Mosaic + DuckDB@kanzo-tech/mosaic (with its peers @uwdata/mosaic-core and @uwdata/mosaic-sql), @uwdata/vgplot, @uwdata/mosaic-plot, valibot
@kanzo-tech/ui/editorThe CodeMirror editors@codemirror/state, @codemirror/view, @codemirror/language, @codemirror/commands, @codemirror/search, @codemirror/autocomplete, @lezer/highlight

The base entry never imports any of them. That is deliberate — a static import of an optional peer from the root barrel makes import { Button } throw for everyone who did not install it, and it is why each of these has a presentational half in the root barrel and a connected half on the subpath. See the engine rule.

The graph is a package, not a subpath

@kanzo-tech/graph is installed separately, and the reason it is not a fourth row above is the reason it is not in @kanzo-tech/ui at all: its renderer peer is required, not optional, and a required WebGL dependency would change what the base library is — the first admission rule excludes graphs by name.

pnpm add @kanzo-tech/graph @cosmos.gl/graph @kanzo-tech/mosaic @kanzo-tech/ui lucide-react react

Every peer is required: @kanzo-tech/mosaic is the engine the graph reads through and the crossfilter it shares with the charts. The graph imports nothing of graphar's or fossil's — the host attaches a GraphAr graph with @kanzo-tech/graphar-duckdb's attachGraph, its own dependency, and names the catalog to the root.

The AI surfaces are a package too

@kanzo-tech/ai is the other sibling, and it is separate for a different reason — not a required renderer, but the barrel itself. The line is does the component know a model exists: Assist asks one and Chat draws its answers, which is domain knowledge the generic vocabulary does not carry, and a consumer who wants a Button must not pay for a transcript.

pnpm add @kanzo-tech/ai @kanzo-tech/llm ai @ai-sdk/react

The model is reached through @kanzo-tech/llm, which has no React: localModel, on @kanzo-tech/llm/local, runs a model in the page, and every model call goes through its stream or generate, asked once. The AI SDK's own names — the agent, tool, the message types — are imported from ai. ai and @ai-sdk/react are required peers, installed once, because the host's agent and these components must share one copy of the SDK's types. /local's WebLLM peers are optional, and only that subpath reaches them. Everything ai draws with is @kanzo-tech/ui and Ark, both required peers, and its appearance is one more line in the stylesheet below — its own @sources, streamdown's included, and nothing else.

Authentication is a package that draws nothing

@kanzo-tech/auth is the third sibling, and it is the one that needs none of the above: it does not import @kanzo-tech/ui, it ships no component and it has no stylesheet. The first admission rule excludes auth by name, for the reason the log-out affordance is missing from the sidebar — a library whose vocabulary every product shares should not know what a session is.

pnpm add @kanzo-tech/auth

The session is read on your server from the token the platform's edge forwards and handed to AuthProvider as a prop (the session page), so the browser half needs nothing else. The root barrel carries no engine, which is what makes it the door every consumer's browser half opens. The other two each name theirs, and you install only the one you open.

subpathwhat it isinstall alongside
@kanzo-tech/auth/serversessionReader: the forwarded token verified and read into a Session, framework-agnosticjose
@kanzo-tech/auth/nextkanzoAuth: the server component's session, the proxy and the API forwarderjose, next 16+

The two server doors are Node's, and nothing on them reaches React — packages/auth/src/server.test.ts imports them without React to say so, rather than trusting this sentence.

Import the entries into your Tailwind stylesheet

app/globals.css
@import "tailwindcss";
@import "@kanzo-tech/ui/tailwind.css";
@import "@kanzo-tech/ai/tailwind.css"; /* only with @kanzo-tech/ai */
@import "@kanzo-tech/graph/tailwind.css"; /* only with @kanzo-tech/graph */

The entries are Tailwind source, not compiled CSS. @kanzo-tech/ui's carries the tokens, the generated themes, the dark variant, the typography plugin and the @source that finds its own components; @kanzo-tech/ai's adds its components and streamdown's, and @kanzo-tech/graph's the parts around its canvas. Your build compiles the page once — one preflight, one utilities layer, one variant order — and your own utilities resolve against the same tokens. Do not import @kanzo-tech/theme/tokens.css beside them: the entry already does.

Wrap the app in the provider

app/layout.tsx
import { KanzoThemeProvider } from "@kanzo-tech/ui";

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <KanzoThemeProvider>{children}</KanzoThemeProvider>
      </body>
    </html>
  );
}

Use a component

import { Button } from "@kanzo-tech/ui";

export default function Page() {
  return <Button>Get started</Button>;
}

Running the services

@kanzo-tech/auth talks to a server, and that server ships from this repository under the same tag as the packages: the identity service, Keycloak with the platform realm as code. A product adopting it includes the service's compose.yaml — and, in development, its compose.dev.yaml — by URL at the release its packages came from, the same files in development and in production, and declares itself — a client and its roles, and its organizations — from its own repository.

Server-side rendering

Render themeScript() in <head> so the theme attributes land before first paint. That one is not optional for an SSR host: every axis lives in browser storage, so without it the server paints the defaults and the client re-skins on hydration — a flash, plus a hydration mismatch in any control whose markup depends on the resolved appearance.

cookieStorageAdapter() is the optional half. It lets the server read the same source the client writes, so the rendered <html> already carries the attributes rather than acquiring them a moment later. Colour is not a reason to add it: every theme ships in the page at once and the choice is an attribute, so the script applies it like any other axis.

app/layout.tsx
import { KanzoThemeProvider, themeScript, cookieStorageAdapter } from "@kanzo-tech/ui";

<head>
  <script dangerouslySetInnerHTML={{ __html: themeScript() }} />
</head>
<KanzoThemeProvider storage={cookieStorageAdapter()}>{children}</KanzoThemeProvider>

See Theming for dark mode and the full axis list.

On this page