Colors & Theme

Canvas uses a semantic token system. Every color is a named token (primary, muted-foreground, border, …) that ships as a plain value per scheme. Components read the active set through useTheme() and build their React Native styles from it, so the same code themes correctly on iOS, Android, and the web, with no per-platform fork. You never touch the values directly: you pick a component's look with semantic boolean props.

Brand

Two brand colors with different jobs. primary is the selection color: checked, selected and current states, links and focus. action is the call to action: primary buttons, meters and count badges. Each has a partner for the label on its fill, and primary-text colors links, text actions and focused Android field labels on neutral surfaces.
Brand text uses primary-text.

Neutrals

Dark Factory's lavender-tinted surfaces, hairlines and indigo-gray inks that everything else sits on.

Semantic

The meaning-bearing tones. Fill tokens have paired foregrounds for their labels; destructive-text supplies readable error and destructive text on neutral surfaces.
Destructive text uses destructive-text.

Washes and overlays

The translucent roles: the soft washes behind a toned badge, an alert band or a tonal selection, the field fill, the scrim that dims the page behind a modal, the shadow tint, and the inverse surface of a tooltip or a snackbar.

Status surfaces

The tinted surfaces behind alerts and status badges. Each tone is one hue family sampled at four steps: a 50 fill, a 200 border, a 500 dot, and a 700 label.
Active
Pending
Failed
Info
Inactive

Data-viz series

Eight colors for charts, assigned in a fixed order and never re-ranked. Distinct enough at 1-2px marks, with no two adjacent hues vibrating.

Glass

Glass is a theming-level surface mode, not a per-component prop: pass glass on the ThemeProvider and every surface takes the material together, layered, or solid to force the flat look; neither means the platform default.

Solid

Workspace
Home
Library
Settings
Project settings
Project name

Glass

Workspace
Home
Library
Settings
Project settings
Project name
Both samples use the same Navbar, Card, Input and Dropdown components. The provider selects their material. Open a menu to compare its dense overlay with the content panel beneath it.
Functional navigation and overlays, content panels, controls and dense menus each use their own tint. Inner fills become ink tints so selected rows and code pills remain part of the surface.
Supported iOS devices use native Liquid Glass for eligible functional surfaces; web surfaces use Dark Factory frost with a 24px blur and inset hairline. Native frost uses supported blur integrations. Missing or unsafe material support restores the complete opaque appearance.
Reduce Transparency and Increase Contrast also select opaque surfaces. The text, controls and layout stay usable in every mode.

Rebranding the accent

The default accent is Dark Factory's violet. These palette steps are starting points for a custom brand. Check your filled controls and text against their actual backgrounds in both schemes.
CSS hand-off rebrands set --primary, --primary-text and --action (with --action-foreground). Setting --primary-text: var(--primary) and --action: var(--primary) at the override scope gives the single-color behavior. CSS does not apply the ThemeProvider override cascade.
The same override rule applies to destructive and destructive-text. ThemeProvider uses a destructive-only override for semantic error text too; supply destructive-text separately for a readable text shade. CSS consumers set --destructive-text explicitly, or use --destructive-text: var(--destructive) for the previous behavior. Menus with a documented fixed red palette retain their independent colors. Check custom colors against their actual resting and pressed surfaces.

Full token reference

Every color token the kit ships, in both schemes and both notations. The hex is what a React Native call site reads; the oklch is what the web token layer publishes.
Token
Light
Light oklch
Dark
Dark oklch
--accent
#f4f3fe
oklch(0.968 0.015 290)
#323254
oklch(0.334 0.059 283)
--accent-foreground
#3b3c5c
oklch(0.370 0.055 282)
#ebebf7
oklch(0.944 0.016 286)
--action
#21804b
oklch(0.532 0.120 154)
#5fd18a
oklch(0.776 0.146 154)
--action-foreground
#ffffff
oklch(1.000 0.000 0)
#0d2a17
oklch(0.257 0.050 153)
--background
#f5f2fe
oklch(0.967 0.016 298)
#272544
oklch(0.283 0.056 285)
--border
#eae8f3
oklch(0.936 0.015 294)
#3b3d54
oklch(0.368 0.040 280)
--card
#ffffff
oklch(1.000 0.000 0)
#252741
oklch(0.284 0.048 280)
--card-foreground
#3b3c5c
oklch(0.370 0.055 282)
#ebebf7
oklch(0.944 0.016 286)
--chart-1
#7b6cf0
oklch(0.613 0.191 284)
#7b6cf0
oklch(0.613 0.191 284)
--chart-2
#03919d
oklch(0.599 0.102 205)
#03919d
oklch(0.599 0.102 205)
--chart-3
#d36225
oklch(0.626 0.159 45)
#d36225
oklch(0.626 0.159 45)
--chart-4
#d25798
oklch(0.633 0.170 350)
#d25798
oklch(0.633 0.170 350)
--chart-5
#4382e3
oklch(0.614 0.161 259)
#4382e3
oklch(0.614 0.161 259)
--chart-6
#009574
oklch(0.596 0.118 170)
#009574
oklch(0.596 0.118 170)
--chart-7
#b560cf
oklch(0.632 0.180 318)
#b560cf
oklch(0.632 0.180 318)
--chart-8
#a37e05
oklch(0.612 0.124 88)
#a37e05
oklch(0.612 0.124 88)
--destructive
#b53a44
oklch(0.530 0.158 20)
#ff8b93
oklch(0.765 0.140 17)
--destructive-foreground
#ffffff
oklch(1.000 0.000 0)
#38181b
oklch(0.257 0.051 15)
--destructive-soft
rgba(181, 58, 68, 0.12)
oklch(0.530 0.158 20 / 0.12)
rgba(255, 139, 147, 0.16)
oklch(0.765 0.140 17 / 0.16)
--destructive-text
#9a1f2f
oklch(0.451 0.158 20)
#ffadb0
oklch(0.827 0.097 17)
--field-border
#d6d3e7
oklch(0.875 0.027 293)
#44455c
oklch(0.399 0.039 283)
--field-fill
rgba(255, 255, 255, 0.7)
oklch(1.000 0.000 0 / 0.7)
rgba(255, 255, 255, 0.06)
oklch(1.000 0.000 0 / 0.06)
--foreground
#3b3c5c
oklch(0.370 0.055 282)
#ebebf7
oklch(0.944 0.016 286)
--hover
rgba(123, 108, 240, 0.08)
oklch(0.613 0.191 284 / 0.08)
rgba(164, 150, 255, 0.1)
oklch(0.729 0.150 288 / 0.1)
--input
#898ba0
oklch(0.642 0.031 281)
#7c7e93
oklch(0.599 0.032 281)
--inverse
#26264a
oklch(0.288 0.065 282)
#26264a
oklch(0.288 0.065 282)
--inverse-foreground
#ffffff
oklch(1.000 0.000 0)
#ffffff
oklch(1.000 0.000 0)
--inverse-primary
#cecfff
oklch(0.870 0.066 284)
#b1a7ff
oklch(0.771 0.124 288)
--muted
#f5f2fc
oklch(0.966 0.014 300)
#2d2f4c
oklch(0.317 0.052 280)
--muted-foreground
#606183
oklch(0.505 0.054 283)
#a6a8c9
oklch(0.741 0.047 282)
--popover
#ffffff
oklch(1.000 0.000 0)
#252741
oklch(0.284 0.048 280)
--popover-foreground
#3b3c5c
oklch(0.370 0.055 282)
#ebebf7
oklch(0.944 0.016 286)
--primary
#7262e5
oklch(0.581 0.191 284)
#a496ff
oklch(0.729 0.150 288)
--primary-foreground
#ffffff
oklch(1.000 0.000 0)
#221f3a
oklch(0.257 0.050 287)
--primary-soft
rgba(123, 108, 240, 0.14)
oklch(0.613 0.191 284 / 0.14)
rgba(164, 150, 255, 0.16)
oklch(0.729 0.150 288 / 0.16)
--primary-text
#5d48c9
oklch(0.500 0.191 285)
#b8b0ff
oklch(0.793 0.111 288)
--ring
#7b6cf0
oklch(0.613 0.191 284)
#a496ff
oklch(0.729 0.150 288)
--scrim
rgba(40, 30, 90, 0.28)
oklch(0.286 0.103 286 / 0.28)
rgba(5, 5, 20, 0.55)
oklch(0.127 0.035 280 / 0.55)
--secondary
#f5f2fc
oklch(0.966 0.014 300)
#2d2f4c
oklch(0.317 0.052 280)
--secondary-foreground
#3b3c5c
oklch(0.370 0.055 282)
#ebebf7
oklch(0.944 0.016 286)
--shade
rgba(121, 100, 214, 0.22)
oklch(0.580 0.168 288 / 0.22)
rgba(0, 0, 0, 0.5)
oklch(0.000 0.000 0 / 0.5)
--success
#0b7440
oklch(0.492 0.120 154)
#5fd18a
oklch(0.776 0.146 154)
--success-foreground
#ffffff
oklch(1.000 0.000 0)
#0d2a17
oklch(0.257 0.050 153)
--success-soft
rgba(33, 128, 75, 0.12)
oklch(0.532 0.120 154 / 0.12)
rgba(95, 209, 138, 0.16)
oklch(0.776 0.146 154 / 0.16)
--warning
#965801
oklch(0.520 0.117 65)
#ffc46b
oklch(0.856 0.126 76)
--warning-foreground
#ffffff
oklch(1.000 0.000 0)
#311f03
oklch(0.256 0.050 75)
--warning-soft
rgba(240, 160, 40, 0.18)
oklch(0.767 0.154 71 / 0.18)
rgba(255, 180, 80, 0.16)
oklch(0.823 0.143 71 / 0.16)

How theming works

You change a component's look with semantic boolean props. Each one resolves through the active scheme's tokens, so the same markup renders correctly in every theme and accent, with no re-skinning and no per-call overrides.

1 · Tokens (tokens.ts)

tsx
// tokens.ts: plain values for every platform
export const lightColors = {
primary: "#7262e5",
"primary-text": "#5d48c9",
"destructive-text": "#9a1f2f",
background: "#f5f2fe",
// …
};
export const mintColors = {
primary: "#3474d4",
"primary-text": "#1a5bb9",
"destructive-text": "#9a1f2f",
background: "#f0f7fd",
// …
};
export const darkColors = {
primary: "#a496ff",
"primary-text": "#b8b0ff",
"destructive-text": "#ffadb0",
background: "#272544",
// …
};

2 · The theme runtime (useTheme)

tsx
// ThemeProvider supplies the active scheme;
// components read it through useTheme().
const { tokens } = useTheme();
tokens.primary; // Selection: "#7262e5" blush, "#3474d4" mint, "#a496ff" dark
tokens.action ?? tokens.primary; // Call to action, including legacy token maps
tokens["primary-text"] ?? tokens.primary; // Brand text, including legacy token maps
tokens["destructive-text"] ?? tokens.destructive; // Error text, including legacy token maps

3 · One prop, resolved live

tsx
<Button primary>Save</Button>
// You set the look with a prop, never a class. The skin
// builds { backgroundColor: tokens.action }, so one prop
// resolves live per theme:
// light → #21804b
// light mint → #1f8049
// dark → #5fd18a
// teal accent → #0d9488 (a primary-only override
// repaints action too)
The primary prop is the whole styling API; the button reads tokens.action from useTheme(). Switch the scheme or point the accent at a new hue and the ThemeProvider swaps the token set, so every component bound to it re-renders with the new colour.

Do's and don'ts

Keep colour theme-routed. These are the patterns that follow the accent and dark mode for free, beside the ones that quietly break them.
Don’t
tsx
<Button style={{ backgroundColor: "#6366f1" }}>
Save
</Button>
A hard-coded fill painted over the component. It bypasses the theme: an accent change won't reach it, and it looks wrong in dark mode.
Do
tsx
<Button primary>Save</Button>
The same button, styled by its semantic prop. The prop resolves through the active scheme's tokens, so theme and accent changes are free.
Don’t
tsx
<Button variant="primary" size="lg">
String-valued enum props are rejected on components: variant, size, and tone are not part of the API.
Do
tsx
<Button primary large>
Flat boolean props, at most one per axis. The prop name is the value.
Don’t
tsx
backgroundColor: tokens.primary,
color: tokens.foreground
In a skin's style object, foreground is the body-text color, not the partner of a primary fill: this pairing is not contrast-guaranteed.
Do
tsx
backgroundColor: tokens.primary,
color: tokens["primary-foreground"]
Every fill token has a -foreground partner. Pair them and contrast holds in both schemes.
Don’t
tsx
import { darkColors } from "@nannier/canvas"
const bg = darkColors.primary
Frozen to one scheme: this value never updates when the theme flips.
Do
tsx
const { tokens } = useTheme()
const bg = tokens.primary
Reads the active scheme and re-renders when the theme changes.
Don’t
tsx
<Popover glass>
Glass is not a per-component prop, and you never hand-paint a blur onto one component.
Do
tsx
<ThemeProvider glass>
Glass is a surface mode set once on the provider, so every surface that takes the material turns together. Same boolean grammar as every component axis: glass or solid, and omitting both keeps the platform default.