Themes

Create themes and sub-themes

Themes map neatly to CSS variables: they are objects whose values you want to contextually change at any point in your React tree. Tamagui lets you define, nest, and read them on web and native.

Bare theme names are the first lookup for style values, and useTheme exposes them directly. Themes nest both in the definition and at runtime, and values resolve upward, ultimately all the way back to tokens.

For the recommended theme setup, see the Config v6 docs.

See the ThemeBuilder guide for generating custom theme suites.

To style bento or tamagui components, see Styling tamagui UI components.

For a copy-paste custom palette and recipe tree, see the v6 Colors guide.

You define a theme like this:

const dark = {
background: '#000',
color: '#fff',
// define any key to any string or number value
}

If you use tokens, you can share values from tokens down to themes. Tokens act as fallback values for themes, like global CSS variables vs scoped ones:

const tokens = createTokens({
color: {
black: '#000',
white: '#fff',
},
})
// theme:
const dark = {
background: tokens.color.black,
color: tokens.color.white,
}

Sub-themes

Tamagui supports theme nesting. Define sub-themes using the parentName_subName format, where each segment resolves as a valid theme. Sub-themes can nest across multiple levels:

  • dark_green_subtle
  • light_green_subtle
<Theme name="dark">
<Theme name="green">
<Button theme="subtle">Hello world</Button>
</Theme>
</Theme>

You can also access a specific sub-theme more specifically:

<Theme name="dark">
<Button theme="green_subtle">Hello world</Button>
</Theme>

Inverse themes

In v3, inverse is a real sub-theme generated by the default theme setup. Use it like any other sub-theme:

<Button theme="inverse">Inverted colors</Button>
<Theme name="inverse">
<Card />
</Theme>

The old themeInverse prop and <Theme inverse> path are removed. Because inverse is a named sub-theme, SSR can emit the right CSS variables without client-only light/dark inversion. See the theme creation guide for custom theme generation details.

Forcing a scheme with black and white

inverse is relative: it flips whichever scheme its parent happened to be. When you need a specific scheme instead, use black or white. They resolve to the dark and light themes from any parent, including from inside a palette sub-theme:

// dark, wherever this is mounted
<Theme name="black">
<Card />
</Theme>
// still dark, even though the parent is light and red
<Theme name="light">
<Theme name="red">
<Theme name="black">
<Card />
</Theme>
</Theme>
</Theme>

Reach for them when a subtree cannot know which scheme it is mounted under, such as a menu that always reads as dark over a light page. Levels nest inside them as usual, so <Theme name="black"><Theme name="level2"> steps within the dark scheme rather than falling back to the parent’s.

Keep themes consistent in shape, sharing the same named keys and typed values. Sub-themes can define subsets of parent themes. The useTheme hook and style system resolve missing keys upward through parent themes and back to tokens.

Component themes

V3 does not select themes from styled component display names. The displayName option sets React’s debugging identity and may add an is_Circle class, but it does not make Tamagui search for light_Circle or affect style resolution.

Use a normal theme boundary when a component owns a theme operation:

import { Theme, View, styled } from 'tamagui'
const CircleFrame = styled(View, {
displayName: 'Circle',
backgroundColor: 'background',
})
function Circle(props) {
return (
<Theme name="level2">
<CircleFrame {...props} />
</Theme>
)
}

This composes under light, dark, color, inverse, and existing level themes without a second component-specific lookup system.

Styling Tamagui Components

The tamagui component suite uses standard semantic theme keys across all components:

  • background: component surface color
  • color: text and icon color
  • border-color: border color
  • shadow-color: elevation and shadow color
  • placeholder-color: placeholder text color

Each key supports interactive pseudo-states: background-hover, background-press, and background-focus.

Defining these standard keys lets you re-theme both built-in UI components and your own components consistently across light and dark mode.

A minimal theme might look like this:

const dark = {
// Standard keys for all components
background: '#000',
'background-hover': '#111',
'background-press': '#222',
'background-focus': '#333',
color: '#fff',
'color-hover': '#eee',
'color-press': '#ddd',
'color-focus': '#ccc',
'border-color': '#555',
'border-color-hover': '#666',
'border-color-focus': '#777',
'border-color-press': '#888',
'placeholder-color': '#999',
'outline-color': '#aaa',
// Custom tokens like "brand"
'brand-background': '#000', // You can add your own tokens like "brand"
'brand-color': '#fff', // and use them in your components
}
const light = {
// Standard keys for all components
background: '#fff',
'background-hover': '#f5f5f5',
'background-press': '#e0e0e0',
'background-focus': '#d5d5d5',
color: '#000',
'color-hover': '#111',
'color-press': '#222',
'color-focus': '#333',
'border-color': '#444',
'border-color-hover': '#555',
'border-color-focus': '#666',
'border-color-press': '#777',
'placeholder-color': '#888',
'outline-color': '#999',
// Custom tokens like "brand"
'brand-background': '#000', // You can add your own tokens like "brand"
'brand-color': '#fff', // and use them in your components
}

You can of course do all of this yourself in your own design system with styled:

If you are building a component with more than one sub-component, you can follow this pattern:

import { GetProps, View, Text, createStyledHOC, styled } from 'tamagui' // or '@tamagui/core'
const ButtonFrame = styled(View, {
displayName: 'Button',
backgroundColor: 'background',
})
const ButtonText = styled(Text, {
displayName: 'ButtonText',
color: 'color',
})
type ButtonProps = GetProps<typeof ButtonFrame>
// createStyledHOC wraps a functional component so it can be further styled with styled()
// see /docs/core/styled#createstyledhoc for full documentation
export const Button = createStyledHOC(
ButtonFrame,
({ children, ...props }: ButtonProps, ref) => {
return (
<ButtonFrame ref={ref} {...props}>
<ButtonText>{children}</ButtonText>
</ButtonFrame>
)
}
)

The frame and text share the active theme. Wrap the composed component in an explicit named or relative level theme when it needs a different semantic surface.

Full Example

Let’s start with an example of inline styling with a subset of the configuration:

import { TamaguiProvider, createTokens, createTamagui, Theme } from 'tamagui';
import { html } from "@tamagui/tailwind";
const tokens = createTokens({
color: {
darkRed: '#550000',
lightRed: '#ff0000'
}
// ... see configuration docs for required tokens
});
const config = createTamagui({
tokens,
themes: {
dark: {
red: tokens.color.darkRed
},
light: {
red: tokens.color.lightRed
}
}
});
export const App = () => <TamaguiProvider config={config} defaultTheme="light">
<html.div className="bg-[red]" />
<Theme name="dark">
<html.div className="bg-[red]" />
</Theme>
</TamaguiProvider>;

In this example we’ve set up darkRed and lightRed variables and a dark and light theme that use those variables. Tamagui will handle defining:

:root {
--colors-dark-red: #550000;
--colors-light-red: #ff0000;
}
.tui_dark {
--red: var(--colors-dark-red);
}
.tui_light {
--red: var(--colors-light-red);
}

Which will automatically apply at runtime, or can be gathered for use in SSR with config.getCSS().

Finally, the compiler on web will extract your views roughly as so:

export const App = () => (
<Provider defaultTheme="light">
<div className="baCo-2nesi3" />
<Theme name="dark">
<div className="baCo-2nesi3" />
</Theme>
</Provider>
)
// CSS output:
// .color-2nesi3 { background-color: var(--red); }

Ensuring valid types

This structure keeps everything typed. Keep themes in a separate themes.ts file, and structure it like this:

import { tokens } from './tokens'
const light = {
background: '#fff',
'background-hover': tokens.color['gray-100'],
'background-press': tokens.color['gray-200'],
'background-focus': tokens.color['gray-300'],
'border-color': tokens.color['gray-200'],
'border-color-hover': tokens.color['gray-400'],
color: tokens.color['gray-950'],
'color-hover': tokens.color['gray-800'],
'color-press': tokens.color['gray-700'],
'color-focus': tokens.color['gray-400'],
'shadow-color': 'rgba(0, 0, 0, 0.12)',
}
// note: we set up a single consistent base type to validate the rest:
type BaseTheme = typeof light
// the rest of the themes use BaseTheme
const dark: BaseTheme = {
background: '#000',
'background-hover': tokens.color['gray-900'],
'background-press': tokens.color['gray-800'],
'background-focus': tokens.color['gray-700'],
'border-color': tokens.color['gray-800'],
'border-color-hover': tokens.color['gray-700'],
color: '#ddd',
'color-hover': tokens.color['gray-100'],
'color-press': tokens.color['gray-200'],
'color-focus': tokens.color['gray-400'],
'shadow-color': 'rgba(0, 0, 0, 0.25)',
}
const dark_translucent: BaseTheme = {
...dark,
background: 'rgba(0,0,0,0.7)',
'background-hover': 'rgba(0,0,0,0.5)',
'background-press': 'rgba(0,0,0,0.25)',
'background-focus': 'rgba(0,0,0,0.1)',
}
const light_translucent: BaseTheme = {
...light,
background: 'rgba(255,255,255,0.85)',
'background-hover': 'rgba(250,250,250,0.85)',
'background-press': 'rgba(240,240,240,0.85)',
'background-focus': 'rgba(240,240,240,0.7)',
}
export const allThemes = {
dark,
light,
dark_translucent,
light_translucent,
} satisfies { [key: string]: BaseTheme }

Dynamic Themes

Sometimes you want to defer loading themes, or change existing theme values at runtime. Tamagui exports three helpers for this in the package @tamagui/theme which exports addTheme, updateTheme, and replaceTheme.

addTheme

updateTheme

replaceTheme

Notes

  • Dynamic themes only work on the client side and will be ignored on the server side.
  • The difference between updateTheme and replaceTheme is that replaceTheme will replace the entire theme, while updateTheme will only update the values that are passed in.

Advanced Optimization

To omit theme objects from a server-rendered web app’s client bundle, follow Tree shaking themes. That section covers the CSS and bundler setup required before hydration.