Adding a component
From the issue to the pull request: every file a new component needs.
This guide adds a primitive called tag to the atoms. Compositions, blocks and hooks follow the same steps, minus the ones marked as primitives only.
Open an issue first
Describe the component, its API and why an existing one can’t do the job. A pull request can be correct and still not fit Axiom’s direction: agreeing first avoids that.
Check it’s needed
Before writing a component, ask whether it can be built by composing existing ones. Adding a prop to an existing component comes second, and a new component last. Read the design principles.
Create the folder
packages/registry/atoms/tag
A component no styling changes, like a composition built only from primitives, has no styles file at all.
Write the component
tag.tsx holds the props, the state, the accessibility and the structure. Start from a component of the same shape and keep its conventions.
For a primitive, the props are the root element’s props plus the component’s own, and the rest is spread on the root before the props the component controls:
import type { ComponentPropsWithRef } from "react";
import { View } from "react-native";
import { Text } from "@/components/ui/text";
import { useTagStyles } from "./tag.styles";
export type TagProps = Omit<ComponentPropsWithRef<typeof View>, "children"> & {
label: string;
selected?: boolean;
};
export function Tag({ label, selected = false, ...props }: TagProps) {
const styles = useTagStyles();
return (
<View
{...props}
accessibilityRole="text"
accessibilityState={{ selected }}
{...styles.root(selected, props)}
>
<Text {...styles.label(selected)}>{label}</Text>
</View>
);
}- Controlled and uncontrolled state go through
useControllableState. An effect never copies a prop into state. - When the caller can pass a handler you also need (
onLayout, aref), call both:composeRefsfrom@/components/core/slotmerges refs. - Keep
useCallbackanduseMemowhere they matter. Axiom code must run well without the React Compiler. - Compound components expose their parts as statics:
Card.Header,Tab.List. Each part renders its children where they’re written.
Write the four styles files
Write tag.styles.stylesheet.ts first, then the other three with the same exported names. Keep to the styles file contract: one useTagStyles function, a returned object, one entry per element, no block bodies.
Add the tokens
If the component has colors, tag-tokens.ts exports tagTokens(colors, tokens), mapping theme roles to the component’s roles. Never hardcode a color or a size: read tokens.
Don’t register it in foundations/theme/components/index.ts: that file is a template the CLI fills in the user’s project.
Declare it in registry.json
{
"name": "tag",
"type": "atoms",
"tokens": "atoms/tag/tag-tokens.ts",
"cssTokens": {
"nativewind": "atoms/tag/tag-tokens.nativewind.css",
"uniwind": "atoms/tag/tag-tokens.uniwind.css"
},
"files": ["atoms/tag/tag-tokens.ts", "atoms/tag/tag.tsx"],
"dependencies": [],
"internalDependencies": ["text", "theme"],
"variants": {
"stylesheet": { "files": ["atoms/tag/tag.styles.stylesheet.ts"] },
"unistyles": { "files": ["atoms/tag/tag.styles.unistyles.ts"] },
"nativewind": { "files": ["atoms/tag/tag.styles.nativewind.ts"] },
"uniwind": { "files": ["atoms/tag/tag.styles.uniwind.ts"] }
}
}dependencieslists npm packages,internalDependenciesother Axiom items. List only what the files import directly: the CLI resolves the rest.- An icon the component renders itself goes in
requiredIcons. - A file the user fills once copied is declared
{ "path": "…", "createOnly": true }.
Generate and check
From packages/registry:
bun run theme-css # writes tag-tokens.nativewind.css and tag-tokens.uniwind.css
bun run typecheck # every styling, and the standalone form
bun run parity # the four styles files expose the same APICommit the generated CSS files.
Add a demo screen
Each demo has one screen per component. In every apps/demo-*:
- add
tagto theitemsofaxiom.json, then runbun run axiom add tag; - add
src/app/tag.tsx, the screen, built with theScreenandSectionhelpers ofsrc/demo; - add an entry to
SCREENSinsrc/demo/screens.ts.
The four demos show the same screen: the same rendering is expected in each. Try it on a phone, in light and dark, on iOS and Android when you can, with a screen reader for a minute. Then run bunx tsc --noEmit and bunx expo-doctor in each demo.
Test it
Add packages/registry/tests/components/tag/tag.test.tsx: render the tag, query it by its role and label, and check what it reports. One test per behavior the API promises. Next to it, tag-scenarios.md lists those scenarios in plain words, and what is left to a device. See Testing.
The demos run the same tests on their own copy, so run them in each styling:
bun demo:stylesheet test tag
bun demo:uniwind test tagWrite its page
Every component needs a page in apps/web/content/docs. See Writing docs.
Before you open the pull request
- API: small, predictable props, consistent with existing components.
- Composition: can it be combined with other pieces instead of growing props?
- Theming: every color and size comes from tokens.
- Accessibility: role, label, states, touch targets sized for a finger.
- Touch states: pressed, disabled and focused are handled.
- Platforms: checked on iOS and Android.
- Behaviors and gestures: interruptible, safe on fast repeated input and on unmount mid-animation, with an alternative to gesture-only actions.