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

Colors

Semantic colors and the light and dark themes.

Draft

The values and key names shown here are conceptual and may change.

ThemeColors is the semantic layer. It names colors by role instead of by hue, so components ask for “the border color” and not “gray 200”.

Each color points to a step of the palette. The roles stay the same in both themes; only the step they point to changes.

Colors are grouped into four roles:

RoleAnswers the question
backgroundWhat is this surface painted with?
contentWhat color is the text or icon on it?
borderWhat outlines or separates it?
feedbackWhat state is it communicating?
Conceptual
export type ThemeColors = {
	background: {
		default: string;
		subtle: string;
		elevated: string;
		inverse: string;
	};
	content: {
		default: string;
		muted: string;
		subtle: string;
		disabled: string;
		inverse: string;
		link: string;
	};
	border: { default: string; subtle: string; strong: string; focus: string };
	feedback: {
		info: string;
		infoSubtle: string;
		success: string;
		successSubtle: string;
		warning: string;
		warningSubtle: string;
		error: string;
		errorSubtle: string;
	};
};

background

Surfaces, from the screen itself to whatever floats above it. The keys describe depth, not components: a card and a bottom sheet both use elevated.

key
usage
light
dark
background.default
Screen background
hsla(0, 0%, 100%, 1)
hsla(0, 0%, 0%, 1)
background.subtle
Grouped lists, secondary areas
gray.50hsla(0, 0%, 96%, 1)
gray.950hsla(240, 4%, 14%, 1)
background.elevated
Cards, sheets, dialogs, menus
hsla(0, 0%, 100%, 1)
gray.900hsla(240, 3%, 20%, 1)
background.inverse
Toasts, snackbars, tooltips
gray.950hsla(240, 4%, 14%, 1)
gray.50hsla(0, 0%, 96%, 1)
  • default is the base of a screen.
  • subtle sets grouped content apart, like iOS grouped lists. Put elevated surfaces on top of it.
  • elevated is for anything above the screen: cards, sheets, dialogs, menus. In dark mode it is lighter than default, since shadows are barely visible on black.
  • inverse flips the scheme for short, high-contrast messages. Pair it with content.inverse.

content

Text and icons. The keys go from most to least emphasis, so each piece of text picks a level of importance rather than a gray.

key
usage
light
dark
content.default
Titles and body text, icons
gray.950hsla(240, 4%, 14%, 1)
gray.50hsla(0, 0%, 96%, 1)
content.muted
Secondary text, descriptions
gray.600hsla(240, 2%, 48%, 1)
gray.400hsla(240, 2%, 65%, 1)
content.subtle
Placeholders, captions
gray.400hsla(240, 2%, 65%, 1)
gray.600hsla(240, 2%, 48%, 1)
content.disabled
Disabled labels and icons
gray.300hsla(240, 3%, 73%, 1)
gray.700hsla(240, 2%, 39%, 1)
content.inverse
Text on inverse backgrounds
hsla(0, 0%, 100%, 1)
gray.950hsla(240, 4%, 14%, 1)
content.link
Links and text actions
blue.500hsla(211, 100%, 50%, 1)
blue.400hsla(215, 98%, 65%, 1)
  • default is for titles, body text and icons.
  • muted is for supporting text: descriptions, secondary lines of an item, timestamps.
  • subtle is for placeholders and captions. Keep it off text the user has to read.
  • disabled is only for disabled controls.
  • inverse goes on background.inverse and on filled buttons.
  • link marks tappable text.

Contrast

default and muted are meant for readable text on default, subtle and elevated backgrounds. Check contrast again when you change these steps.

border

Outlines and separators. Most borders use default; the other keys change the weight or signal an interaction.

key
usage
light
dark
border.default
Inputs, cards, separators
gray.200hsla(240, 2%, 82%, 1)
gray.800hsla(240, 2%, 30%, 1)
border.subtle
Dividers inside a surface
gray.100hsla(240, 2%, 91%, 1)
gray.900hsla(240, 3%, 20%, 1)
border.strong
Unchecked checkboxes and radios
gray.400hsla(240, 2%, 65%, 1)
gray.600hsla(240, 2%, 48%, 1)
border.focus
Focused input, selected item
blue.500hsla(211, 100%, 50%, 1)
blue.400hsla(215, 98%, 65%, 1)
  • default outlines inputs and cards and draws separators between list items.
  • subtle divides content inside a surface that already has an outline.
  • strong is for controls that need to stay visible when empty: an unchecked checkbox or radio.
  • focus shows the focused input or the selected item.

feedback

States the user must notice: information, success, warning and error. Each state has two keys:

  • the base key (error) for icons, text, fills and destructive actions;
  • the Subtle key (errorSubtle) for the tinted background behind that message.
key
usage
light
dark
feedback.info
Informative icon, text or fill
blue.500hsla(211, 100%, 50%, 1)
blue.400hsla(215, 98%, 65%, 1)
feedback.infoSubtle
Informative alert background
blue.50hsla(218, 100%, 96%, 1)
blue.950hsla(215, 100%, 15%, 1)
feedback.success
Confirmation icon, text or fill
green.500hsla(135, 59%, 49%, 1)
green.400hsla(130, 56%, 62%, 1)
feedback.successSubtle
Success alert background
green.50hsla(124, 58%, 95%, 1)
green.950hsla(135, 100%, 9%, 1)
feedback.warning
Warning icon, text or fill
orange.500hsla(35, 100%, 50%, 1)
orange.400hsla(29, 99%, 68%, 1)
feedback.warningSubtle
Warning alert background
orange.50hsla(25, 100%, 96%, 1)
orange.950hsla(31, 100%, 12%, 1)
feedback.error
Error icon, text, destructive action
red.500hsla(3, 100%, 59%, 1)
red.400hsla(6, 99%, 69%, 1)
feedback.errorSubtle
Error alert background
red.50hsla(7, 100%, 97%, 1)
red.950hsla(359, 100%, 14%, 1)

Put base text on its own Subtle background, as in an Alert. Don’t rely on color alone: pair a feedback color with an icon or a label.

light and dark

Axiom ships two complete themes, light and dark. Both implement the same ThemeColors contract with different steps:

Conceptual
export const lightColors = {
	background: {
		default: "hsla(0, 0%, 100%, 1)",
		subtle: palette.gray[50],
		elevated: "hsla(0, 0%, 100%, 1)",
		inverse: palette.gray[950],
	},
	content: { default: palette.gray[950], muted: palette.gray[600] /* … */ },
	border: { default: palette.gray[200] /* … */ },
	feedback: { error: palette.red[500], errorSubtle: palette.red[50] /* … */ },
} satisfies ThemeColors;

export const darkColors = {
	background: {
		default: "hsla(0, 0%, 0%, 1)",
		subtle: palette.gray[950],
		elevated: palette.gray[900],
		inverse: palette.gray[50],
	},
	content: { default: palette.gray[50], muted: palette.gray[400] /* … */ },
	border: { default: palette.gray[800] /* … */ },
	feedback: { error: palette.red[400], errorSubtle: palette.red[950] /* … */ },
} satisfies ThemeColors;

In dark mode, feedback and link colors use a lighter step (400 instead of 500) so they stay readable on dark surfaces.

The names match the conventions of each styling tool:

VariantHow the names are used
stylesheetlight / dark objects picked by a useTheme() hook based on useColorScheme
unistylesRegistered as adaptive themes
tailwindMapped to the dark: prefix

The stylesheet variant follows the system color scheme without a global provider. Add a provider only if you want to force a theme manually.

Customizing

Edit the copied theme files. Because components read component tokens derived from these colors, a change here reaches every component.

To change the brand color, point content.link, border.focus and feedback.info to another hue.