Icon
Themed icon component backed by an icon registry you fill.
Installation
npx axiom add iconpnpm dlx axiom add iconyarn dlx axiom add iconbun x axiom add iconCopies icon.tsx, icon-types.ts and your registry icons.tsx.
The first time, the CLI asks where the icons of your app come from, and saves the answer as icons in axiom.json:
Where do the icons of your app come from?
1. expo-symbols SF Symbols on iOS, Material Symbols on Android. Ready to use.
2. custom An empty registry you fill with your own icons or any icon set.| Source | Registry created | Installs |
|---|---|---|
expo-symbols | Filled with common icons (close, check, chevron-right, search…), ready to use. Offered only in Expo projects. | expo-symbols |
custom | Empty, with examples in comments. You fill it with any set or your own SVGs. | nothing |
Without a terminal, pass the source: npx axiom add icon --icons custom.
Usage
import { Icon } from '@/components/ui/icon';
<Icon name="settings" />
<Icon name="alert" size="lg" color="error" />The registry
Axiom doesn’t pick an icon set for you. Your app builds its own base of icons, and every icon goes through it.
axiom add icon creates icons.tsx, where you map names to components from any set: Lucide, Phosphor, SF Symbols through expo-symbols, or your own SVGs. The CLI creates the file once and never overwrites it: it’s yours.
import { Bell, House, Search, Settings } from "lucide-react-native";
import type { IconRegistry } from "./icon-types";
export const icons = {
home: House,
search: Search,
notifications: Bell,
settings: Settings,
} satisfies IconRegistry;
export type IconName = keyof typeof icons;IconName is inferred from the registry, so name autocompletes and a typo fails to compile. Every Axiom component that shows an icon (Button, IconButton, Chip…) takes an IconName.
Changing source later doesn’t rewrite icons.tsx: replace its entries yourself. Nothing else in your app changes.
Icons that components need
Some components draw an icon themselves: the close button of a sheet header, the check of a checkbox. Their registry entry lists these names, and add tells you which ones your registry lacks:
Add these icons to components/ui/icons.tsx:
close used by bottom-sheetWith expo-symbols, the registry already has them. With custom, add them yourself. Until you do, the component doesn’t compile: a missing icon never shows up as a blank space in the app.
Rules
Axiom holds every project to these rules. The components are built on them.
- One registry. Every icon the app shows is declared in
icons.tsx. Screens and components never import an icon library. - One component. Icons are rendered with
<Icon name="…" />. Axiom components take anIconName, never an element, so an icon can’t bypass the registry. - Names say what the icon means, not what it looks like:
close,settings,delete, notxmark,gear,trash. Changing the glyph then never renames anything. - kebab-case names:
chevron-right,arrow-left. - One contract. Every entry takes
{ size, color, strokeWidth? }. A set with other props gets an adapter inicons.tsx, not in the screens. - Sizes and colors come from the theme. Use
size="sm" | "md" | "lg"and theme color names, so icons follow the scale and dark mode. A raw number or color is an exception. - Import each icon on its own (
import { Bell } from 'lucide-react-native'), so the bundle only holds the icons of the registry. - Decorative by default. Add an
accessibilityLabelonly when the icon is the only signal. An action with only an icon is an IconButton, whose label is required.
Adapters
Sets don’t share prop names. Wrap them once, in the registry:
import { SymbolView } from "expo-symbols";
import type { IconComponent, IconRegistry } from "./icon-types";
function symbol(ios: SFSymbol, android: AndroidSymbol): IconComponent {
return function SymbolIcon({ size, color }) {
return (
<SymbolView name={{ ios, android }} size={size} tintColor={color} />
);
};
}
export const icons = {
close: symbol("xmark", "close"),
settings: symbol("gearshape", "settings"),
} satisfies IconRegistry;Props
| Prop | Type | Default | Description |
|---|---|---|---|
name | IconName | — | Required. Key in your registry. |
size | 'sm' | 'md' | 'lg' | number | 'md' | From the icon size tokens: 16, 20, 24pt. A number sets it directly. |
color | IconColor | string | 'default' | A content color name (default, muted, subtle, disabled, link, inverse) or a feedback color (info, success, warning, error), resolved from the theme so it follows light and dark. A raw color string is accepted but won’t follow the scheme. |
strokeWidth | number | set-dependent | Passed to the icon component for sets that support it. |
accessibilityLabel | string | — | Makes the icon visible to screen readers. Without it the icon is decorative and hidden (accessible={false}). |
style | StyleProp<ViewStyle> | — | Extra styles for the icon. |
Use cases
Leading icon in a list
A decorative icon: no accessibility label, since the row label already says it all.
<Item>
<Item.Leading>
<Icon name="lock" color="muted" />
</Item.Leading>
<Item.Content>
<Item.Title>Privacy</Item.Title>
</Item.Content>
<Item.Trailing>
<Icon name="chevron-right" size="sm" color="subtle" />
</Item.Trailing>
</Item>Icon that carries meaning
When the icon is the only signal (a verified mark, a warning), give it an accessibilityLabel.
<Icon name="circle-check" color="link" accessibilityLabel="Verified account" />Mixing icon sets
The registry can point to different sets, so you keep one API when a set lacks an icon.
import { Mail, Globe } from "lucide-react-native";
import { FaceIdIcon } from "@/assets/icons/face-id";
export const icons = {
mail: Mail,
globe: Globe,
"face-id": FaceIdIcon,
} satisfies IconRegistry;