From registry to app
How one source file becomes the code of four styling tools, and the rules that make it work.
A component is written once in packages/registry. When a user runs axiom add, the CLI writes it into their app for their styling tool: StyleSheet, Unistyles, NativeWind or Uniwind. The result is one file that looks written by hand for that tool, with no trace of the other three.
This page explains what you write, what the CLI does with it, and the rules your files have to keep so it can.
What you write
packages/registry/atoms/badge
badge.tsxholds everything the four stylings share: props, state, accessibility, structure. It imports./badge.stylesand calls one hook,useBadgeStyles().badge.styles.<styling>.tsxholds only what differs: astyleobject for StyleSheet and Unistyles, class names for NativeWind and Uniwind.badge-tokens.tsmaps theme colors to the component’s roles. It’s the only token file you edit:bun run theme-csswrites the two CSS files from it.registry.jsonlists the item: its files, its npm dependencies, the other items it needs.
Write every file as it will look in the user’s project. Import other items through the default aliases (@/theme, @/components/core, @/components/ui…). Import files of the same item with a relative path (../use-switch): the CLI rewrites it to ./use-switch.
What the CLI does with it
1. resolve badge → badge, icon, text, theme, … (internalDependencies, recursively)
2. plan pick the files of the app's styling, give each one a destination
3. rewrite point every import to where the other files landed
4. inline merge badge.tsx and badge.styles.<styling>.tsx into one file
5. write only now, and only the files that changed
6. tokens register badge in the theme (TypeScript or CSS)
7. install the npm packages the copied files needSteps 1 to 4 run on every file before anything is written, so a failure leaves the app untouched. The code lives in packages/cli/src, and packages/cli/docs follows axiom add button through it function by function.
Inlining a styles file
Inlining is the step that makes the result look handwritten. The CLI replaces the call to the hook with its statements, and every {...styles.x(…)} with the expression the entry returns. Simplified:
import { useTagStyles } from "./tag.styles";
export function Tag({ label, selected }: TagProps) {
const styles = useTagStyles();
return (
<View {...styles.root(selected)}>
<Text {...styles.label}>{label}</Text>
</View>
);
}export function Tag({ label, selected }: TagProps) {
const { components } = useTheme();
return (
<View
style={[
styles.root,
{ backgroundColor: components.tag.background },
selected && { backgroundColor: components.tag.selected },
]}
>
<Text style={styles.label}>{label}</Text>
</View>
);
}
const styles = StyleSheet.create({ … });export function Tag({ label, selected }: TagProps) {
return (
<View className={cx("rounded-full px-3 bg-tag", selected && "bg-tag-selected")}>
<Text className="text-tag-foreground">{label}</Text>
</View>
);
}The styles file contract
The CLI can only inline a styles file that keeps these rules. It refuses one that breaks them, and says why.
- It declares one
use<Item>Stylesfunction. Its parameters have the names of the variables it’s called with. - The function ends by returning an object, one entry per styled element. Its other statements (
const { tokens } = useTheme()) are copied into each component that calls it, minus the ones that component doesn’t reach. - An entry is an expression, or an arrow function returning one. No block body: logic that needs statements goes in a function of the file.
- An entry spread on an element is an object of its props:
{ style }or{ className }. One that merges the caller’s props takes them by name:header: ({ style }) => ({ style: [styles.header, style] }). - An element that takes its color as a prop, like an icon, is exported by the styles file (
BadgeIcon). Unistyles wraps it withwithUnistyles; the other stylings export it as is. - With NativeWind and Uniwind, entries are whole class names (
bg-button-solid,active:bg-button-solid-pressed), picked from tables indexed by variant, size and state. A state lists what it changes, and the last class setting a property wins. classNamenever uses a rem-based size outside the spacing scale: a rem is 14px in NativeWind and 16px in Uniwind. Writeh-[24px].
NativeWind and Uniwind each have their file because they pass a color to a prop differently (cssInterop for one, withUniwind and colorClassName for the other). When nothing differs, the two files are the same.
The full contract, with every edge case, is in the registry README.
Tokens
Colors go through three levels: raw palette, semantic theme roles (colors.primary.default), then component tokens (badge.neutral.default.background). A component reads only its own tokens, so an app restyles it in one place.
badge-tokens.ts ──bun run theme-css──▶ badge-tokens.nativewind.css
badge-tokens.uniwind.css- With StyleSheet and Unistyles, the CLI copies
badge-tokens.tsto the app’stheme/components/badge.tsand registers it between the// axiom:…markers oftheme/components/index.ts. - With NativeWind and Uniwind, it copies the CSS instead:
--color-badge-neutralbecomes the classbg-badge-neutral, and each variable points to a theme color, so it follows light and dark withoutdark:.
Run bun run theme-css in packages/registry after changing a token, a theme color or a *-tokens.ts, and commit the generated files.
A few components (switch, slider, segmented-control, skeleton) keep their tokens in TypeScript with every styling: an animation interpolates their colors, which no class can express.
How the registry checks itself
The registry has no app around it, so aliases don’t resolve on their own. scripts/tsconfig.ts reads registry.json and writes one tsconfig.<styling>.json per styling, mapping every alias to its registry file the way the CLI maps it in a project. moduleSuffixes resolves ./badge.styles to the file of that styling.
| Command | Checks |
|---|---|
bun run typecheck | every item in each styling, and the standalone form of every item |
bun run parity | each styles file exports the same API as its .stylesheet sibling |
bun run tsconfig | regenerates the configs, after you edit registry.json |
A file that isn’t listed in registry.json has no alias: importing it fails the typecheck. Never edit the generated tsconfig.*.json files.
Standalone
axiom add <item> --standalone writes an item as one self-contained file, for an app with its own design system: dependencies inlined, the theme resolved to values, no Axiom import left. There is nothing to write per item. It only works because items follow the conventions above: the theme read through useTheme(), tokens files exporting <item>Tokens, other items imported through aliases.
An item that can’t work alone, like portal, declares "standalone": false.