Creating Themes with Tamagui

Generate typed themes from color tokens and a recipe tree

Tamagui v3 creates theme suites with one function: createThemes(tokens, tree, { getTheme }). The tree describes names and inheritance. Your getTheme function turns each recipe into a plain theme record.

The default recipe helpers live in @tamagui/themes/builder. The generated default themes live in @tamagui/themes, so applications using @tamagui/config/v6 do not run the generator at startup.

Start with the default themes

tamagui.config.ts

import { defaultConfig } from '@tamagui/config/v6'
import { createTamagui } from 'tamagui'
export const config = createTamagui(defaultConfig)

The default suite contains light and dark roots, relative levels, inverse, accent, brand, and the red, yellow, and green semantic colors.

Build a theme tree

themes.ts

import {
createThemes,
getTheme,
levels,
tokens,
} from '@tamagui/themes/builder'
export const tree = {
light: { scheme: 'light', palette: 'gray' },
dark: { scheme: 'dark', palette: 'gray' },
children: {
...levels(),
accent: {
palette: 'brand',
treatment: 'tint',
children: levels(),
},
inverse: ({ parent }) => ({
scheme: parent.scheme === 'light' ? 'dark' : 'light',
children: levels(),
}),
red: {
palette: 'red',
treatment: 'tint',
children: levels(2),
},
},
} as const
export const themes = createThemes(tokens, tree, { getTheme })
export type Themes = typeof themes

The roots must be named light and dark. A child inherits its parent’s recipe fields by shallow merge. children and values are structural keys; every other field is application-defined data passed to getTheme.

Generated names flatten the path:

light
├── light_level2
│ └── light_level2_level2
├── light_accent
│ └── light_accent_level2
├── light_inverse
└── light_red
└── light_red_level2

The same children attach below dark. Definition-local children attach only below that definition.

Resolve recipes with getTheme

The default resolver combines two small helpers:

import {
fromShades,
ramp,
scales,
type GetThemeContext,
type DefaultRecipe,
} from '@tamagui/themes/builder'
export function getTheme({ recipe }: GetThemeContext<typeof tokens, DefaultRecipe>) {
const scale =
scales[recipe.treatment ?? 'normal'][recipe.scheme][recipe.level ?? 1]
return {
...ramp(recipe.palette, recipe.scheme, scale),
...fromShades(recipe.palette, scale),
}
}

ramp() emits the adaptive color-1 through color-11 keys. color-1 is nearest the background and color-11 is nearest the foreground in every theme. Pass the scale so a bold light theme, whose background is deeper than its type, runs its ramp deep to pale; without it the direction follows the scheme alone. fromShades() maps numeric shades in a semantic scale to the active palette and passes exact token names or color literals through.

These numbers describe steps relative to the theme’s own scheme. They are not Tailwind’s absolute 50 through 950 scale. Only the hyphenated spelling changed.

All generated values resolve against tokens.color immediately. A misspelled token or unsupported value fails generation and reports the nearest token name.

Relative levels

levels() creates self-nesting level2, level3, and level4 children. Their names are relative operations:

<Theme name="level2">
<Panel>
<Button>{/* the Button's own level2 boundary resolves to level 3 */}</Button>
</Panel>
</Theme>

At the configured maximum, deeper levels saturate. The generated names remain valid aliases, but identical resolved maps share one parsed theme and one CSS declaration block.

Pass a lower maximum for color themes when only one raised step is useful:

red: {
palette: 'red',
treatment: 'tint',
children: levels(2),
}

This creates a real light_red_level2 map. Deeper relative paths resolve to the same map instead of falling back to a neutral level.

One-theme values

Recipe fields inherit. values apply only to the theme defined at that node:

const tree = {
light: {
scheme: 'light',
palette: 'gray',
values: { 'border-color': 'transparent' },
},
dark: { scheme: 'dark', palette: 'gray' },
} as const

Values are applied after getTheme, so they are useful for a precise override. Put data in the recipe when descendants must retain it.

You may omit getTheme entirely when every node supplies its theme through values.

Function children

A definition function receives the resolved parent recipe and may return a definition or null:

inverse: ({ parent }) => ({
scheme: parent.scheme === 'light' ? 'dark' : 'light',
})

Returning null skips that path. The default inverse theme uses this ordinary mechanism. Its resolved maps deduplicate with the opposite root, so inverse adds names without duplicating declarations.

Customize scales and treatments

The default scales object has normal, bold, and tint treatments in both schemes at levels 1 through 4. Each level is plain data. raise(scale, steps) shifts background and border shades along the white, 50 through 950, black ladder and clamps at its ends.

import { raise, scales } from '@tamagui/themes/builder'
const subtleLight = {
...raise(scales.tint.light[1], 1),
'border-color': 200,
}

Add a key to your own scales object and reference it with a recipe field. Then use that field in your resolver. createThemes does not reserve scheme, palette, level, or treatment; these are conventions implemented by the default resolver.

No component themes, templates, or masks

V3 does not inspect styled component display names or look for uppercase theme name segments. A Button gets a raised theme because its skin creates a level2 boundary, the same operation available to application code.

The v2 builder options componentThemes, templates, masks, childrenThemes, and grandChildrenThemes have no v3 equivalent. Express the structure directly in tree, semantic values in scale objects, and exact single-theme changes in values.

Pre-generate a custom suite

Runtime generation is small, but a shared design system can emit the flat module once:

yarn dlx tamagui generate-themes ./src/themes.input.ts ./src/themes.generated.ts

The input module exports themes or exports it as the default. Import the generated module from your Tamagui config so the authoring helpers stay out of the client bundle.

See v6 Colors for a complete custom-palette example and Surfaces and levels for using relative levels in components.