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 arrayfilter?: FilterValue // brightness, opacity cross-platform; blur, contrast, etc. Android 12+backgroundImage?: string // linear-gradient, radial-gradient; supports $tokenscursor?: Properties['cursor'] // web + iOS 17+ (trackpad/stylus/gaze)mixBlendMode?: 'normal' | 'multiply' | 'screen' | 'overlay' | ... // blend modesisolation?: 'auto' | 'isolate'boxSizing?: 'border-box' | 'content-box'display?: 'flex' | 'none' | 'contents' | ...position?: 'absolute' | 'relative' | 'fixed' | 'static' | 'sticky' // 'fixed' converts to 'absolute' on nativeborder?: BorderValue // shorthand: "1px solid $borderColor"; expands on nativeoutline?: OutlineValue // shorthand: "2px solid $outlineColor"; expands on nativeoutlineColor?: ColorValueoutlineOffset?: SpaceValueoutlineStyle?: '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?: numbery?: numberperspective?: numberscale?: numberscaleX?: numberscaleY?: numberskewX?: stringskewY?: stringmatrix?: number[]rotate?: stringrotateY?: stringrotateX?: stringrotateZ?: string}// add the pseudo and enter/exit style statestype WithStates = BaseViewStyle & {hoverStyle?: BaseViewStylepressStyle?: BaseViewStylefocusStyle?: BaseViewStylefocusVisibleStyle?: BaseViewStylefocusWithinStyle?: BaseViewStyledisabledStyle?: BaseViewStyleenterStyle?: BaseViewStyleexitStyle?: BaseViewStyle}// final View style propstype 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 pressedfocusStyle- 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 whendisabled={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 nativebackgroundImage- Works on both web and native (see below)filter- Works on both web and nativeborder- 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 onlybackground- 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:
borderWidth→borderTopWidth,borderRightWidth,borderBottomWidth,borderLeftWidthborderStyle→borderStyleborderColor→borderTopColor,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:
- You want to control which styles are overridden.
- 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 = 3export default (props: ViewProps) => <MyView huge scale={3} />// this will be scale = 2export 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.