Icon
Pre-alphaThe registry and the CLI are not published yet.Roadmap

Icon

Themed icon component backed by an icon registry you fill.

Icons
Your registry
Sizes · sm 16 · md 20 · lg 24
Content colors

Installation

npx axiom add icon
pnpm dlx axiom add icon
yarn dlx axiom add icon
bun x axiom add icon

Copies 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.
SourceRegistry createdInstalls
expo-symbolsFilled with common icons (close, check, chevron-right, search…), ready to use. Offered only in Expo projects.expo-symbols
customEmpty, 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.

components/ui/icons.tsx
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-sheet

With 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.

  1. One registry. Every icon the app shows is declared in icons.tsx. Screens and components never import an icon library.
  2. One component. Icons are rendered with <Icon name="…" />. Axiom components take an IconName, never an element, so an icon can’t bypass the registry.
  3. Names say what the icon means, not what it looks like: close, settings, delete, not xmark, gear, trash. Changing the glyph then never renames anything.
  4. kebab-case names: chevron-right, arrow-left.
  5. One contract. Every entry takes { size, color, strokeWidth? }. A set with other props gets an adapter in icons.tsx, not in the screens.
  6. 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.
  7. Import each icon on its own (import { Bell } from 'lucide-react-native'), so the bundle only holds the icons of the registry.
  8. Decorative by default. Add an accessibilityLabel only 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:

components/ui/icons.tsx
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

PropTypeDefaultDescription
nameIconNameRequired. Key in your registry.
size'sm' | 'md' | 'lg' | number'md'From the icon size tokens: 16, 20, 24pt. A number sets it directly.
colorIconColor | 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.
strokeWidthnumberset-dependentPassed to the icon component for sets that support it.
accessibilityLabelstringMakes the icon visible to screen readers. Without it the icon is decorative and hidden (accessible={false}).
styleStyleProp<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.

Profile
Privacy
Storage
<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.

AL

Ada Lovelace
@ada · 18.2k followers

Last seen 3 days ago

<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.

Sign in with

Continue with email

Continue with Face ID

Continue with SSO

components/ui/icons.tsx
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;