Variants

Simple typed prop styles through styled()

Variants turn a small set of typed component props into reusable groups of styles.

Variants expand a single prop into a group of styles. Example:

import { type SizeTokens, View, styled } from 'tamagui' // or '@tamagui/core'
export const Circle = styled(View, {
borderRadius: 100_000_000,
variants: {
pin: {
top: {
position: 'absolute',
top: 0,
},
},
centered: {
true: {
alignItems: 'center',
justifyContent: 'center',
},
},
size: styled.dynamic<SizeTokens | number>((size, { tokens }) => ({
width: tokens.size[size] ?? size,
height: tokens.size[size] ?? size,
})),
} as const,
})

Notice the as const on the variant definition object. This is necessary to please TypeScript until it gains the ability to infer constant objects. If left out, your types may break.

Usage:

<Circle pin="top" centered size="4" />

This component uses a few different types of variants, expanded on below.

Why variants?

Sharing plain style objects means typing props by hand and gluing them together with arbitrary logic inside a functional component. The types may not map to the actual output, and a compiler cannot optimize what it cannot see.

Variants force you out of the React render function, which means no hooks and a much clearer contract: a dynamic takes one value and the style environment, and outputs a statically-shaped group of styles. Sibling-prop logic belongs in a component .resolve callback.

Because variants work with the styled function, they nest without adding depth to your render tree. styled(styled()) is a single React component that the compiler can optimize and flatten.

Variants

Typed variants

true or false

The special keys true and false will map to a boolean. So the centered prop will be typed to accept true or false, and when true it will apply its styles.

import { View, styled } from 'tamagui' // or '@tamagui/core'
export const MyView = styled(View, {
variants: {
selectable: {
true: {
userSelect: 'auto',
},
false: {
userSelect: 'none',
},
},
} as const,
})

String, boolean, and number domains

For an open-ended value domain, declare the prop type on styled.dynamic. There is no runtime type-key lookup:

import { View, styled } from 'tamagui' // or '@tamagui/core'
export const ColorfulView = styled(View, {
variants: {
color: styled.dynamic<string>((color) => ({
color,
borderColor: color,
})),
} as const,
})

Use an exact-key object when the domain is finite, or one dynamic when it is parametric. A key such as string, number, or Size is otherwise just an exact literal key.

Dynamic token variants

When you write variants, you have to be explicit so TypeScript and the runtime know exactly which props you accept. This can be especially cumbersome when you want to “gather” all the values of a specific token scale. For example, without a dynamic, if you wanted to have a pad property that accepted all the keys from tokens.size, you’d have to write this:

// in your tamagui.config.ts:
const tokens = createTokens({
size: {
sm: 10,
md: 15,
lg: 25,
// ...
}
// ... see configuration docs for required tokens
})
export default createTamagui({
tokens
})
// somewhere in your app:
const MyButton = styled(View, {
variants: {
pad: {
sm: {
padding: tokens.size.sm,
},
md: {
padding: tokens.size.md,
},
lg: {
padding: tokens.size.lg,
},
// ...
}
} as const
})
// now you can
<MyButton pad="lg" />

This is verbose, and every token change means updating every component. With a dynamic instead:

// in your tamagui.config.ts:
const tokens = createTokens({
size: {
sm: 10,
md: 15,
lg: 25,
// ...
}
// ... see configuration docs for required tokens
})
export default createTamagui({
tokens
})
// somewhere in your app:
const MyButton = styled(View, {
variants: {
pad: styled.dynamic<SizeTokens>((val, { tokens }) => ({
padding: tokens.size[val]
}))
} as const
})
// now you can
<MyButton pad="lg" />

Dynamic variants save you from hardcoding every token key. The generic parameter is the accepted prop type, so one callback can cover a token scale, raw values, or a union without runtime type-key matching:

const Square = styled(View, {
variants: {
size: styled.dynamic<SizeTokens | number>((val, { tokens }) => {
const value = tokens.size[val] ?? val
return { width: value, height: value }
}),
} as const,
})
// both work:
<Square size="4" />
<Square size={100} />

Environment passed to dynamic variants

The second argument contains the current tokens, theme, and fonts. It never contains component props. Use .resolve((props, env) => ...) when styles depend on sibling props.

const SizableText = styled(Text, {
variants: {
size: styled.dynamic<SizeTokens>((size, { tokens, font }) => ({
fontSize: font?.size[size],
lineHeight: font?.lineHeight[size],
height: tokens.size[size] ?? size,
})),
} as const,
})

Usage:

<SizableText size="4">Hello world</SizableText>

The second argument is always { theme, tokens, fonts, font, fontFamily }.

Props

  • theme

    ThemeParsed

    A proxy to your theme that lets you access active theme values using normal keys.

  • tokens

    TokensParsed

    All tokens parsed from your configuration, accessible with bare keys (e.g. tokens.size[val]).

  • font

    Font

    Maybe undefined. A single resolved Font object from your font configuration.

  • fontFamily

    string

    Maybe undefined. The name of the current fontFamily.

  • fonts

    GenericFonts

    All fonts parsed from your configuration.

  • Bare dynamic declarations

    When variant styles depend on sibling props, declare the prop type with bare styled.dynamic<T>() and apply the styles in a component .resolve callback:

    import { View, styled } from 'tamagui' // or '@tamagui/core'
    export const AlertBox = styled(View, {
    variants: {
    tone: styled.dynamic<'neutral' | 'critical'>(),
    },
    }).resolve((props, env) => ({
    backgroundColor: props.tone === 'critical' ? env.theme['red-10'] : undefined,
    opacity: props.disabled ? 0.5 : undefined,
    }))

    Bare dynamic variants declare the accepted prop type and mark the prop as consumed. It is not forwarded to the underlying DOM element or native view.

    Dynamic value domains

    Use the dynamic’s generic parameter to declare the whole accepted domain, then branch on the value inside the callback when needed:

    import { View, styled } from 'tamagui' // or '@tamagui/core'
    export const ColorfulView = styled(View, {
    variants: {
    colorful: styled.dynamic<true | string>((val) => ({
    color: val === true ? 'red' : val,
    })),
    } as const,
    })

    Dynamic variants

    If you need more complex types, use styled.dynamic to brand the value-to-style function and declare the prop type:

    import { View, styled } from 'tamagui' // or '@tamagui/core'
    export const MyView = styled(View, {
    variants: {
    doubleMargin: styled.dynamic<number>((val) => ({
    margin: val * 2,
    })),
    } as const,
    })

    The callback is invoked once per active flat-clause payload, so responsive and stateful values keep the same behavior as exact variants.

    defaultVariants

    Sometimes you’d like to set a default value for a variant you’ve just set on your styled() component. Due to the way Typescript types parse from left to right, we can’t properly type variants directly on the object you define them on.

    The defaultVariants option allows you to set these, properly typed:

    const Square = styled(View, {
    variants: {
    size: styled.dynamic<SizeTokens>((size, { tokens }) => ({
    // size === 'lg'
    // tokens.size.lg === 25
    width: tokens.size[size] ?? size,
    height: tokens.size[size] ?? size,
    })),
    } as const,
    // <Square /> will get size '10' from size tokens automatically
    defaultVariants: {
    size: '10',
    },
    })

    Variants and flat clauses

    Variant branches can use the same state and media clauses as any other style value, in string or object form:

    const SizedText = styled(Text, {
    variants: {
    size: {
    sm: { fontSize: 'sm' },
    md: {
    fontSize: 'sm md:base xl:lg',
    // equivalent:
    // fontSize: { default: 'sm', md: 'base', xl: 'lg' },
    },
    },
    } as const,
    })

    The variant prop itself accepts conditions too, so a variant can change per state or breakpoint:

    <SizedText size="sm md:md" />
    <SizedText size={{ default: 'sm', md: 'md' }} />

    Variants and parent variants

    Styled components can access their parent components variants, even in their variants:

    const ColorfulText = styled(Text, {
    variants: {
    colored: {
    true: {
    color: 'color',
    },
    },
    large: {
    true: {
    fontSize: '8',
    },
    },
    } as const,
    })
    const MyParagraph = styled(ColorfulText, {
    colored: true,
    variants: {
    hero: {
    true: {
    large: true,
    },
    },
    } as const,
    })