Style API
Tamagui supports a superset of the React Native style properties - either to the
styled()
function as the second argument, or directly as props on the View and
Text base components.
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:
The full Tamagui typed style props can be simplified to something like this,
except the values can accept "unset"
or one of your design tokens:
import { ViewStyle as RNViewStyle } from 'react-native'type BaseViewStyle = RNViewStyle & FlatTransformStyles & WebOnlyStyles// these are accepted but only render on web:type WebOnlyStyles = {contain?: Properties['contain']touchAction?: Properties['touchAction']cursor?: Properties['cursor']outlineColor?: Properties['outlineColor']outlineOffset?: SpaceValueoutlineStyle?: Properties['outlineStyle']outlineWidth?: SpaceValueuserSelect?: Properties['userSelect']filter?: Properties['filter']backdropFilter?: Properties['backdropFilter']mixBlendMode?: Properties['mixBlendMode']backgroundImage?: Properties['backgroundImage']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?: 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,}
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
psuedo modifiers are hover
(web only), press
, and focus
.
For more advanced usecases 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 group styling props, define them like so:interface TypeOverride {groupNames(): 'a' | 'b' | 'c'}}
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.
Style order is important
One thing that's very important to understand in Tamagui is that style props are sensitive to their order - a feature that without which would leave us with impossible styling challenges and awkward rules of inheritence we're trying to get away from with CSS in JS. Let's first explain how it works, and then why it's necessary.
Define a new styled component:
const CalHeader = styled(Text, {variants: {isHero: {true: {fontSize: 36,backgroundColor: 'blue',color: 'white',},},},})
And then use it in a view you're building:
export const MyCalendar = (props: { isHero?: boolean; headerFontSize?: number }) => {return (<>{/* ... some other components... */}<CalHeader isHero={props.isHero} fontSize={props.headerFontSize}>{monthName}</CalHeader></>)}
Why it's important
Notice two things: isHero
sets a variety of properties, but you want to allow
overriding one of those properties, fontSize
.
If Tamagui didn't respect the order of the props on the JSX element of
CalHeader
, you wouldn't know if the isHero
font size wins, or if the
headerFontSize
wins.
CSS does a "last defined style wins", which is a huge pain because it means you have to carefully manage the order your CSS is actually loaded into the document. Tamagui could do something similar, with a "inline props always win, defined props go in order of definition", but this would be dramatically less flexible.
Especially when it comes to spreading props downwards. Because Tamagui supports prop order, you have complete control over which styles you want to always win, vs which can be overridden by a user:
export const MyCalendarHeader = (props: CalHeaderProps) => {return (<CalHeader isHero {...props} fontSize={36}>{monthName}</CalHeader>)}
This component defaults to isHero
styles, but if a user passes in isHero
as
false
, it will disable all those styles. But because fontSize
is always
after the spread, it will always be set to 36.