Themes

How to create and use themes in Tamagui.

Themes in Tamagui are very flexible, and work a bit differently than you may expect. If you just want to dive in and copy a complete theme config, head down to Quick Start.

You make a theme with createTheme, like so:

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

You can pass tokens as values to any theme property, doing share colors in a type-friendly way and saves on bundle-size (using CSS Variables):

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

Think of tokens as your base variables which can be shared downwards to themes. Themes meanwhile are meant to be concise, we typically define a handful of "common" properties that any of your styled() components can use.

Tamagui make some assumptions about the themes dark and light. If you use them, changing the between the two will avoid re-rendering the whole tree.

Subset themes

One of the real powers of Tamagui is theme nesting (we'll explain more on the importance and usage below). If you define a theme with the name in the form [parentName]_[subName], Tamagui then accepts <Theme name="[subName]" /> as though it's valid.

You can do this as many times as you'd like. For example you can have the following themes:

  • dark_green_subtle
  • light_green_subtle

And you're able to then access them (fully typed):

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

You can also do:

<Theme name="dark_green">
<Button theme="subtle">Hello world</Button>
</Theme>

Component subset themes

Finally, each component in Tamagui can be set up to accept a component specific theme by passing the name property to the second argument of styled(). For example, for the Button component, we define the frame of it like so:

const ButtonFrame = styled(SizableFrame, {
name: 'Button',
tag: 'button',
})

We've decided to enforce the first letter being capitalized. This makes it easier to users to distinguish component themes, and allows Tamagui internally to avoid extra work and nesting.

The name attribute will be removed from the defaultProps and used internally by Tamagui to check for the sub-theme that matches. This means any theme named with an ending of _Button will apply. So you can add the following themes to customize buttons at any level:

  • dark_Button
  • dark_green_Button
  • dark_green_subtle_Button

Tamagui theme keys

While @tamagui/core isn't prescriptive at all, tamagui is. This is because standardizing on specific shared theme keys unlocks huge upside. In tamagui, all components will look for the following keys:

  • background
  • color
  • borderColor
  • shadowColor

Plus all the "pseudo" variants for each, so for example, backgroundHover, backgroundPress, and backgroundFocus.

In the future, tamagui will expand use more, like borderWidth.

Putting this all together, you'll see how you can get complete control over Tamagui component themes:

// this will customize all `Button` styles
const dark_Button = createTheme({
background: '#333',
color: '#999',
})
// this will customize all `Button` styles inside the `dark_green` theme:
const dark_green_Button = createTheme({
background: 'darkgreen',
color: 'lightgreen',
})

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

import { Stack, styled } from '@tamagui/core'
const Button = styled(Stack, {
// note: this name will match it to any theme ending with `_Button`:
name: 'Button',
})

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

import { Text, Stack, styled, GetProps } from '@tamagui/core'
const ButtonFrame = styled(Stack, {
name: 'Button'
backgroundColor: '$background'
})
const ButtonText = styled(Text, {
name: 'ButtonText'
color: '$color'
})
type ButtonProps = GetProps<typeof ButtonFrame>
// note: extractable will tell the tamagui compiler to optimize usages of this:
export const Button = ButtonFrame.extractable(({ children, ...props }: ButtonProps) => {
return (
<ButtonFrame {...props}>
<ButtonText>
{children}
</ButtonText>
</ButtonFrame>
)
})

And now you can add two themes: dark_Button and dark_ButtonText, and override their default styles.

We'd like to better organize some helpers to allow for composable components as well, but just note you can use these same patterns to build any type of API you'd like - you could export ButtonText separately, for example, for a more verbose but flexible API.

Quick start

To get started quickly, you can use the themes we've developed alongside this site and with other apps, @tamagui/theme-base. It's even easier to see how it all comes together by using create-tamagui-app to bootstrap.

To install, just add import it and add it to your tamagui.config.ts:

import { createInterFont } from '@tamagui/font-inter'
import { color, radius, size, space, themes, zIndex } from '@tamagui/theme-base'
import { createTamagui, createTokens } from 'tamagui'
const inter = createInterFont()
const tokens = createTokens({
font: {
heading: inter,
body: inter,
},
size,
space,
zIndex,
color,
radius,
})
const config = createTamagui({
themes,
tokens,
})
export type Conf = typeof config
declare module 'tamagui' {
interface TamaguiCustomConfig extends Conf {}
}
export default config

Full Example

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

import { createTokens, createTamagui, YStack, Theme } from 'tamagui'
const tokens = createTokens({
color: {
darkRed: '#550000'
lightRed: '#ff0000'
}
})
const { Provider } = createTamagui({
tokens,
themes: {
dark: {
red: tokens.color.darkRed,
},
light: {
red: tokens.color.lightRed,
}
}
})
export const App = () => (
<Provider defaultTheme="light">
<YStack backgroundColor="$red" />
<Theme name="dark">
<YStack backgroundColor="$red" />
</Theme>
</Provider>
)

In this example we've set up darkRed and lightRed variables and a 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 using Tamagui.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

Here's what we've landed on which helps ensure everything is typed properly. Use createTheme, which is a simple helper for creating a theme and having all the values turned into variables. Keep themes in a separate themes.ts file, and structure it like this:

import { createTheme } from 'tamagui'
import { tokens } from './tokens'
const light = createTheme({
background: '#fff',
backgroundHover: tokens.color.gray3,
backgroundPress: tokens.color.gray4,
backgroundFocus: tokens.color.gray5,
borderColor: tokens.color.gray4,
borderColorHover: tokens.color.gray6,
color: tokens.color.gray12,
colorHover: tokens.color.gray11,
colorPress: tokens.color.gray10,
colorFocus: tokens.color.gray6,
shadowColor: tokens.color.grayA5,
shadowColorHover: tokens.color.grayA6,
})
// 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',
backgroundHover: tokens.color.gray2Dark,
backgroundPress: tokens.color.gray3Dark,
backgroundFocus: tokens.color.gray4Dark,
borderColor: tokens.color.gray3Dark,
borderColorHover: tokens.color.gray4Dark,
color: '#ddd',
colorHover: tokens.color.gray11Dark,
colorPress: tokens.color.gray10Dark,
colorFocus: tokens.color.gray6Dark,
shadowColor: tokens.color.grayA6,
shadowColorHover: tokens.color.grayA7,
}
// if you need to add non-token values, use createTheme
const dark_translucent: BaseTheme = createTheme({
...dark,
background: 'rgba(0,0,0,0.7)',
backgroundHover: 'rgba(0,0,0,0.5)',
backgroundPress: 'rgba(0,0,0,0.25)',
backgroundFocus: 'rgba(0,0,0,0.1)',
})
const light_translucent: BaseTheme = createTheme({
...light,
background: 'rgba(255,255,255,0.85)',
backgroundHover: 'rgba(250,250,250,0.85)',
backgroundPress: 'rgba(240,240,240,0.85)',
backgroundFocus: 'rgba(240,240,240,0.7)',
})
// note the steps here
// we recommend doing this because it avoids a category of confusing type errors
// 1. to get ThemeNames/Theme, first create an object with all themes
const allThemes = {
dark,
light,
dark_translucent,
light_translucent,
...colorThemes,
}
// 2. then get the name type
type ThemeName = keyof typeof allThemes
// 3. then, create a Themes type that explicitly maps ThemeName => BaseTheme
type Themes = {
[key in ThemeName]: BaseTheme
}
// 4. finally, export it with the stricter type
export const themes: Themes = allThemes

Dynamic Themes

Sometimes you want to defer loading themes, or change existing theme values at runtime. Tamagui exports two helpers for this, respectively: addTheme and updateTheme.

addTheme

Theme: inherit

updateTheme