Themes
How to create and use themes in Tamagui.
Themes map neatly to CSS variables: they are objects whose values you want to contextually change at any point in your React tree. They are used either as the first lookup for $
prefixed style values, or with the useTheme
hook directly. Tamagui allows nesting themes - both the definition and at runtime. At runtime Tamagui resolves theme values upwards, ultimately all the way back to tokens.
If you learn faster through example, skip to the Quick Start.
Once you've learned the basics here, be sure to check out the ThemeBuilder guide for generating more interesting theme suites.
If you want a copy-paste theme generation setup, try this gist for a well-structured example.
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
One of the unique powers of Tamagui is theme nesting . Define a theme with a name in the form of parentName_subName
and Tamagui will let you nest themes, with both parentName
and subName
being valid theme names.
You can do this as many times as you'd like. Here's an example of having three 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>
In general you want your themes to all be the same shape - the same named keys and typed values - but sub-themes can be sub-sets of parent themes. The useTheme
hook and style system will resolve missing keys upwards to parent themes, and ultimately to tokens.
Component themes
Every Tamagui styled()
component looks for it's own specific theme if you pass it the name
property. For example:
import { Stack, styled } from 'tamagui' // or '@tamagui/core'const Circle = styled(Stack, {name: 'Circle',backgroundColor: '$background',})
The name
attribute will be removed from the defaultProps and used internally by Tamagui to check for a sub-theme that ends with _Circle
.
Now you can create the default theme for all Circle components at any level of nesting:
const dark_Circle = {background: 'darkred',color: 'white',}const light_Circle = {background: 'lightred',color: 'black',}
Component themes must have the first letter capitalized.
dark_Circle
dark_green_Circle
dark_green_subtle_Circle
This is an incredibly powerful and unique feature that allows authors of UI components control over design, while still letting users customize them completely.
While @tamagui/core
isn't prescriptive at all, tamagui
is. This is because standardizing on specific shared theme keys unlocks huge upside. We recommend authors to use these values as well, to enable maximum sharing.
In tamagui
, all components will look for the following keys:
background
color
borderColor
shadowColor
placeholderColor
(no pseudo variants)
...plus all the pseudo variants for each, eg, backgroundHover
, backgroundPress
, and backgroundFocus
.
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-components, you can follow this pattern:
import { GetProps, Stack, Text, styled } from 'tamagui' // or '@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.
Quick start
To get started quickly, you can use the themes we've developed alongside this site and with other apps, @tamagui/themes
. It's even easier to see how it all comes together by using create-tamagui to bootstrap.
To install, just add import it and add it to your tamagui.config.ts
:
import { color, radius, size, space, themes, zIndex } from '@tamagui/themes'import { createTamagui, createTokens } from 'tamagui'const tokens = createTokens({size,space,zIndex,color,radius,})const config = createTamagui({themes,tokens,// ... see Configuration})export type Conf = typeof configdeclare module 'tamagui' {interface TamaguiCustomConfig extends Conf {}}export default config
If you want to customize the starter themes, we recommend you just grab the src
for
@tamagui/themes
and copy/paste it into your app, and customize from there.
Full Example
Let's start with an example of inline styling with a subset of the configuration:
import { TamaguiProvider, createTokens, createTamagui, View, Theme } from 'tamagui'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"><View backgroundColor="$red" /><Theme name="dark"><View backgroundColor="$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 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. Keep themes in a separate themes.ts
file, and structure it like this:
import { tokens } from './tokens'const light = {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 BaseThemeconst 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,}const dark_translucent: BaseTheme = {...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 = {...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)',}export const allThemes = {dark,light,dark_translucent,light_translucent,} satisfies {[key: string]: BaseTheme}// note: `satisfies` was introduced with TypeScript 4.9
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
andreplaceTheme
is thatreplaceTheme
will replace the entire theme, whileupdateTheme
will only update the values that are passed in. - On the web if you are going to change between dark and light themes more than 3 times, you'll want to adjust the
maxDarkLightNesting
option, see Configuration.
Advanced Optimization
You can configure Tamagui to not send any themes JS to the client side, so long as your are serving the resulting css file from the getCSS
call on initial load of your app (SSR).
To enable this you need to have your bundler tree shake away the themes object you'd typically pass to createTamagui
for the client bundle. Note this is a somewhat advanced optimization and not necessary to do right away.
import { themes as themesIn } from './your-themes-file'// We leave this value empty for production client side bundles to save on bundle size.// The `@tamagui/next-plugin` sets TAMAGUI_IS_SERVER automatically.// If you pass an empty themes object Tamagui will try to hydrate by scanning CSS in browser environments.// It typically takes low single-digit ms to scan and can save significantly on JS size.const themes =process.env.TAMAGUI_IS_SERVER || process.env.NODE_ENV !== 'production'? themesIn: ({} as typeof themesIn)export const config = createTamagui({themes,// ...})
Previous
Variants
Next
Animations