styled()

Extend and build custom and optimizable components

styled() extends a component with base styles and typed variants while preserving Tamagui’s compiler optimizations.

For the full list of accepted style properties, see the Styles page.

Start from View or Text for new components. styled() adds base styles and typed variants, and the compiler still optimizes the result:

import { GetProps, styled, View } from '@tamagui/core'
export const RoundedSquare = styled(View, {
borderRadius: 20,
})
// helper to get props for any TamaguiComponent
export type RoundedSquareProps = GetProps<typeof RoundedSquare>

styled() wraps any Tamagui component, including the HTML elements such as html.article.

Usage:

<RoundedSquare x={10} y={10} backgroundColor="red" />

You can pass any prop that is supported by the component you are wrapping in styled.

Variants

With variants:

import { type SizeTokens, View, styled } from '@tamagui/core'
export const RoundedSquare = styled(View, {
borderRadius: 20,
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,
})

Please use as const for the variants definition until Typescript gains the ability to infer generics as const.

Usage:

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

To learn more, see the docs on variants.

Non-working React Native views

React Native utility views like Pressable and TouchableOpacity are not supported targets for styled(). Their event handling logic conflicts with Tamagui. Tamagui components provide press handling and interaction states internally, or you can render Pressable directly when needed.

Using on the web

The styled() function supports Tamagui views, React Native views, and any other React component that accepts a style prop. If you wrap an external component that Tamagui doesn’t recognize, Tamagui will assume it only supports the style prop and not optimize it.

If it does accept className, you can opt-in to className, CSS media queries, and compile-time optimization by adding acceptsClassName:

import { SomeCustomComponent } from 'some-library'
import { styled } from 'tamagui' // or '@tamagui/core'
export const TamaguiCustomComponent = styled(SomeCustomComponent, {
acceptsClassName: true,
})

render

The render prop lets you control which element or component is rendered. It accepts three forms:

String (HTML element)

Render as a specific HTML element on web while maintaining native View on mobile:

const Button = styled(View, {
render: 'button',
padding: '4',
backgroundColor: 'background',
})
const Anchor = styled(Text, {
render: 'a',
color: 'blue-500',
})
const Nav = styled(View, {
render: 'nav',
})

This is the recommended approach for semantic HTML and accessibility. String render props are optimized by the Tamagui compiler.

JSX element

Pass a JSX element to clone with Tamagui’s computed props:

<View className="p-4 bg-background" render={<a href="/about" target="_blank" />}>
About Page
</View>;

The JSX element’s props are merged with Tamagui’s computed styles, classNames, and event handlers. This is useful when you need to pass element-specific props like href or target.

JSX element render props are not optimized by the compiler and will cause a deopt.

Function

For full control, pass a render function that receives props and component state:

import { TamaguiComponentState } from '@tamagui/core';
<View className="p-4 hover:bg-blue-200" render={(props, state: TamaguiComponentState) => <CustomButton {...props} isHovered={state.hover} isPressed={state.press} />}>
Custom Button
</View>;

The state object includes:

  • hover: true when hovered (web)
  • press: true when pressed
  • focus: true when focused
  • disabled: true when disabled

Function render props are not optimized by the compiler and will cause a deopt.

Runtime override

You can also override the render prop at runtime:

const Box = styled(View, {
padding: '4',
})
// Use as a button
<Box render="button">Click me</Box>
// Use as a link
<Box render="a" href="/about">About</Box>

createStyledHOC

If a functional component that renders a Tamagui-styled component should itself accept styled(), wrap it with createStyledHOC. Example:

// 1. you create a `styled` component as usual:
const StyledText = styled(Text)
// 2. you create a wrapper component that adds some logic
// but still returns a styled component that receives the props:
const HigherOrderStyledText = (props) => <StyledText {...props} />
// 3. you want that wrapper component itself to be able to use with `styled`:
const StyledHigherOrderStyledText = styled(HigherOrderStyledText, {
variants: {
// oops, variants will merge incorrectly
},
})

Without the wrapper, Tamagui resolves the style props before passing them down, so the inner component merges variants differently than you’d expect.

The way to fix this is to wrap your HigherOrderStyledText with createStyledHOC. You’ll also want to forward the ref, which is forwarded for you:

const StyledText = styled(Text)
// note the createStyledHOC wrapper here:
const HigherOrderStyledText = createStyledHOC(StyledText, (props, ref) => (
<StyledText ref={ref} {...props} />
))
const StyledHigherOrderStyledText = styled(HigherOrderStyledText, {
variants: {
// variants now merge correctly
},
})

Now your component will handle everything properly, even if a theme is changed on HigherOrderStyledText, it will be applied.

Pass all Tamagui style props given to HigherOrderStyledText down to a single StyledText for everything to work correctly.

And if you’d like to add new props on top of the existing props, annotate the render function’s props parameter. The annotated props merge over the wrapped component’s props:

import { createStyledHOC, View, ViewProps } from '@tamagui/core'
type ExtraProps = {
someCustomProp: boolean
}
export type CustomProps = ViewProps & ExtraProps
const Custom = createStyledHOC(View, (props: ExtraProps) => {
// ...
return null
})

Component resolvers (.resolve)

The .resolve method attaches a dynamic style resolver to a styled component. It receives the component’s complete resolved props and the style environment:

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

Complete props and environment

The resolver callback receives two arguments:

  1. props: the complete styled component prop type, including base props and all declared variants.
  2. env: an object containing { theme, tokens, fonts, font, fontFamily }.

Because props includes the component’s full prop types, .resolve replaces compound variants and sibling-prop inspection logic.

Immutable chaining

Calling .resolve returns a new component. It never mutates the original component:

const Original = styled(View, {})
const Resolved = Original.resolve(() => ({ opacity: 0.5 }))
// Original is unchanged; Resolved has the resolver attached

Each styled layer may attach one resolver. When extending a component with styled(), child components inherit their parent’s resolver chain.

Parent-first execution

The resolver chain executes in parent-first order. The child resolver runs after its parent resolver and overrides conflicting keys:

const Parent = styled(View, {
variants: {
tone: styled.dynamic<'neutral' | 'critical'>(),
},
}).resolve((props, env) => ({
backgroundColor: props.tone === 'critical' ? env.theme['red-10'] : undefined,
width: 100,
}))
const Child = styled(Parent, {}).resolve((props) => ({
width: props.tone === 'critical' ? 200 : undefined,
}))

For <Child tone="critical" />, width is 200 (child resolver wins) and backgroundColor is env.theme['red-10'] (parent resolver lands).

Precedence tiers

Component resolvers sit at layer 2 in Tamagui’s precedence hierarchy:

0 base styles < 1 variants < 2 component resolvers < 3 callsite style props < 4 style prop

Rules for resolution:

  • A resolver beats base styles and variants for the same property.
  • A callsite style prop (such as <AlertBox width={50} />) beats the resolver.
  • The style prop beats everything.
  • Setting a property to undefined in resolver output means absent. The value falls through to lower tiers (variants or base styles).

Static shape rule

For compiler extraction, write resolver return objects with static keys and scalar expressions. Return undefined for inactive branches instead of spreading objects into the return literal:

// good: static keys, undefined for inactive branch
const Box = 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,
}))
// avoid: spreads and computed keys deopt compiler extraction
const DeoptBox = styled(View, {
variants: {
tone: styled.dynamic<'neutral' | 'critical'>(),
},
}).resolve((props) => ({
...(props.tone === 'critical' && { backgroundColor: 'red' }),
}))