Introduction

One component library for web, native and desktop, on one substrate. One import, one root, and nothing to configure.

@hanzo/ui is the Hanzo component library. Every component renders through @hanzo/gui, a style engine for React and React Native that takes styles as props and compiles them to atomic CSS on the web. Because the engine is cross-platform, so is the library: the same Switch renders as a div in a browser and as a native view on a phone.

There is no utility-class build here. No Tailwind, no PostCSS plugin, no Radix primitive underneath a component, no registry, no components.json, and no CLI that copies source into your repository. You install a package and import from it.

Install and render

pnpm add @hanzo/ui @hanzo/gui
main.tsx
import { Hanzo, Button } from '@hanzo/ui'
export default function App() {
return (
<Hanzo>
<Button>Ship</Button>
</Hanzo>
)
}

That is the whole setup. Hanzo is the root, and it carries three things that used to be each app’s job: the gui config the tokens resolve against, the stylesheet, and the theme. There is no CSS to import and no generator to run — the sheet is produced when the package is published, by rendering every component and harvesting the CSS that render produces, so the first paint and every server render already have their rules.

The one thing a host still does is tell its bundler that react-native means react-native-web. That is one alias, and each host page under Installation shows where it goes.

Styling is props

A style is a prop on the component, and a responsive style is the same prop under a breakpoint key.

import { Button, Text, XStack, YStack } from '@hanzo/ui'
<YStack gap="$4" p="$5" $md={{ p: '$8' }}>
<Text fontSize="$5" color="$color11">
Balance
</Text>
<XStack gap="$2">
<Button>Send</Button>
<Button variant="outline">Receive</Button>
</XStack>
</YStack>

Breakpoints are mobile-first minimum widths: $sm 640, $md 768, $lg 1024, $xl 1280. A value that starts with $ is a token — a colour, a size, a step of the spacing ramp — and resolves against the theme, so $background is a different colour in light and dark without the call site knowing. Font size is always a token, never a number: fontSize="$3", not fontSize={14}. A person can set their own type scale and the ramp $n reads moves with it; a hardcoded 14 cannot. Theming has the ladders.

Layout

Stacks are flex. XStack lays out in a row, YStack in a column, and both take gap, items and justify. A grid is a real CSS grid, at @hanzo/ui/grid, whose tracks come from the container rather than the children — see Grid.

What is where

ImportWhat it is
@hanzo/uiThe component API — every primitive, the stacks, the type scale, and Hanzo.
@hanzo/ui/productThe app layer: charts, page chrome, status tags, detail panes, the theme toggle.
@hanzo/ui/blocksContent as data, and the renderers that draw it.
@hanzo/ui/gridGrid and Cell, web only.
@hanzo/ui/product/pureThe rules the product layer runs on, with no component attached.
@hanzo/ui/csssubstitute, for tests a DOM without a cascade cannot answer.
@hanzo/ui/nextThe Next wrapper that sets the web aliases for both bundlers.

The catalog pages on this site — Components, Blocks, Product — are read from the package itself: each one renders a module’s examples beside their source and quotes its types.

Questions

Do I need Tailwind? No. Components take style props, and the identity is plain CSS custom properties. If an app arrives with class strings, Box, sx and css read them through tw and turn them into the same props; nothing reaches the DOM as a class the package did not define.

Is this copy-and-paste? No. Install the package and import from it. The components are versioned with the package, and the export map is what a subpath resolves through, so a deep import keeps working across patches.

Which frameworks? Any React 19 renderer, once the bundler knows the react-native alias. There are pages for Vite, Next, One, Expo and Tauri.

Can I use it commercially? Yes. Apache-2.0 or MIT, at your option.