Styling

Tamagui accepts a superset of React Native styles

Tamagui supports a superset of the React Native style properties on to any styled component like the base View and Text components, or through the styled() function as the second argument.

Here’s how that looks in practice:

import { View, styled } from '@tamagui/core'
const StyledView = styled(View, {
padding: 10,
})
const MyView = () => (
<StyledView backgroundColor="red" hoverStyle={{ backgroundColor: 'green', }} />
)

The types for the full set of styles accepted by styled, View and Text are exported as ViewStyle and TextStyle.

For the full base styles, see the React Native docs (version 2 targets React Native 0.82):

The full Tamagui typed style props can be simplified to something like this, except the values can also accept one of your design tokens:

import { ViewStyle as RNViewStyle } from 'react-native'
type BaseViewStyle = RNViewStyle & FlatTransformStyles & CrossPlatformStyles & WebOnlyStyles
// Cross-platform styles:
type CrossPlatformStyles = {
boxShadow?: BoxShadowValue // string, object, or array
filter?: FilterValue // brightness, opacity cross-platform; blur, contrast, etc. Android 12+
backgroundImage?: string // linear-gradient, radial-gradient; supports $tokens
cursor?: Properties['cursor'] // web + iOS 17+ (trackpad/stylus/gaze)
mixBlendMode?: 'normal' | 'multiply' | 'screen' | 'overlay' | ... // blend modes
isolation?: 'auto' | 'isolate'
boxSizing?: 'border-box' | 'content-box'
display?: 'flex' | 'none' | 'contents' | ...
position?: 'absolute' | 'relative' | 'fixed' | 'static' | 'sticky' // 'fixed' converts to 'absolute' on native
border?: BorderValue // shorthand: "1px solid $borderColor"; expands on native
outline?: OutlineValue // shorthand: "2px solid $outlineColor"; expands on native
outlineColor?: ColorValue
outlineOffset?: SpaceValue
outlineStyle?: 'solid' | 'dotted' | 'dashed'
outlineWidth?: SpaceValue
}
// these are accepted but only render on web:
type WebOnlyStyles = {
contain?: Properties['contain']
touchAction?: Properties['touchAction']
backdropFilter?: Properties['backdropFilter']
backgroundOrigin: Properties['backgroundOrigin'],
backgroundPosition: Properties['backgroundPosition'],
backgroundRepeat: Properties['backgroundRepeat'],
backgroundSize: Properties['backgroundSize']
backgroundColor: Properties['backgroundColor']
backgroundClip: Properties['backgroundClip']
backgroundBlendMode: Properties['backgroundBlendMode']
backgroundAttachment: Properties['backgroundAttachment']
background: Properties['background']
clipPath: Properties['clipPath'],
caretColor: Properties['caretColor']
transformStyle: Properties['transformStyle'],
mask: Properties['mask'],
maskImage: Properties['maskImage'],
textEmphasis: Properties['textEmphasis'],
borderImage: Properties['borderImage'],
float: Properties['float']
content: Properties['content']
overflowBlock: Properties['overflowBlock']
overflowInline: Properties['overflowInline']
maskBorder: Properties['maskBorder']
maskBorderMode: Properties['maskBorderMode']
maskBorderOutset: Properties['maskBorderOutset']
maskBorderRepeat: Properties['maskBorderRepeat']
maskBorderSlice: Properties['maskBorderSlice']
maskBorderSource: Properties['maskBorderSource']
maskBorderWidth: Properties['maskBorderWidth']
maskClip: Properties['maskClip']
maskComposite: Properties['maskComposite']
maskMode: Properties['maskMode']
maskOrigin: Properties['maskOrigin']
maskPosition: Properties['maskPosition']
maskRepeat: Properties['maskRepeat']
maskSize: Properties['maskSize']
maskType: Properties['maskType']
}
// these turn into the equivalent transform style props:
type FlatTransformStyles = {
x?: number
y?: number
perspective?: number
scale?: number
scaleX?: number
scaleY?: number
skewX?: string
skewY?: string
matrix?: number[]
rotate?: string
rotateY?: string
rotateX?: string
rotateZ?: string
}
// add the pseudo and enter/exit style states
type WithStates = BaseViewStyle & {
hoverStyle?: BaseViewStyle
pressStyle?: BaseViewStyle
focusStyle?: BaseViewStyle
focusVisibleStyle?: BaseViewStyle
focusWithinStyle?: BaseViewStyle
disabledStyle?: BaseViewStyle
enterStyle?: BaseViewStyle
exitStyle?: BaseViewStyle
}
// final View style props
type ViewStyle = WithStates & {
// add media queries
$sm?: WithStates
// add group queries
$group-hover?: WithStates
$group-focus?: WithStates
$group-press?: WithStates
// add group + container queries
$group-sm-hover?: WithStates
$group-sm-focus?: WithStates
$group-sm-press?: WithStates
// add named group queries
$group-tabs?: WithStates
$group-tabs-hover?: WithStates
$group-tabs-focus?: WithStates
$group-tabs-press?: WithStates
// add named group + container queries
$group-tabs-sm?: WithStates
$group-tabs-sm-hover?: WithStates
$group-tabs-sm-focus?: WithStates
$group-tabs-sm-press?: WithStates
// add theme queries
$theme-light?: WithStates
$theme-dark?: WithStates
// add platform queries
$platform-native?: WithStates
$platform-ios?: WithStates
$platform-android?: WithStates
$platform-web?: WithStates
}
// Text style starts with this base but builds up the same:
type TextStyleBase = BaseViewStyle & {
color?: string,
fontFamily?: string,
fontSize?: string,
fontStyle?: string,
fontWeight?: string,
letterSpacing?: string,
lineHeight?: string,
textAlign?: string,
textDecorationColor?: string,
textDecorationLine?: string,
textDecorationStyle?: string,
textShadowColor?: string,
textShadowOffset?: string,
textShadowRadius?: string,
textTransform?: string,
}

Pseudo States

Tamagui supports several style states that apply based on interaction or focus:

  • hoverStyle - Styles applied when hovering over the element (web only, maps to CSS :hover)
  • pressStyle - Styles applied while the element is being pressed
  • focusStyle - Styles applied when the element has focus (maps to CSS :focus)
  • focusVisibleStyle - Styles applied when the element has keyboard focus (maps to CSS :focus-visible). Use this for keyboard navigation indicators.
  • focusWithinStyle - Styles applied when any child element has focus (maps to CSS :focus-within). Useful for styling containers when an input inside them is focused.
  • disabledStyle - Styles applied when disabled={true} is set
<View backgroundColor="$background" hoverStyle={{ backgroundColor: '$backgroundHover' }} pressStyle={{ backgroundColor: '$backgroundPress', scale: 0.98 }} focusStyle={{ outlineColor: '$blue10', outlineWidth: 2, outlineStyle: 'solid' }} focusVisibleStyle={{ outlineColor: '$blue10', outlineWidth: 2 }} focusWithinStyle={{ borderColor: '$blue10' }} disabledStyle={{ opacity: 0.5 }} />

Enter and Exit Styles

For mount/unmount animations, use:

  • enterStyle - Initial styles when the component mounts (animates from these values)
  • exitStyle - Final styles when the component unmounts (animates to these values)

See Animations for more details on enter/exit animations.

rem Units

Tamagui supports rem units cross-platform. On web, browsers handle rem natively. On native, Tamagui converts rem values to pixels using PixelRatio.getFontScale() to respect the user’s font size preferences.

// works on both web and native
<View padding="1rem" />
<Text fontSize="1.5rem" />

You can configure the base font size (default 16) in your config:

createTamagui({
settings: {
remBaseFontSize: 16, // default
},
})

On native, 1rem with a base of 16 equals 16 * PixelRatio.getFontScale() pixels, scaling with the user’s accessibility settings.

CSS Shorthand with Variables

Tamagui supports using $variables directly inside CSS shorthand string values. This is particularly useful for boxShadow and backgroundImage where you want to reference theme colors:

// boxShadow with variables
<View boxShadow="0 0 10px $shadowColor" />
<View boxShadow="0 0 5px $shadowColor, 0 0 15px $color" />
// backgroundImage with linear-gradient (cross-platform, RN 0.76+)
<View backgroundImage="linear-gradient(to bottom, $background, $color)" />
<View backgroundImage="linear-gradient(45deg, $black 0%, $white 50%, $black 100%)" />
// filter with variables
<View filter="blur($2)" />
// On web: resolves to CSS custom properties
// boxShadow: "0 0 10px var(--t-shadow-shadowColor)"
// backgroundImage: "linear-gradient(to bottom, var(--background), var(--color))"
// On native: resolves to raw values
// boxShadow: "0 0 10px rgba(0,0,0,0.2)"
// backgroundImage: "linear-gradient(to bottom, #fff, #000)"

Supported shorthand properties with variable resolution:

  • boxShadow - Works on both web and native
  • backgroundImage - Works on both web and native (see below)
  • filter - Works on both web and native
  • border - Works on both web and native (expands to borderWidth/Style/Color on native)
  • outline - Works on both web and native (expands to outlineWidth/Style/Color on native)
  • borderTop, borderRight, borderBottom, borderLeft - Web only
  • background - Web only

border

The border shorthand now works cross-platform:

// Works on both web and native
<View border="1px solid $borderColor" />
<View border="2px dashed red" />
<View border="none" />

On web, the CSS shorthand is passed through directly. On native, it expands to individual properties:

  • borderWidthborderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidth
  • borderStyleborderStyle
  • borderColorborderTopColor, borderRightColor, borderBottomColor, borderLeftColor

Note: On native, only a single border (all sides) is supported. For per-side borders on native, use individual props.

outline

The outline shorthand works cross-platform, similar to border:

// Works on both web and native
<View outline="2px solid $outlineColor" />
<View outline="1px dashed red" />
<View outline="none" />

On web, the CSS shorthand is passed through directly. On native, it expands to outlineWidth, outlineStyle, and outlineColor.

backgroundImage

Supports linear-gradient() and radial-gradient() cross-platform. Does not support url() on native - use <ImageBackground> for images.

<View backgroundImage="linear-gradient(to bottom, $background, $color)" />
<View backgroundImage="radial-gradient(circle, $white, $black)" />

textShadow

Text shadows support CSS shorthand with token resolution, just like boxShadow:

import { Text } from 'tamagui'
// CSS shorthand with tokens
<Text textShadow="2px 2px 4px $shadowColor">
Shadowed Text
</Text>
// multiple shadows (web only)
<Text textShadow="1px 1px 2px $shadowColor, 0 0 8px $color">
Glowing Text
</Text>

You can also use the individual React Native props:

<Text textShadowColor="$shadowColor" textShadowOffset={{ width: 2, height: 2 }} textShadowRadius={4} >
Shadowed Text
</Text>

On native, only a single text shadow is supported (React Native limitation).

Parent based styling

Tamagui has a variety of ways to style a child based on the “parent”, a parent being one of: platform, screen size, theme, or group. All of these styles use the same pattern, they use a $ prefix for their styles, and they nest styles as a sub-object.

For example you can target $theme-light, $platform-ios, or $group-header. For screen size, which we call media queries, they have no prefix. Instead you define media queries on createTamagui. For example, if you define a media query named large, then $large is the prop name.

These parent style props accept all the Tamagui style props in their value object.

Media query

Based on whatever media queries you define in createTamagui, you can now use any of them to apply styling on native and web using the $ prefix.

If you defined your media query like:

createTamagui({
media: {
sm: { maxWidth: 800 },
},
})

Then you can use it like:

<Text color="red" $sm={{ color: 'blue' }} />

Theme

Theme style props let you style a child based on a parent theme. At the moment, they only can target your top level themes, so if you have light, and light_subtle themes, then only light can be targeted.

Use them like so:

<Text $theme-dark={{ color: 'white' }} />

Platform

Platform style props let you style a child based on the platform the app is running on. This can be one of ios, android, web, or native (iOS and Android).

Use it like so:

<Text $platform-ios={{ color: 'white' }} />

Group

Groups are a new feature in beta that lets you define a named group, and then style children based whether they are inside a parent that is given that group name.

A short example:

<View group="header">
<Text $group-header={{ color: 'white' }} />
</View>

This will make the Text turn white, as it’s inside a parent item with group set to the matching header value.

Group styles also allow for targeting the parent pseudo state:

<View group>
<Text $group-hover={{ color: 'white' }} />
</View>

Now only when the parent View is hovered the Text will turn white. The allowed pseudo modifiers are hover (web only), press, and focus.

For more advanced use cases you can use named groups:

<View group="card">
<Text>Outer</Text>
<View group>
<Text $group-card-hover={{ color: 'blue' }}>Inner</Text>
<Text $group-hover={{ color: 'green' }}>Sibling</Text>
</View>
</View>

Now the Inner Text will turn blue when the card group is hovered, and the Sibling Text will turn green when its parent is hovered.

To make this typed, you need to set TypeOverride alongside the same area you set up your Tamagui types:

declare module 'tamagui' {
interface TamaguiCustomConfig extends AppConfig {}
// if you want types for named group styling props (e.g. $group-card-hover),
// define your group names here:
interface TypeOverride {
groupNames(): 'card' | 'header' | 'sidebar'
}
}

Group Container

The final feature of group styles is the ability to style a child only when the parent is of a certain size. On the web these are known as “container queries”, which is what Tamagui outputs as CSS under the hood. They look like this:

<View group>
<Text $group-sm={{ color: 'white' }} $group-sm-hover={{ color: 'green' }} />
</View>

Now the Text will be white, but only when the View matches the media query sm. This uses the same media query breakpoints you defined in createTamagui({ media }). You can see it also works with pseudo styles!

For more advanced use cases, you can use named groups with container queries:

<View group="card">
<View group>
<Text $group-card-sm={{ color: 'white' }} $group-card-sm-hover={{ color: 'green' }} />
<Text $group-sm={{ color: 'white' }} $group-sm-hover={{ color: 'green' }} />
</View>
</View>

Now the first Text will be white when the card parent matches sm, and the second Text will be white when no named parent matches sm.

A note on group containers and native

On Native, we don’t have access to the layout of a React component as it first renders. Only once we get the dimensions from the onLayout callback after the first render are we able to apply group container styles.

Because of this, we’ve done two things.

First, there’s a new property untilMeasured:

<View group untilMeasured="hide">
<Text $group-sm={{ color: 'white', }} />
</View>

This takes two options, show or hide. If unset it defaults to show, which means it will render before it measures. With hide set, the container will be set to opacity 0 until it finishes measuring.

Alternatively, if you know the dimensions your container will be up-front, you can set width and height on the container. When either of these are set, the children will attempt to use these values on first render and if they satisfy the media query, you’ll avoid the need for a double render altogether.

Order is important

It’s important to note that the order of style properties is significant. This is really important for two reasons:

  1. You want to control which styles are overridden.
  2. You have a variant that expands into multiple style properties, and you need to control it.

Let’s see how it lets us control overriding styles:

import { View, ViewProps } from '@tamagui/core'
export default (props: ViewProps) => <View background="red" {...props} width={200} />

In this case we set a default background to red, but it can be overridden by props. But we set width after the prop spread, so width is always going to be set to 200.

It also is necessary for variants to make sense. Say we have a variant huge that sets scale to 2 and borderRadius to 100:

// this will be scale = 3
export default (props: ViewProps) => <MyView huge scale={3} />
// this will be scale = 2
export default (props: ViewProps) => <MyView scale={3} huge />

If order wasn’t important, how would you expect these two different usages to work? You’d have to make order important somewhere. If you do it in the styled() helper somewhere, you end up having no flexibility and would end up with boilerplate. Making the prop order important gives us maximum expressiveness and is easy to understand.