Overall AI readiness score
A combined result across all audit signals.
Overall AI readiness score
A combined result across all audit signals.
Overall AI readiness assessment
Tamagui Complete Documentation (tamagui.dev) received an AI-readiness score of 60/100 in an automated technical audit. llms.txt was accessible, llms-full.txt was accessible, and ai.txt was not found. The robots.txt analysis found 0 explicitly allowed and 0 blocked AI crawlers, with 0 declared sitemaps. Homepage markup completeness was 0%; no Schema.org types were detected and 0 OpenGraph tags were detected. Results reflect the public site response observed on 2026-08-19T01:40:52.465Z.
llms.txt is accessible and contains 269,592 tokens. An expanded llms-full.txt is also available with 269,592 tokens, giving agents more direct context. No separate ai.txt policy was detected; it is optional, but can clarify training, retrieval, and attribution preferences.
robots.txt is available. 11 of 11 tracked AI bots are not blocked. Declared sitemaps: 0. No Schema.org type was detected on the homepage. 0 OpenGraph tags were found and markup completeness is 0%, leaving more entity interpretation to crawlers.
The Google Lighthouse measurement is still collecting. Once complete, performance, accessibility, technical quality, SEO, and experimental browser-agent signals will be added with a limited weight.
The background queue is collecting mobile Performance, Accessibility, Best Practices, SEO, and experimental browser-agent signals.
Machine-readable files, crawler policy, discovery, and homepage markup.
ai.txt file was not found
Sitemap is not declared in robots.txt
Schema.org markup was not found on the homepage
OpenGraph tags were not found on the homepage
Based on robots.txt analysis
# Tamagui Complete Documentation
> Tamagui is a complete UI solution for React Native and Web, with a fully-featured UI kit, styling engine, and optimizing compiler.
## components/accordion/1.0.0
---
title: Accordion
description: A vertically stacked set of interactive headings with content.
name: accordion
component: Accordion
package: accordion
---
<HeroContainer>
<AccordionDemo />
</HeroContainer>
```tsx hero template=Accordion
```
<Highlights
features={[
'Full keyboard navigation.',
'Can expand one or multiple items.',
'Can be controlled or uncontrolled.',
]}
/>
## Installation
Accordion is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/accordion
```
## Anatomy
Import all parts and piece them together.
```jsx
import { Accordion } from 'tamagui' // or '@tamagui/accordion'
export default () => (
<Accordion>
<Accordion.Item>
<Accordion.Header>
<Accordion.Trigger />
</Accordion.Header>
<Accordion.Content />
</Accordion.Item>
</Accordion>
)
```
## API Reference
### Accordion
Contains all the parts of an accordion.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
{
name: 'type',
required: true,
type: '"single" | "multiple"',
typeSimple: 'enum',
description: (
<span>
Determines whether one or multiple items can be opened at the same time.
</span>
),
},
{
name: 'value',
required: false,
type: 'string',
description: (
<span>
The controlled value of the item to expand when <Code>type</Code> is{' '}
<Code>"single"</Code>. Must be used in conjunction with{' '}
<Code>onValueChange</Code>.
</span>
),
},
{
name: 'defaultValue',
required: false,
type: 'string',
description: (
<span>
The value of the item to expand when initially rendered and <Code>type</Code> is{' '}
<Code>"single"</Code>. Use when you do not need to control the state of the
items.
</span>
),
},
{
name: 'onValueChange',
required: false,
type: '(value: string) => void',
typeSimple: 'function',
description: (
<span>
Event handler called when the expanded state of an item changes and{' '}
<Code>type</Code> is <Code>"single"</Code>.
</span>
),
},
{
name: 'value',
required: false,
default: '[]',
type: 'string[]',
description: (
<span>
The controlled value of the item to expand when <Code>type</Code> is{' '}
<Code>"multiple"</Code>. Must be used in conjunction with{' '}
<Code>onValueChange</Code>.
</span>
),
},
{
name: 'defaultValue',
required: false,
default: '[]',
type: 'string[]',
description: (
<span>
The value of the item to expand when initially rendered when <Code>type</Code>{' '}
is <Code>"multiple"</Code>. Use when you do not need to control the state of the
items.
</span>
),
},
{
name: 'onValueChange',
required: false,
type: '(value: string[]) => void',
typeSimple: 'function',
description: (
<span>
Event handler called when the expanded state of an item changes and{' '}
<Code>type</Code> is <Code>"multiple"</Code>.
</span>
),
},
{
name: 'collapsible',
required: false,
default: 'false',
type: 'boolean',
description: (
<span>
When <Code>type</Code> is <Code>"single"</Code>, allows closing content when
clicking trigger for an open item.
</span>
),
},
{
name: 'disabled',
required: false,
type: 'boolean',
default: 'false',
description: (
<span>
When <Code>true</Code>, prevents the user from interacting with the accordion
and all its items.
</span>
),
},
{
name: 'dir',
required: false,
type: '"ltr" | "rtl"',
typeSimple: 'enum',
default: '"ltr"',
description:
'The reading direction of the accordion when applicable. If omitted, assumes LTR (left-to-right) reading mode.',
},
]}
/>
### Item
Contains all the parts of a collapsible section.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
{
name: 'disabled',
required: false,
type: 'boolean',
default: 'false',
description: (
<span>
When <Code>true</Code>, prevents the user from interacting with the item.
</span>
),
},
{
name: 'value',
required: true,
type: 'string',
description: 'A unique value for the item.',
},
]}
/>
### Header
Wraps an `Accordion.Trigger`. Use the `asChild` prop to update it to the appropriate heading level for your page.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
]}
/>
### Trigger
Toggles the collapsed state of its associated item. It should be nested inside of an `Accordion.Header`.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
]}
/>
### Content
Contains the collapsible content for an item.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
{
name: 'forceMount',
type: 'boolean',
description: (
<span>
Used to force mounting when more control is needed. Useful when controlling
animation with React animation libraries.
</span>
),
},
]}
/>
## Examples
### Expanded by default
Use the `defaultValue` prop to define the open item by default.
```jsx line=1
<Accordion type="single" __defaultValue__="item-2">
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion>
```
### Allow collapsing all items
Use the `collapsible` prop to allow all items to close.
```jsx line=1
<Accordion type="single" __collapsible__>
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion>
```
### Multiple items open at the same time
Set the `type` prop to `multiple` to enable opening multiple items at once.
```jsx line=1
<Accordion type="__multiple__">
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion>
```
## Accessibility
Adheres to the [Accordion WAI-ARIA design pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#accordion).
## components/accordion/2.0.0
---
title: Accordion
description: A vertically stacked set of interactive headings with content
name: accordion
component: Accordion
package: accordion
demoName: Accordion
---
<HeroContainer>
<AccordionDemo />
</HeroContainer>
```tsx hero template=Accordion
```
<Highlights
features={[
'Full keyboard navigation.',
'Can expand one or multiple items.',
'Can be controlled or uncontrolled.',
]}
/>
## Installation
Accordion is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/accordion
```
## Anatomy
Import all parts and piece them together.
```jsx
import { Accordion } from 'tamagui' // or '@tamagui/accordion'
export default () => (
<Accordion>
<Accordion.Item>
<Accordion.Header>
<Accordion.Trigger />
</Accordion.Header>
<Accordion.Content />
</Accordion.Item>
</Accordion>
)
```
## API Reference
### Accordion
Contains all the parts of an accordion.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
{
name: 'type',
required: true,
type: '"single" | "multiple"',
typeSimple: 'enum',
description: (
<span>
Determines whether one or multiple items can be opened at the same time.
</span>
),
},
{
name: 'value',
required: false,
type: 'string',
description: (
<span>
The controlled value of the item to expand when <Code>type</Code> is{' '}
<Code>"single"</Code>. Must be used in conjunction with{' '}
<Code>onValueChange</Code>.
</span>
),
},
{
name: 'defaultValue',
required: false,
type: 'string',
description: (
<span>
The value of the item to expand when initially rendered and <Code>type</Code> is{' '}
<Code>"single"</Code>. Use when you do not need to control the state of the
items.
</span>
),
},
{
name: 'onValueChange',
required: false,
type: '(value: string) => void',
typeSimple: 'function',
description: (
<span>
Event handler called when the expanded state of an item changes and{' '}
<Code>type</Code> is <Code>"single"</Code>.
</span>
),
},
{
name: 'value',
required: false,
default: '[]',
type: 'string[]',
description: (
<span>
The controlled value of the item to expand when <Code>type</Code> is{' '}
<Code>"multiple"</Code>. Must be used in conjunction with{' '}
<Code>onValueChange</Code>.
</span>
),
},
{
name: 'defaultValue',
required: false,
default: '[]',
type: 'string[]',
description: (
<span>
The value of the item to expand when initially rendered when <Code>type</Code>{' '}
is <Code>"multiple"</Code>. Use when you do not need to control the state of the
items.
</span>
),
},
{
name: 'onValueChange',
required: false,
type: '(value: string[]) => void',
typeSimple: 'function',
description: (
<span>
Event handler called when the expanded state of an item changes and{' '}
<Code>type</Code> is <Code>"multiple"</Code>.
</span>
),
},
{
name: 'collapsible',
required: false,
default: 'false',
type: 'boolean',
description: (
<span>
When <Code>type</Code> is <Code>"single"</Code>, allows closing content when
clicking trigger for an open item.
</span>
),
},
{
name: 'disabled',
required: false,
type: 'boolean',
default: 'false',
description: (
<span>
When <Code>true</Code>, prevents the user from interacting with the accordion
and all its items.
</span>
),
},
{
name: 'dir',
required: false,
type: '"ltr" | "rtl"',
typeSimple: 'enum',
default: '"ltr"',
description:
'The reading direction of the accordion when applicable. If omitted, assumes LTR (left-to-right) reading mode.',
},
]}
/>
### Accordion.Item
Contains all the parts of a collapsible section.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
{
name: 'disabled',
required: false,
type: 'boolean',
default: 'false',
description: (
<span>
When <Code>true</Code>, prevents the user from interacting with the item.
</span>
),
},
{
name: 'value',
required: true,
type: 'string',
description: 'A unique value for the item.',
},
]}
/>
### Accordion.Header
Wraps an `Accordion.Trigger`. Use the `asChild` prop to update it to the appropriate heading level for your page.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
]}
/>
### Accordion.Trigger
Toggles the collapsed state of its associated item. It should be nested inside of an `Accordion.Header`.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
]}
/>
### Accordion.Content
Contains the collapsible content for an item.
<PropsTable
data={[
{
name: 'asChild',
required: false,
type: 'boolean',
default: 'false',
description:
'Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.',
},
{
name: 'forceMount',
type: 'boolean',
description: (
<span>
Used to force mounting when more control is needed. Useful when controlling
animation with React animation libraries.
</span>
),
},
]}
/>
## Examples
### Expanded by default
Use the `defaultValue` prop to define the open item by default.
```jsx line=1
<Accordion type="single" __defaultValue__="item-2">
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion>
```
### Allow collapsing all items
Use the `collapsible` prop to allow all items to close.
```jsx line=1
<Accordion type="single" __collapsible__>
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion>
```
### Multiple items open at the same time
Set the `type` prop to `multiple` to enable opening multiple items at once.
```jsx line=1
<Accordion type="__multiple__">
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion>
```
## Accessibility
Adheres to the [Accordion WAI-ARIA design pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#accordion).
## components/alert-dialog/1.0.0
---
title: AlertDialog
description: Show an alert prompt in a dialog
name: alertDialog
component: AlertDialog
package: alert-dialog
demoName: AlertDialog
---
<HeroContainer showAnimationDriverControl>
<AlertDialogDemo />
</HeroContainer>
```tsx hero template=AlertDialog
```
<Highlights
features={[
`Comes with styling, completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible with dev-time checks to ensure ARIA props.`,
]}
/>
## Installation
Alert Dialog is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/alert-dialog
```
In order to use this component independently of `tamagui`, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
## Anatomy
```tsx
import { AlertDialog } from 'tamagui' // or '@tamagui/alert-dialog'
export default () => (
<AlertDialog>
<AlertDialog.Trigger />
<AlertDialog.Portal>
<AlertDialog.Overlay />
<AlertDialog.Content>
<AlertDialog.Title />
<AlertDialog.Description />
<AlertDialog.Cancel />
{/* ... */}
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog>
)
```
## API Reference
### AlertDialog
Contains every component for the AlertDialog. Shares all [Dialog Props](/docs/components/dialog#api), except modal which is on by default. Adds:
<PropsTable
data={[
{
name: 'native',
type: 'boolean',
required: false,
description: `When true, on iOS it will render as a native AlertDialog.`,
},
]}
/>
### AlertDialog.Trigger
Just [Tamagui Props](/docs/intro/props).
### AlertDialog.Portal
Renders AlertDialog into appropriate container. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### AlertDialog.Content
Main container for AlertDialog content, this is where you should apply animations.
Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### AlertDialog.Overlay
Displays behind Content. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### AlertDialog.Title
Required. Can wrap in VisuallyHidden to hide.
Defaults to H2, see [Headings](/docs/components/headings).
### AlertDialog.Description
Required. Can wrap in VisuallyHidden to hide.
Defaults to Paragraph, see [Paragraph](/docs/components/text).
### AlertDialog.Cancel
Closes the AlertDialog, accepts all YStack props. Recommended to use with your own component and `asChild`.
<PropsTable
data={[
{
name: 'displayWhenAdapted',
type: 'boolean',
description: `By default Cancel elements hide when Adapt is active. If set to true, they will show when adapted.`,
},
]}
/>
### PortalProvider
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Examples
### Inside native modals
If you're using native modals (maybe from react-navigation), you'll notice the Dialogs won't show up inside the modal. To get around this, you should wrap your screen inside `PortalProvider`, like so:
```tsx
import { PortalProvider } from 'tamagui'
// this component used in react-navigation/expo-router with `presentation: "modal"`
export function Page() {
return (
<PortalProvider>{/* rest of your page, including the Dialog... */}</PortalProvider>
)
}
```
## components/alert-dialog/2.0.0
---
title: Alert Dialog
description: Show an alert prompt in a dialog
name: alert-dialog
component: AlertDialog
package: alert-dialog
demoName: AlertDialog
---
<HeroContainer showAnimationDriverControl>
<AlertDialogDemo />
</HeroContainer>
```tsx hero template=AlertDialog
```
<Highlights
features={[
`Comes with styling, completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible with dev-time checks to ensure ARIA props.`,
]}
/>
AlertDialog is a modal dialog that interrupts the user with important content and expects a response. It's built on Dialog with stricter accessibility requirements, and [automatically stacks](/ui/z-index) above other content.
## Installation
Alert Dialog is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/alert-dialog
```
## Anatomy
```tsx
import { AlertDialog } from 'tamagui' // or '@tamagui/alert-dialog'
export default () => (
<AlertDialog>
<AlertDialog.Trigger />
<AlertDialog.Portal>
<AlertDialog.Overlay />
<AlertDialog.Content>
<AlertDialog.Title />
<AlertDialog.Description />
<AlertDialog.Cancel />
{/* ... */}
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog>
)
```
## API Reference
### AlertDialog
Contains every component for the AlertDialog. Shares all [Dialog Props](/docs/components/dialog#api), except modal which is on by default. Adds:
<PropsTable
data={[
{
name: 'native',
type: 'boolean',
required: false,
description: `When true, on iOS it will render as a native AlertDialog.`,
},
]}
/>
### AlertDialog.Trigger
Just [Tamagui Props](/docs/intro/props).
### AlertDialog.Portal
Renders AlertDialog into appropriate container. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### AlertDialog.Content
Main container for AlertDialog content, this is where you should apply animations.
Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### AlertDialog.Overlay
Displays behind Content. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### AlertDialog.Title
Required. Can wrap in VisuallyHidden to hide.
Defaults to H2, see [Headings](/docs/components/headings).
### AlertDialog.Description
Required. Can wrap in VisuallyHidden to hide.
Defaults to Paragraph, see [Paragraph](/docs/components/text).
### AlertDialog.Cancel
Closes the AlertDialog, accepts all YStack props. Recommended to use with your own component and `asChild`.
<PropsTable
data={[
{
name: 'displayWhenAdapted',
type: 'boolean',
description: `By default Cancel elements hide when Adapt is active. If set to true, they will show when adapted.`,
},
]}
/>
### AlertDialog.Action
Confirms the AlertDialog action, accepts all YStack props. Recommended to use with your own component and `asChild`.
<PropsTable
data={[
{
name: 'displayWhenAdapted',
type: 'boolean',
description: `By default Action elements hide when Adapt is active. If set to true, they will show when adapted.`,
},
]}
/>
### AlertDialog.Destructive
A destructive action button for the AlertDialog. When using `native` mode on iOS, this renders with the native red destructive button style. Accepts all YStack props.
```tsx
<AlertDialog native>
<AlertDialog.Trigger asChild>
<Button>Delete Account</Button>
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Overlay />
<AlertDialog.Content>
<AlertDialog.Title>Delete Account?</AlertDialog.Title>
<AlertDialog.Description>This action cannot be undone.</AlertDialog.Description>
<AlertDialog.Cancel asChild>
<Button>Cancel</Button>
</AlertDialog.Cancel>
<AlertDialog.Destructive asChild>
<Button theme="red">Delete</Button>
</AlertDialog.Destructive>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog>
```
<PropsTable
data={[
{
name: 'displayWhenAdapted',
type: 'boolean',
description: `By default Destructive elements hide when Adapt is active. If set to true, they will show when adapted.`,
},
]}
/>
### PortalProvider
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## components/anchor/1.0.0
---
title: Anchor
description: Link to external websites.
name: html
component: Anchor
---
<Highlights
features={['Supports SSR.', 'Works on native and web.', 'Accepts Tamagui style props.']}
/>
## Usage
The Anchor component provides a way to link to external websites. It extends [SizableText](/docs/components/text#sizable-text), adding the `href`, `target`, and `rel` attributes.
On native, it will use React Native `Linking.openURL`, on web it will render to an `a` element with `href` set appropriately.
## API Reference
### Anchor
Inherits [Tamagui props](/docs/intro/props) as well as:
<PropsTable
data={[
{
name: 'href',
required: false,
type: 'string',
description: `Location to link to.`,
},
{
name: 'target',
required: false,
type: 'string',
},
{
name: 'rel',
required: false,
type: 'string',
},
]}
/>
## components/anchor/2.0.0
---
title: Anchor
description: Link to external websites
name: anchor
component: Anchor
package: html
---
<Highlights
features={['Supports SSR.', 'Works on native and web.', 'Accepts Tamagui style props.']}
/>
## Installation
Anchor is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/html
```
## Usage
The Anchor component provides a way to link to external websites. It extends [SizableText](/docs/components/text#sizable-text), adding the `href`, `target`, and `rel` attributes.
On native, it uses React Native `Linking.openURL`. On web, it renders an `a` element with `href` set appropriately.
## API Reference
### Anchor
Inherits [Tamagui props](/docs/intro/props) as well as:
<PropsTable
data={[
{
name: 'href',
required: false,
type: 'string',
description: `The URL to link to.`,
},
{
name: 'target',
required: false,
type: 'string',
description: `Specifies where to open the linked document.`,
},
{
name: 'rel',
required: false,
type: 'string',
description: `Specifies the relationship between the current document and the linked document.`,
},
]}
/>
## components/avatar/1.0.0
---
title: Avatar
description: Display aspect-fixed images with a fallback while loading
name: avatar
component: Avatar
package: avatar
demoName: Avatar
---
<HeroContainer>
<AvatarDemo />
</HeroContainer>
```tsx hero template=Avatar
```
<Highlights
features={[
'Accepts size prop that stays in sync with other components.',
'Completely control styles on every element.',
'Automatically swaps fallback for image on load.',
]}
/>
## Installation
Avatar is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/avatar
```
## Usage
```tsx
import { Avatar } from 'tamagui'
export default () => (
<Avatar circular size="$6">
<Avatar.Image src="http://picsum.photos/200/300" />
<Avatar.Fallback bc="red" />
</Avatar>
)
```
## API Reference
### Avatar
Avatar extends [Square](/docs/components/shapes#shape-props), giving it all the [Tamagui standard props](/docs/intro/props) as well as `size` and `circular`.
### Avatar.Fallback
Avatar.Fallback extends [YStack](/docs/components/stacks), plus:
<PropsTable
data={[
{
name: 'delayMs',
required: false,
type: 'number',
description: `Milliseconds to wait before showing the fallback, to prevent flicker.`,
},
]}
/>
### Avatar.Image
Avatar.Image extends [Image](/docs/components/image).
## components/avatar/2.0.0
---
title: Avatar
description: Display aspect-ratio-fixed images with a fallback while loading
name: avatar
component: Avatar
package: avatar
demoName: Avatar
---
<HeroContainer>
<AvatarDemo />
</HeroContainer>
```tsx hero template=Avatar
```
<Highlights
features={[
'Accepts size prop that stays in sync with other components.',
'Completely control styles on every element.',
'Automatically swaps fallback for image on load.',
]}
/>
## Installation
Avatar is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/avatar
```
## Usage
```tsx
import { Avatar } from 'tamagui'
export default () => (
<Avatar circular size="$6">
<Avatar.Image src="http://picsum.photos/200/300" />
<Avatar.Fallback bc="red" />
</Avatar>
)
```
## API Reference
### Avatar
Avatar extends [Square](/docs/components/shapes#shape-props), giving it all the [Tamagui standard props](/docs/intro/props) as well as `size` and `circular`.
### Avatar.Fallback
Avatar.Fallback extends [YStack](/docs/components/stacks), plus:
<PropsTable
data={[
{
name: 'delayMs',
required: false,
type: 'number',
description: `Milliseconds to wait before showing the fallback, to prevent flicker.`,
},
]}
/>
### Avatar.Image
Avatar.Image extends [Image](/docs/components/image).
## components/button/1.0.0-alpha
---
title: Button
description: A simple button component
name: button
component: Button
demoName: Button
---
# Button
<Description>A simple, sizable button.</Description>
<HeroContainer>
<ButtonDemo />
</HeroContainer>
```tsx hero template=Button
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Can inverse theme with themeInverse.',
'Place an icon before or after.',
]}
/>
### Usage
```tsx
import { Button } from 'tamagui'
export default () => <Button>Lorem ipsum</Button>
```
### Sizing
Sizing buttons provides a unique challenge especially for a compiler, because
you need to adjust many different properties - not just on the outer frame, but
on the text wrapped inside. Tamagui supports adjusting the padding, border
radius, font size and icons sizes all in one with the `size` prop.
```tsx
import { Button } from 'tamagui'
export default () => <Button size="$6">Lorem ipsum</Button>
```
Given your theme defines a size `6`, the button will adjust all of the
properties appropriately. You can also pass a plain number to get an arbitrary
size.
### Icon Theming
You can pass icons as either elements or components. If passing components,
Tamagui will automatically pass the `size` and `color` prop to them based on
your theme.
### Button props
Button extends View, inheriting all the
[Tamagui standard props](/docs/intro/props), adding:
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'string | tokens.size',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'theme',
required: false,
type: 'string',
description: `Apply a theme just to the button and it's children`,
},
{
name: 'themeInverse',
required: false,
type: 'boolean',
description: `Helpful for "flipping" any theme between dark and light (including flipping a sub themes defined as [subtheme]-[dark/light]`,
},
{
name: 'textProps',
required: false,
type: 'TextProps',
description: `By default a button wraps contents in Text and will pass textProps if set.`,
},
{
name: 'noTextWrap',
required: false,
type: 'boolean',
description: `If true, Button won't wrap content with a Text element.`,
},
{
name: 'icon',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears before the text.`,
},
{
name: 'iconAfter',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears after the text.`,
},
]}
/>
## components/button/1.0.0-beta.0
---
title: Button
description: A simple button component
name: button
component: Button
demoName: Button
---
# Button
<Description>A simple, sizable button.</Description>
<HeroContainer demoMultiple>
<ButtonDemo />
</HeroContainer>
```tsx hero template=Button
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Can inverse theme with themeInverse.',
'Place an icon before or after.',
]}
/>
### Usage
```tsx
import { Button } from 'tamagui'
export default () => <Button>Lorem ipsum</Button>
```
### Sizing
Sizing buttons provides a unique challenge especially for a compiler, because
you need to adjust many different properties - not just on the outer frame, but
on the text wrapped inside. Tamagui supports adjusting the padding, border
radius, font size and icons sizes all in one with the `size` prop.
```tsx
import { Button } from 'tamagui'
export default () => <Button size="$6">Lorem ipsum</Button>
```
Given your theme defines a size `6`, the button will adjust all of the
properties appropriately. You can also pass a plain number to get an arbitrary
size.
### Icon Theming
You can pass icons as either elements or components. If passing components,
Tamagui will automatically pass the `size` and `color` prop to them based on
your theme.
### Button props
Button extends View, inheriting all the
[Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'string | tokens.size',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'theme',
required: false,
type: 'string',
description: `Apply a theme just to the button and it's children`,
},
{
name: 'themeInverse',
required: false,
type: 'boolean',
description: `Helpful for "flipping" any theme between dark and light (including flipping a sub themes defined as [subtheme]-[dark/light]`,
},
{
name: 'noTextWrap',
required: false,
type: 'boolean',
description: `If true, Button won't wrap content with a Text element.`,
},
{
name: 'icon',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears before the text.`,
},
{
name: 'iconAfter',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears after the text.`,
},
{
name: 'scaleIcon',
required: false,
type: 'number',
description: `Scale the icon more than usual by this number.`,
},
{
name: 'scaleSpace',
required: false,
type: 'number',
description: `Scale the spacing more than usual by this number.`,
},
{
name: 'spaceFlex',
required: false,
type: `boolean`,
description: `Makes all space elements have a flex.`,
},
{
name: 'color',
required: false,
type: `SizableTextProps['color']`,
description: `Passes "color" down to the inner text component`,
},
{
name: 'fontWeight',
required: false,
type: `SizableTextProps['fontWeight']`,
description: `Passes "fontWeight" down to the inner text component`,
},
{
name: 'letterSpacing',
required: false,
type: `SizableTextProps['letterSpacing']`,
description: `Passes "letterSpacing" down to the inner text component`,
},
{
name: 'textAlign',
required: false,
type: `SizableTextProps['textAlign']`,
description: `Passes "textAlign" down to the inner text component`,
},
]}
/>
## components/button/1.0.0
---
title: Button
description: A simple button component
name: button
component: Button
package: button
demoName: Button
---
# Button
<Description>A simple, sizable button.</Description>
<HeroContainer demoMultiple>
<ButtonDemo />
</HeroContainer>
```tsx hero template=Button
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Can inverse theme with themeInverse.',
'Place an icon before or after.',
]}
/>
### Usage
```tsx
import { Button } from 'tamagui'
export default () => <Button>Lorem ipsum</Button>
```
### Sizing
Sizing buttons provides a unique challenge especially for a compiler, because you need to adjust many different properties - not just on the outer frame, but on the text wrapped inside. Tamagui supports adjusting the padding, border radius, font size and icons sizes all in one with the `size` prop.
```tsx
import { Button } from 'tamagui'
export default () => <Button size="$6">Lorem ipsum</Button>
```
Given your theme defines a size `6`, the button will adjust all of the properties appropriately. You can also pass a plain number to get an arbitrary size.
### Icon Theming
You can pass icons as either elements or components. If passing components, Tamagui will automatically pass the `size` and `color` prop to them based on your theme.
You can [use the source of Button itself](https://github.com/tamagui/tamagui/blob/v2/code/ui/button/src/Button.tsx) to see in more detail what variants you can override, and how we use this pattern internally to create our Button component.
### Customization (Advanced)
Button only supports a limited subset of text props directly, and doesn't accept `hoverStyle` text props. If you need more control, you can do a simple customization using some exported helpers.
Please note that this pattern is a bit antithetical to the multiple-components APIs that Tamagui generally prefers. In a future release we hope to fix this, but that change should be easy to migrate to.
```tsx
import { forwardRef } from 'react'
import {
ButtonFrame,
ButtonText,
GetProps,
ButtonProps as TamaguiButtonProps,
styled,
themeable,
useButton,
} from 'tamagui'
const CustomButtonFrame = styled(ButtonFrame, {
// ...
})
const CustomButtonText = styled(ButtonText, {
// ...
})
// to capture the custom variant types you define
type CustomButtonFrameProps = GetProps<typeof CustomButtonFrame>
type CustomButtonTextProps = GetProps<typeof CustomButtonText>
export type CustomButtonProps = TamaguiButtonProps &
CustomButtonFrameProps &
CustomButtonTextProps
export const Button = CustomButtonFrame.styleable<CustomButtonProps>((propsIn, ref) => {
const { props } = useButton(propsIn, { Text: CustomButtonText })
return <CustomButtonFrame {...props} ref={ref} />
})
```
### Button props
Buttons extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'string | tokens.size',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'theme',
required: false,
type: 'string',
description: `Apply a theme just to the button and it's children`,
},
{
name: 'themeInverse',
required: false,
type: 'boolean',
description: `Helpful for "flipping" any theme between dark and light (including flipping a sub themes defined as [subtheme]-[dark/light]`,
},
{
name: 'noTextWrap',
required: false,
type: 'boolean',
description: `If true, Button won't wrap content with a Text element.`,
},
{
name: 'icon',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears before the text.`,
},
{
name: 'iconAfter',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears after the text.`,
},
{
name: 'scaleIcon',
required: false,
type: 'number',
description: `Scale the icon more than usual by this number.`,
},
{
name: 'scaleSpace',
required: false,
type: 'number',
description: `Scale the spacing more than usual by this number.`,
},
{
name: 'spaceFlex',
required: false,
type: `boolean`,
description: `Makes all space elements have a flex.`,
},
{
name: 'color',
required: false,
type: `SizableTextProps['color']`,
description: `Passes "color" down to the inner text component`,
},
{
name: 'fontWeight',
required: false,
type: `SizableTextProps['fontWeight']`,
description: `Passes "fontWeight" down to the inner text component`,
},
{
name: 'letterSpacing',
required: false,
type: `SizableTextProps['letterSpacing']`,
description: `Passes "letterSpacing" down to the inner text component`,
},
{
name: 'textAlign',
required: false,
type: `SizableTextProps['textAlign']`,
description: `Passes "textAlign" down to the inner text component`,
},
{
name: 'circular',
required: false,
type: `boolean`,
description: `Forces a circular button.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
## components/button/1.28.0
---
title: Button
description: An incredibly flexible button.
name: button
component: Button
package: button
demoName: Button
---
<HeroContainer demoMultiple>
<ButtonDemo />
</HeroContainer>
```tsx hero template=Button
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Can inverse theme with themeInverse.',
'Place an icon before or after.',
]}
/>
## Usage
When using the simple Button API, it's as simple as this:
```tsx
import { Button } from 'tamagui'
export default () => <Button>Lorem ipsum</Button>
```
### Sizing
Sizing buttons provides a unique challenge especially for a compiler, because you need to adjust many different properties - not just on the outer frame, but on the text wrapped inside. Tamagui supports adjusting the padding, border radius, font size and icons sizes all in one with the `size` prop.
```tsx
import { Button } from 'tamagui'
export default () => <Button size="$6">Lorem ipsum</Button>
```
Given your theme defines a size `6`, the button will adjust all of the properties appropriately. You can also pass a plain number to get an arbitrary size.
### Icon Theming
You can pass icons as either elements or components. If passing components, Tamagui will automatically pass the `size` and `color` prop to them based on your theme.
You can [use the source of Button itself](https://github.com/tamagui/tamagui/blob/main/code/ui/button/src/Button.tsx) to see in more detail what variants you can override, and how we use this pattern internally to create our Button component.
### Creating your own Button
Tamagui now has all the features necessary to make creating a custom Button easy enough that you may want to roll your own button. Learn how to do it with our dedicated guide, [How to Build a Button](/docs/guides/how-to-build-a-button).
<Notice>
The previous `useButton` API is deprecated and will be removed in a future version. It's
brittle and is easily replaced with the new compound component APIs as described in the
guide.
</Notice>
## API Reference
### Button
Buttons extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'string | tokens.size',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'theme',
required: false,
type: 'string',
description: `Apply a theme just to the button and it's children`,
},
{
name: 'themeInverse',
required: false,
type: 'boolean',
description: `Helpful for "flipping" any theme between dark and light (including flipping a sub themes defined as [subtheme]-[dark/light]`,
},
{
name: 'noTextWrap',
required: false,
type: 'boolean',
description: `If true, Button won't wrap content with a Text element.`,
},
{
name: 'icon',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears before the text.`,
},
{
name: 'iconAfter',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears after the text.`,
},
{
name: 'scaleIcon',
required: false,
type: 'number',
description: `Scale the icon more than usual by this number.`,
},
{
name: 'scaleSpace',
required: false,
type: 'number',
description: `Scale the spacing more than usual by this number.`,
},
{
name: 'spaceFlex',
required: false,
type: `boolean`,
description: `Makes all space elements have a flex.`,
},
{
name: 'color',
required: false,
type: `SizableTextProps['color']`,
description: `Passes "color" down to the inner text component`,
},
{
name: 'fontWeight',
required: false,
type: `SizableTextProps['fontWeight']`,
description: `Passes "fontWeight" down to the inner text component`,
},
{
name: 'letterSpacing',
required: false,
type: `SizableTextProps['letterSpacing']`,
description: `Passes "letterSpacing" down to the inner text component`,
},
{
name: 'textAlign',
required: false,
type: `SizableTextProps['textAlign']`,
description: `Passes "textAlign" down to the inner text component`,
},
{
name: 'circular',
required: false,
type: `boolean`,
description: `Forces a circular button.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
## components/button/2.0.0
---
title: Button
description: A simple button component
name: button
component: Button
package: button
demoName: Button
---
<HeroContainer demoMultiple>
<ButtonDemo />
</HeroContainer>
```tsx hero template=Button
```
<Highlights
features={[
'Size prop that works on all styles.',
'Place an icon before or after.',
'Control icon size explicitly with `iconSize`.',
'Theme groups of buttons with `Button.Apply`.',
]}
/>
## Installation
Button is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/button
```
## Usage
```tsx
import { Button } from 'tamagui'
export default () => <Button>Lorem ipsum</Button>
```
## Sizing
Sizing buttons provides a unique challenge especially for a compiler, because
you need to adjust many different properties - not just on the outer frame, but
on the text wrapped inside. Tamagui supports adjusting the padding, border
radius, font size and icons sizes all in one with the `size` prop.
```tsx
import { Button } from 'tamagui'
export default () => <Button size="$6">Lorem ipsum</Button>
```
Given your theme defines a size `6`, the button will adjust all of the
properties appropriately. You can also pass a plain number to get an arbitrary
size.
## Variants
The Button component supports different visual styles through the `variant`
prop. Currently, the primary available variant is `"outlined"`.
```tsx
import { Button, XStack } from 'tamagui'
export default () => (
<XStack gap="$2">
<Button>Default</Button>
<Button variant="outlined">Outlined</Button>
</XStack>
)
```
When `variant="outlined"` is applied, the button typically has a transparent
background with a visible border. The exact appearance (border color,
hover/press states) is determined by your theme's definitions for an outlined
button.
## Icon Theming
You can pass icons as either elements or components. If passing components,
Tamagui will automatically theme them (passing `size`). The icon size is
determined by the Button's `size` prop by default.
You can also explicitly set the icon size using the `iconSize` prop, which
accepts a `SizeTokens` value (e.g., `"$2"`). The `scaleIcon` prop can be used to
further adjust the size relative to the determined or explicitly set icon size.
When an icon is present, Tamagui automatically adds spacing between the icon and
the button's text using the CSS `gap` property on the Button frame. The gap is
derived from the Button's `size` token value, keeping spacing consistent with
the overall button dimensions.
```tsx
import { Button, Star } from 'tamagui'
export default () => (
<>
<Button icon={Star} size="$5">
Icon sized by Button
</Button>
<Button icon={Star} iconSize="$2" size="$5">
Icon sized explicitly
</Button>
</>
)
```
You can
[use the source of Button itself](https://github.com/tamagui/tamagui/blob/v2/code/ui/button/src/Button.tsx)
to see in more detail what variants you can override, and how we use this
pattern internally to create our Button component.
## Group Theming
You can use `Button.Apply` to theme a group of Buttons using a shared context.
This is useful for applying consistent sizing or variants to multiple buttons
without passing props to each one individually.
```tsx
import { Button, ButtonDemo, YStack } from 'tamagui'
export default () => (
<YStack gap="$4">
<Button.Apply size="$2" variant="outlined">
<Button>Small Outlined 1</Button>
<Button>Small Outlined 2</Button>
<Button icon={Star}>With Icon</Button>
</Button.Apply>
<Button.Apply size="$5">
<Button>Large 1</Button>
<Button theme="blue">Large Themed</Button>
</Button.Apply>
</YStack>
)
```
## Web Form Props
Button supports all standard HTML `<button>` attributes for form integration.
These props are web-only and ignored on native:
```tsx
import { Button, Form } from 'tamagui'
export default () => (
<Form action="/submit">
<Button type="submit">Submit Form</Button>
<Button type="reset">Reset</Button>
<Button type="button">Regular Button (default)</Button>
{/* Override form attributes */}
<Button type="submit" formAction="/alternative-endpoint" formMethod="post">
Submit to Different Endpoint
</Button>
</Form>
)
```
<Notice>
Button defaults to `type="button"` to prevent unintended form submissions. Use
`type="submit"` explicitly when you want form submission behavior.
</Notice>
## Text Styling
**Changed from v1:** In v1, you could pass text style props like `color`,
`fontSize`, `fontWeight`, `fontFamily`, and `letterSpacing` directly to
`<Button>` and they would be forwarded to the button's text via context. In v2,
this is no longer supported — Button's frame is a `View` and text style props
passed to it are ignored.
To customize text appearance, use `Button.Text` explicitly:
```tsx
import { Button } from 'tamagui'
export default () => (
<Button>
<Button.Text color="blue" fontSize="$5" fontWeight="bold">
Styled text
</Button.Text>
</Button>
)
```
### Interactive Text Styles
Combine `Button.Text` with the `group` prop on Button to coordinate text
styles across hover, press, and focus states:
```tsx
import { Button } from 'tamagui'
export default () => (
<Button group="btn" bg="pink">
<Button.Text color="blue" $group-btn-hover={{ color: 'red' }}>
Hello world
</Button.Text>
</Button>
)
```
The `group` prop lets any child reference interactive states like
`$group-btn-hover`, `$group-btn-press`, and `$group-btn-focus` for
precise styling per interaction state.
## Creating Your Own Button
Tamagui now has all the features necessary to make creating a custom Button easy
enough that you may want to roll your own button. Learn how to do it with our
dedicated guide, [How to Build a Button](/docs/guides/how-to-build-a-button).
## API Reference
Buttons extend Stack views inheriting all the
[Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'SizeTokens | number',
description: `Set a size from your theme's size tokens (e.g., "$4") or a specific number. Adjusts padding, font size, and icon sizes.`,
},
{
name: 'variant',
required: false,
type: '"outlined" | undefined',
description: 'Applies the "outlined" button style.',
},
{
name: 'elevation',
required: false,
type: 'SizeTokens | number',
description: `Adds a shadow/elevation effect to the button. Accepts a size token or number.`,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description:
'If true, the button will be non-interactive and visually disabled. Sets `pointerEvents: "none"`.',
},
{
name: 'theme',
required: false,
type: 'string',
description: `Apply a theme just to the button and its children.`,
},
{
name: 'icon',
required: false,
type: 'React.ReactNode | React.ComponentType<{ color?: string, size?: number }>',
description: `Pass any React element or a component. Appears before the text. If a component is passed, 'color' and 'size' props are passed to it.`,
},
{
name: 'iconAfter',
required: false,
type: 'React.ReactNode | React.ComponentType<{ color?: string, size?: number }>',
description: `Pass any React element or a component. Appears after the text. If a component is passed, 'color' and 'size' props are passed to it.`,
},
{
name: 'iconSize',
required: false,
type: 'SizeTokens',
description: `Explicitly set the size of the icon, overriding the default size derived from the Button's 'size' prop. Uses theme size tokens (e.g., "$2").`,
},
{
name: 'scaleIcon',
required: false,
type: 'number',
description: `Scale the icon by this factor. Default is 1. Applied after 'iconSize' or the default size calculation.`,
},
{
name: 'circular',
required: false,
type: `boolean`,
description: `Forces a circular button. Applies border radius to make the button a circle.`,
},
{
name: 'chromeless',
required: false,
type: 'boolean | "all"',
description:
'Removes default Tamagui visual styles (background, border) for a more minimal appearance, while keeping layout and interactions. If "all", removes hover/press/focus styles too.',
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all Tamagui default styles, including layout, interactions, and accessibility attributes. Prefer 'chromeless' for removing only visual styles.`,
},
// Web-only form props
{
name: '// Web Form Props',
description:
'The following props are web-only and provide full HTML button form semantics. They are ignored on native.',
type: '---',
},
{
name: 'type',
required: false,
type: '"button" | "submit" | "reset"',
description:
'Web-only. The button type. Defaults to "button" to prevent unintended form submissions.',
},
{
name: 'form',
required: false,
type: 'string',
description: 'Web-only. The ID of the form element the button is associated with.',
},
{
name: 'formAction',
required: false,
type: 'string',
description: "Web-only. URL for form submission, overrides the form's action.",
},
{
name: 'formMethod',
required: false,
type: '"get" | "post"',
description: 'Web-only. HTTP method for form submission.',
},
{
name: 'formEncType',
required: false,
type: 'string',
description: 'Web-only. Encoding type for form data.',
},
{
name: 'formNoValidate',
required: false,
type: 'boolean',
description: 'Web-only. Bypass form validation when submitting.',
},
{
name: 'formTarget',
required: false,
type: 'string',
description: 'Web-only. Where to display form response (_self, _blank, etc).',
},
{
name: 'name',
required: false,
type: 'string',
description: 'Web-only. Name submitted with form data.',
},
{
name: 'value',
required: false,
type: 'string',
description: 'Web-only. Value submitted with form data.',
},
{
name: 'popoverTarget',
required: false,
type: 'string',
description: 'Web-only. ID of popover element to control.',
},
{
name: 'popoverTargetAction',
required: false,
type: '"hide" | "show" | "toggle"',
description: 'Web-only. Action to perform on the popover.',
},
// Note about text styling
{
name: '// Text Styling',
description:
'Button does not support `color` directly. To style text color, use `Button.Text` with the `group` prop for interactive states. See the Text Styling section above.',
type: '---',
},
]}
/>
## components/card/1.0.0
---
title: Card
description: Sizable, themeable cards with a flexible API.
name: card
component: Card
package: card
demoName: Card
---
<HeroContainer>
<CardDemo />
</HeroContainer>
```tsx hero template=Card
```
<Highlights
features={[
`Sizable with a size prop that passes to Card children.`,
`Themeable helper props like elevate.`,
`Background component that handles positioning.`,
]}
/>
## Installation
Card is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/card
```
## Anatomy
```tsx
import { Card } from 'tamagui' // or '@tamagui/card'
export default () => (
<Card>
<Card.Header />
<Card.Footer />
{/* any other components */}
<Card.Background />
</Card>
)
```
## API Reference
### Card
Frame of the card, extends [ThemeableStack props](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down to all sub-components when set for padding, arrow, borderRadius.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Card.Header
Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Card.Footer
Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Card.Background
Extends [YStack](/docs/components/stacks), set to `fullscreen` and zIndex below Header/Footer.
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
## components/card/2.0.0
---
title: Card
description: Sizable, themeable cards with a flexible API
name: card
component: Card
package: card
demoName: Card
---
<HeroContainer>
<CardDemo />
</HeroContainer>
```tsx hero template=Card
```
<Highlights
features={[
`Sizable with a size prop that passes to Card children.`,
`Themeable helper props like elevate.`,
`Background component that handles positioning.`,
]}
/>
## Installation
Card is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/card
```
## Anatomy
```tsx
import { Card } from 'tamagui' // or '@tamagui/card'
export default () => (
<Card>
<Card.Header />
<Card.Footer />
{/* any other components */}
<Card.Background />
</Card>
)
```
## API Reference
### Card
Frame of the card, extends [ThemeableStack props](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down to all sub-components when set for padding, arrow, borderRadius.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Card.Header
Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Card.Footer
Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Card.Background
Extends [YStack](/docs/components/stacks), set to `fullscreen` and `zIndex` below Header/Footer.
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
## components/checkbox/1.3.0
---
title: Checkbox
description: A simple checkbox component
name: checkbox
component: Checkbox
package: checkbox
demoName: Checkbox
---
# Checkbox
<Description>Use in forms to toggle between two states.</Description>
<HeroContainer>
<CheckboxDemo />
</HeroContainer>
```tsx hero template=Checkbox
```
<Highlights
features={[
`Supports indeterminate state.`,
`Accessible, easy to compose and customize.`,
`Sizable & works controlled or uncontrolled.`,
`Ability to opt-out to native checkbox on web.`,
]}
/>
## Installation
Checkbox is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/checkbox
```
## Usage
```tsx
import { Check } from '@tamagui/lucide-icons-2'
import { Checkbox } from 'tamagui'
export default () => (
<Checkbox size="$4">
<Checkbox.Indicator>
<Check />
</Checkbox.Indicator>
</Checkbox>
)
```
## API Reference
### Checkbox
`Checkbox` extend ThemeableStack inheriting all the [props](/docs/components/stacks#themeablestack), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native checkbox input on web.`,
default: false,
},
{
name: 'onCheckedChange',
type: '(checked: boolean | "indeterminate") => void',
description: 'Callback that fires when the checkbox state is changed.',
},
{
name: 'sizeAdjust',
type: 'number',
description: `Adjust the checkbox size scaling by this number.`,
},
{
name: 'scaleIcon',
type: 'number',
description: `Scale the indicator icon more than usual by this number.`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `The Tamagui size tokens should map to the height of a button at any given step. This means you want somewhat smaller checkboxes typically.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Checkbox.Indicator
`Checkbox.Indicator` extend ThemeableStack inheriting all the [props](/docs/components/stacks#themeablestack), plus:
<PropsTable
data={[
{
name: 'forceMount',
required: false,
type: `boolean`,
description: `Used to force mounting when more control is needed.`,
},
{
name: 'disablePassStyles',
required: false,
type: 'boolean',
description: `Used to disable passing styles down to children.`,
},
]}
/>
## components/checkbox/1.85.0
---
title: Checkbox
description: A simple checkbox component
name: checkbox
component: Checkbox
package: checkbox
demoName: Checkbox
---
# Checkbox
<Description>Toggle state in forms.</Description>
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Tamagui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer showAnimationDriverControl>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<CheckboxDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<CheckboxUnstyledDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<CheckboxHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=Checkbox
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=CheckboxUnstyled
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=CheckboxHeadless
````
</Tabs.Content>
<Highlights
features={[
`Supports indeterminate state.`,
`Accessible, easy to compose and customize.`,
`Sizable & works controlled or uncontrolled.`,
`Ability to opt-out to native checkbox on web.`,
]}
/>
## Installation
<Tabs.Content value="styled">
Checkbox is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/checkbox
````
</Tabs.Content>
<Tabs.Content value="unstyled">
Checkbox is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/checkbox
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless switch, you want to import it from the
`@tamagui/switch-headless` package. This package has no dependency on
`@tamagui/core`, but still works off the react-native APIs.
This means can bring your own style library.
```bash
npm install @tamagui/switch-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { Check } from '@tamagui/lucide-icons-2'
import { Checkbox } from 'tamagui'
export default () => (
<Checkbox size="$4">
<Checkbox.Indicator>
<Check />
</Checkbox.Indicator>
</Checkbox>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Use the `createCheckbox` export to create a fully custom checkbox that still
uses the Tamagui styling system. This is similar to setting `unstyled`, but goes
a bit further. It doesn't add any types for `size` or `unstyled`, and it won't
automatically apply the `active` theme. If does pass the `checked` prop down as
indicated in the types of `createCheckbox`.
```tsx template=CheckboxUnstyled
```
</Tabs.Content>
<Tabs.Content value="headless">
Using the `useCheckbox` API, you can make your own Checkbox from scratch.
```tsx template=CheckboxHeadless
```
</Tabs.Content>
## API Reference
### Checkbox
`Checkbox` extend ThemeableStack inheriting all the
[props](/docs/components/stacks#themeablestack), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native checkbox input on web.`,
default: false,
},
{
name: 'onCheckedChange',
type: '(checked: boolean | "indeterminate") => void',
description: 'Callback that fires when the checkbox state is changed.',
},
{
name: 'sizeAdjust',
type: 'number',
description: `Adjust the checkbox size scaling by this number.`,
},
{
name: 'scaleIcon',
type: 'number',
description: `Scale the indicator icon more than usual by this number.`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `The Tamagui size tokens should map to the height of a button at any given step. This means you want somewhat smaller checkboxes typically.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Checkbox.Indicator
`Checkbox.Indicator` extend ThemeableStack inheriting all the
[props](/docs/components/stacks#themeablestack), plus:
<PropsTable
data={[
{
name: 'forceMount',
required: false,
type: `boolean`,
description: `Used to force mounting when more control is needed.`,
},
{
name: 'disablePassStyles',
required: false,
type: 'boolean',
description: `Used to disable passing styles down to children.`,
},
]}
/>
</Tabs>
## components/checkbox/1.89.0
---
title: Checkbox
description: Toggle state in forms.
name: checkbox
component: Checkbox
package: checkbox
demoName: Checkbox
---
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Tamagui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer showAnimationDriverControl>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<CheckboxDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<CheckboxUnstyledDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<CheckboxHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=Checkbox
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=CheckboxUnstyled
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=CheckboxHeadless
````
</Tabs.Content>
<Highlights
features={[
`Supports indeterminate state.`,
`Accessible, easy to compose and customize.`,
`Sizable & works controlled or uncontrolled.`,
`Ability to opt-out to native checkbox on web.`,
]}
/>
## Installation
<Tabs.Content value="styled">
Checkbox is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/checkbox
````
</Tabs.Content>
<Tabs.Content value="unstyled">
Checkbox is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/checkbox
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless checkbox, you want to import it from the
`@tamagui/checkbox-headless` package. This package has no dependency on
`@tamagui/core`, but still works off the react-native APIs.
This means you can bring your own style library.
```bash
npm install @tamagui/checkbox-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { Check } from '@tamagui/lucide-icons-2'
import { Checkbox } from 'tamagui'
export default () => (
<Checkbox size="$4">
<Checkbox.Indicator>
<Check />
</Checkbox.Indicator>
</Checkbox>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Use the `createCheckbox` export to create a fully custom checkbox that still
uses the Tamagui styling system. This is similar to setting `unstyled`, but goes
a bit further. It doesn't add any types for `size` or `unstyled`, and it won't
automatically apply the `active` theme. It does pass the `checked` prop down as
indicated in the types of `createCheckbox`.
```tsx template=CheckboxUnstyled
```
</Tabs.Content>
<Tabs.Content value="headless">
Using the `useCheckbox` API, you can make your own Checkbox from scratch.
```tsx template=CheckboxHeadless
```
</Tabs.Content>
## API Reference
### Checkbox
`Checkbox` extend ThemeableStack inheriting all the
[props](/docs/components/stacks#themeablestack), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native checkbox input on web.`,
default: false,
},
{
name: 'onCheckedChange',
type: '(checked: boolean | "indeterminate") => void',
description: 'Callback that fires when the checkbox state is changed.',
},
{
name: 'sizeAdjust',
type: 'number',
description: `Adjust the checkbox size scaling by this number.`,
},
{
name: 'scaleIcon',
type: 'number',
description: `Scale the indicator icon more than usual by this number.`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `The Tamagui size tokens should map to the height of a button at any given step. This means you want somewhat smaller checkboxes typically.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Checkbox.Indicator
`Checkbox.Indicator` extend ThemeableStack inheriting all the
[props](/docs/components/stacks#themeablestack), plus:
<PropsTable
data={[
{
name: 'forceMount',
required: false,
type: `boolean`,
description: `Used to force mounting when more control is needed.`,
},
{
name: 'disablePassStyles',
required: false,
type: 'boolean',
description: `Used to disable passing styles down to children.`,
},
]}
/>
</Tabs>
## components/checkbox/2.0.0
---
title: Checkbox
description: Toggle state in forms
name: checkbox
component: Checkbox
package: checkbox
demoName: Checkbox
---
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Tamagui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer showAnimationDriverControl>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<CheckboxDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<CheckboxUnstyledDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<CheckboxHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=Checkbox
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=CheckboxUnstyled
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=CheckboxHeadless
````
</Tabs.Content>
<Highlights
features={[
`Supports indeterminate state.`,
`Accessible, easy to compose and customize.`,
`Sizable & works controlled or uncontrolled.`,
`Ability to opt-out to native checkbox on web.`,
]}
/>
## Installation
<Tabs.Content value="styled">
Checkbox is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/checkbox
````
</Tabs.Content>
<Tabs.Content value="unstyled">
Checkbox is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/checkbox
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless checkbox, you want to import it from the
`@tamagui/checkbox-headless` package. This package has no dependency on
`@tamagui/core`, but still works off the react-native APIs.
This means you can bring your own style library.
```bash
npm install @tamagui/checkbox-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { Check } from '@tamagui/lucide-icons-2'
import { Checkbox } from 'tamagui'
export default () => (
<Checkbox size="$4">
<Checkbox.Indicator>
<Check />
</Checkbox.Indicator>
</Checkbox>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Use the `createCheckbox` export to create a fully custom checkbox that still
uses the Tamagui styling system. This is similar to setting `unstyled`, but goes
a bit further. It doesn't add any types for `size` or `unstyled`, and it won't
automatically apply the `active` theme. It does pass the `checked` prop down as
indicated in the types of `createCheckbox`.
```tsx template=CheckboxUnstyled
```
</Tabs.Content>
<Tabs.Content value="headless">
The `useCheckbox` hook provides all the state and accessibility props needed to build a custom checkbox with any styling solution.
```tsx template=CheckboxHeadless
```
### Basic Usage
```tsx
import { useCheckbox } from '@tamagui/checkbox-headless'
import { useState } from 'react'
import { Pressable, View } from 'react-native'
function MyCheckbox({ defaultChecked, onCheckedChange, ...props }) {
const [checked, setChecked] = useState(defaultChecked || false)
const { checkboxProps, checkboxRef, bubbleInput } = useCheckbox(
props,
[checked, setChecked],
null
)
return (
<>
<Pressable
ref={checkboxRef}
{...checkboxProps}
style={{
width: 24,
height: 24,
borderRadius: 4,
borderWidth: 2,
borderColor: checked ? '#3b82f6' : '#d1d5db',
backgroundColor: checked ? '#3b82f6' : 'transparent',
justifyContent: 'center',
alignItems: 'center',
}}
>
{checked && <View style={{ width: 12, height: 12, backgroundColor: 'white' }} />}
</Pressable>
{bubbleInput}
</>
)
}
```
</Tabs.Content>
## API Reference
### Checkbox
`Checkbox` extends ThemeableStack inheriting all the
[props](/docs/components/stacks#themeablestack), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native checkbox input on web.`,
default: false,
},
{
name: 'onCheckedChange',
type: '(checked: boolean | "indeterminate") => void',
description: 'Callback that fires when the checkbox state is changed.',
},
{
name: 'sizeAdjust',
type: 'number',
description: `Adjust the checkbox size scaling by this number.`,
},
{
name: 'scaleIcon',
type: 'number',
description: `Scale the indicator icon more than usual by this number.`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `The Tamagui size tokens should map to the height of a button at any given step. This means you want somewhat smaller checkboxes typically.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
{
name: 'activeStyle',
required: false,
type: `StyleProp`,
description: `Styles to apply when the checkbox is checked.`,
},
{
name: 'activeTheme',
required: false,
type: `string | null`,
description: `Theme to apply when the checkbox is checked. Set to null for no theme change.`,
},
]}
/>
### Checkbox.Indicator
`Checkbox.Indicator` extends ThemeableStack inheriting all the
[props](/docs/components/stacks#themeablestack), plus:
<PropsTable
data={[
{
name: 'forceMount',
required: false,
type: `boolean`,
description: `Used to force mounting when more control is needed.`,
},
{
name: 'disablePassStyles',
required: false,
type: 'boolean',
description: `Used to disable passing styles down to children.`,
},
]}
/>
<Tabs.Content value="headless">
### useCheckbox
The `useCheckbox` hook accepts three arguments:
```tsx
const { checkboxProps, checkboxRef, bubbleInput } = useCheckbox(
props, // CheckboxProps
state, // [checked: CheckedState, setChecked: (checked: CheckedState) => void]
ref // React.Ref
)
```
#### CheckedState
The checkbox supports three states:
- `true` - checked
- `false` - unchecked
- `'indeterminate'` - indeterminate/mixed state (useful for "select all" patterns)
#### Props (first argument)
<PropsTable
data={[
{
name: 'labelledBy',
type: 'string',
description: `Set aria-labelledby for accessibility.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Whether the checkbox is disabled.`,
},
{
name: 'name',
type: 'string',
description: `Form input name for the hidden input.`,
},
{
name: 'value',
type: 'string',
default: '"on"',
description: `Form input value.`,
},
{
name: 'required',
type: 'boolean',
description: `Whether the checkbox is required in a form.`,
},
{
name: 'onCheckedChange',
type: '(checked: CheckedState) => void',
description: `Called when checked state changes.`,
},
{
name: 'onPress',
type: '(event) => void',
description: `Called when checkbox is pressed (composed with internal handler).`,
},
]}
/>
#### State (second argument)
A tuple of `[checked, setChecked]` where:
- `checked`: Current state (`boolean | 'indeterminate'`)
- `setChecked`: React state setter function
#### Return Value
| Property | Type | Description |
| --------------- | ------------------- | ---------------------------------------------------------------------------- |
| `checkboxProps` | `object` | Props to spread on your checkbox element (role, aria-checked, onPress, etc.) |
| `checkboxRef` | `Ref` | Composed ref to attach to your checkbox element |
| `bubbleInput` | `ReactNode \| null` | Hidden input for form compatibility (render as sibling, web only) |
</Tabs.Content>
</Tabs>
## components/context-menu/2.0.0
---
title: Context Menu
description: A menu component triggered by right-click on web and long press on touch devices
name: context-menu
component: ContextMenu
package: context-menu
demoName: ContextMenu
---
<HeroContainer>
<ContextMenuDemo />
</HeroContainer>
```tsx hero template=ContextMenu
```
<Highlights
features={[
'Full keyboard navigation.',
'Supports items, icons, images, checkboxes, groups, and more.',
'Supports submenus.',
'Supports modal and non-modal modes.',
'Supports native menus on native platforms.',
'Customizable alignment, offsets, and positioning.',
'Support for native iOS and Android icons.',
'Supports previews on iOS.',
]}
/>
ContextMenu displays a menu triggered by right-click on web or long-press on mobile. It supports submenus, native platform menus with iOS previews, and [automatically stacks](/ui/z-index) above other content.
## Installation
ContextMenu is already installed in `tamagui`, or you can install it independently:
```bash
yarn add @tamagui/context-menu
```
If you want to use native menus, add these dependencies:
```sh
yarn add @react-native-menu/menu
yarn add react-native-ios-context-menu
yarn add react-native-ios-utilities
yarn add zeego
yarn add sf-symbols-typescript
```
Then add the setup import at your app entry point:
```tsx
import '@tamagui/native/setup-zeego'
```
<Notice theme="blue">
On native, `ContextMenu.Trigger` coordinates with parent Tamagui pressables
automatically. You should not need extra wrapper views or empty `onPress`
handlers just to prevent a parent `pressStyle` from activating.
</Notice>
<Notice theme="yellow">
**Expo Router users:** This import must run _before_ `expo-router/entry`. Create an `index.js` at your project root that imports the setup first, then expo-router, and update your `package.json` main field to `"index.js"`. See the [upgrade guide](/docs/guides/how-to-upgrade#expo-router-entry-point) for details.
</Notice>
## Anatomy
Import all parts and piece them together.
```jsx
import { ContextMenu } from 'tamagui' // or '@tamagui/context-menu'
export default () => (
<ContextMenu>
<ContextMenu.Trigger asChild>
<YStack>
<Text>Right Click or longPress</Text>
</YStack>
</ContextMenu.Trigger>
<ContextMenu.Portal zIndex={100}>
<ContextMenu.Content>
<ContextMenu.Item>
<ContextMenu.ItemTitle>About Notes</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item>
<ContextMenu.ItemTitle>Settings</ContextMenu.ItemTitle>
</ContextMenu.Item>
{/* when title is nested inside a React element then you need to use `textValue` */}
<ContextMenu.Item textValue="Calendar">
<ContextMenu.ItemTitle>
<Text>Calendar</Text>
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon>
<Calendar color="gray" size="$1" />
</ContextMenu.ItemIcon>
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Sub>
<ContextMenu.SubTrigger>
<ContextMenu.ItemTitle>Actions</ContextMenu.ItemTitle>
</ContextMenu.SubTrigger>
<ContextMenu.Portal zIndex={200}>
<ContextMenu.SubContent>
<ContextMenu.Label fontSize={'$1'}>Note settings</ContextMenu.Label>
<ContextMenu.Item onSelect={onSelect} key="create-note">
<ContextMenu.ItemTitle>Create note</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item onSelect={onSelect} key="delete-all">
<ContextMenu.ItemTitle>Delete all notes</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item onSelect={onSelect} key="sync-all">
<ContextMenu.ItemTitle>Sync notes</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Portal>
</ContextMenu.Sub>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
)
```
## API Reference
### ContextMenu
Contains every component for the ContextMenu.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'Menu parts: Trigger, Portal, Content, Items, etc.',
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `Where the menu appears relative to the trigger. Options: 'top' | 'right' | 'bottom' | 'left' with optional '-start' | '-end' alignment.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: 'Controlled open state for the menu.',
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: 'Initial open state when uncontrolled.',
},
{
name: 'onOpenChange',
type: '(open: boolean, event?: { preventDefault: () => void }) => void',
required: false,
description: 'Called when the menu opens or closes. Call event.preventDefault() to cancel and allow the native context menu to show instead.',
},
{
name: 'onOpenWillChange',
type: '(open: boolean) => void',
required: false,
platform: 'ios',
description: 'Called before the open/close animation begins.',
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description:
'When true, traps focus inside the menu and blocks outside scroll/interactions.',
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
default: '{ padding: 10 }',
description: 'Shifts the menu horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).',
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: 'Flips the menu to the opposite side if there is insufficient space.',
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: 'Distance between the menu and its trigger.',
},
{
name: 'unstyled',
required: false,
type: 'boolean',
description: 'Removes all default Tamagui styles.',
},
]}
/>
#### Allowing Native Context Menu
You can call `event.preventDefault()` in `onOpenChange` to prevent the Tamagui menu from opening and allow the native browser context menu to appear instead:
```tsx
<ContextMenu
onOpenChange={(open, event) => {
if (someCondition) {
// prevent Tamagui menu, let native context menu show
event?.preventDefault()
}
}}
>
{/* ... */}
</ContextMenu>
```
### ContextMenu.Portal
This is necessary for the ContextMenu.
<PropsTable
data={[
{
name: 'zIndex',
required: false,
type: 'number',
description: 'Stacking order of the portal layer.',
},
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'Content to render inside the portal.',
},
{
name: 'forceMount',
type: 'true',
required: false,
description:
'Forces the portal to stay mounted, useful for controlling animations.',
},
]}
/>
### ContextMenu.Trigger
The ContextMenu will only be triggered when the user right-clicks or
long-presses within the Trigger area.
<PropsTable
data={[
{
name: 'action',
required: false,
type: 'press|longPress',
default: 'longPress',
platform: 'android/ios',
description:
"Works with the native prop and accepts 'press' or 'longPress'. The default is 'longPress' for ContextMenu.",
},
]}
/>
### ContextMenu.Content
Contains the content of the ContextMenu.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'Menu items, groups, labels, separators, and submenus.',
},
{
name: 'loop',
type: 'boolean',
required: false,
default: 'false',
description: 'Whether keyboard navigation wraps from last to first item.',
},
{
name: 'forceMount',
type: 'true',
required: false,
description:
'Forces the content to stay mounted, useful for controlling animations.',
},
{
name: 'onCloseAutoFocus',
type: '(event: Event) => void',
required: false,
description: 'Called when focus returns to the trigger after closing.',
},
{
name: 'onEscapeKeyDown',
type: '(event: KeyboardEvent) => void',
required: false,
description: 'Called when the escape key is pressed. Can be prevented.',
},
{
name: 'onPointerDownOutside',
type: '(event: PointerEvent) => void',
required: false,
description: 'Called when a pointer event occurs outside the content.',
},
{
name: 'onInteractOutside',
type: '(event: Event) => void',
required: false,
description: 'Called when any interaction occurs outside the content.',
},
]}
/>
### ContextMenu.Item
A selectable menu item that triggers an action when selected.
<PropsTable
data={[
{
name: 'key',
type: 'string',
required: true,
description: 'Unique identifier for the item.',
},
{
name: 'disabled',
type: 'boolean',
required: false,
default: 'false',
description: 'Prevents interaction and dims the item.',
},
{
name: 'destructive',
type: 'boolean',
required: false,
platform: 'ios/android',
description:
'Renders the item in red on iOS to indicate a dangerous action (e.g. delete). No effect on web.',
},
{
name: 'hidden',
type: 'boolean',
required: false,
description: 'Hides the item from the menu.',
},
{
name: 'onSelect',
type: '(event?: Event) => void',
required: false,
description: 'Called when the item is selected via click or keyboard.',
},
{
name: 'onFocus',
type: '() => void',
required: false,
description: 'Called when the item receives focus.',
},
{
name: 'onBlur',
type: '() => void',
required: false,
description: 'Called when the item loses focus.',
},
{
name: 'textValue',
type: 'string',
required: false,
platform: 'ios/android',
description:
'Text used for typeahead and native menus. Required when ItemTitle contains a React node instead of a string.',
},
]}
/>
### ContextMenu.ItemTitle
Renders the title of the menu item.
<PropsTable
data={[
{
name: 'children',
type: 'string | React.ReactNode',
required: true,
description: 'The title text or element to display.',
},
]}
/>
<Notice>
You can directly pass a text node to the ItemTitle. However, if you use a nested
React node like `<Text>`, you need to pass `textValue` to the `<Item>` so that it
works with native menus.
</Notice>
### ContextMenu.ItemIcon
A component to render an icon. For non-native menus, you can pass an icon
component. For native menus, you can pass platform-specific icons to the
`android`/`ios` props.
On iOS, it renders the native
[SF Symbols](https://github.com/andrewtavis/sf-symbols-online) icons.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: false,
description: 'Fallback icon for web when native icons are not available.',
},
{
name: 'ios',
type: 'object',
required: false,
platform: 'ios',
description:
'SF Symbol configuration: name, weight, scale, hierarchicalColor, paletteColors.',
},
{
name: 'android',
type: 'object',
required: false,
platform: 'android',
description: 'Android resource drawable name.',
},
]}
/>
```jsx line=1
<ContextMenu.ItemIcon
ios={{
name: '0.circle.fill', // required
pointSize: 5,
weight: 'semibold',
scale: 'medium',
// can also be a color string. Requires iOS 15+
hierarchicalColor: {
dark: 'blue',
light: 'green',
},
// alternative to hierarchical color. Requires iOS 15+
paletteColors: [
{
dark: 'blue',
light: 'green',
},
],
}}
>
<CircleIcon />
</ContextMenu.ItemIcon>
```
### ContextMenu.ItemImage
A component to render an item image. For native menus, it only works on iOS. It
takes the same properties as `@tamagui/image`.
### ContextMenu.ItemSubtitle
A component to render a subtitle for the menu item. For native menus, it only
works on iOS.
<PropsTable
data={[
{
name: 'children',
type: 'string',
required: true,
description: 'The subtitle text to display below the title.',
},
]}
/>
### ContextMenu.Group
A component that groups multiple menu items together.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'Menu items to group together.',
},
]}
/>
### ContextMenu.CheckboxItem
A menu item with a checkbox that can be toggled on/off.
<PropsTable
data={[
{
name: 'key',
type: 'string',
required: true,
description: 'Unique identifier for the checkbox item.',
},
{
name: 'disabled',
type: 'boolean',
required: false,
default: 'false',
description: 'Prevents interaction and dims the item.',
},
{
name: 'destructive',
type: 'boolean',
required: false,
platform: 'ios/android',
description:
'Renders the item in red on iOS to indicate a dangerous action. No effect on web.',
},
{
name: 'hidden',
type: 'boolean',
required: false,
description: 'Hides the item from the menu.',
},
{
name: 'onFocus',
type: '() => void',
required: false,
description: 'Called when the item receives focus.',
},
{
name: 'onBlur',
type: '() => void',
required: false,
description: 'Called when the item loses focus.',
},
{
name: 'textValue',
type: 'string',
required: false,
platform: 'ios/android',
description:
'Text for native menus. Required when ItemTitle contains a React node.',
},
{
name: 'value',
type: "'on' | 'off' | 'mixed'",
required: false,
platform: 'ios/android',
description: 'Controlled checked state for native menus.',
},
{
name: 'onValueChange',
type: '(state, prevState) => void',
required: false,
platform: 'ios/android',
description: 'Called when checked state changes on native menus.',
},
{
name: 'checked',
type: 'boolean',
required: false,
description: 'Controlled checked state for web menus.',
},
{
name: 'onCheckedChange',
type: '(checked: boolean) => void',
required: false,
description: 'Called when checked state changes on web.',
},
]}
/>
### ContextMenu.ItemIndicator
Use inside `CheckboxItem` or `RadioItem` to indicate when an item is checked.
This allows you to conditionally render a checkmark.
```jsx line=1
<ContextMenu.ItemIndicator>
<CheckmarkIcon /> {/* This does not work with the native prop. */}
</ContextMenu.ItemIndicator>
```
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: false,
description: 'Custom checkmark icon. Only works on web.',
},
{
name: 'forceMount',
type: 'true',
required: false,
description: 'Forces the indicator to stay mounted for animation control.',
},
]}
/>
### ContextMenu.Label
Renders a non-focusable label for a group of items. On native menus, only one
label is supported per menu and submenu.
<PropsTable
data={[
{
name: 'children',
type: 'string',
required: true,
description: 'The label text.',
},
{
name: 'textValue',
type: 'string',
required: false,
platform: 'ios/android',
description: 'Text for native menus when children is a React node.',
},
]}
/>
### ContextMenu.Arrow
Renders an arrow pointing to the trigger.
<PropsTable
data={[
{
name: 'size',
type: 'number | SizeToken',
required: false,
description: 'Width and height of the arrow.',
},
{
name: 'unstyled',
type: 'boolean',
required: false,
description: 'Removes default arrow styles.',
},
]}
/>
### ContextMenu.Separator
Renders a visual divider between menu items. Web only.
### ContextMenu.Sub
A container for nested sub-menu components.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'SubTrigger, Portal, and SubContent components.',
},
{
name: 'open',
type: 'boolean',
required: false,
description: 'Controlled open state for the sub-menu.',
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
description: 'Called when the sub-menu opens or closes.',
},
]}
/>
### ContextMenu.SubContent
Renders the content of a sub-menu. Same props as `Content`, excluding `side` and
`align`.
### ContextMenu.SubTrigger
A menu item that opens a sub-menu on hover/focus. Accepts the same props as
`Item`.
### ContextMenu.Preview
When the ContextMenu is visible, this renders a custom preview component. Only
works with native iOS menus.
<Notice>Should be rendered as a child of `ContextMenu.Content`.</Notice>
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode | (() => React.ReactNode)',
required: true,
platform: 'ios',
description: 'The preview content to render. Can be a function for lazy rendering.',
},
{
name: 'size',
type: '{ width?: number, height?: number }',
required: false,
platform: 'ios',
description: 'Dimensions of the preview.',
},
{
name: 'onPress',
type: '() => void',
required: false,
platform: 'ios',
description: 'Called when the preview is pressed.',
},
{
name: 'backgroundColor',
type: 'string | { dark: string, light: string }',
required: false,
platform: 'ios',
description: 'Background color of the preview.',
},
{
name: 'borderRadius',
type: 'number',
required: false,
platform: 'ios',
description: 'Border radius of the preview.',
},
{
name: 'preferredCommitStyle',
type: "'pop' | 'dismiss'",
required: false,
default: "'dismiss'",
platform: 'ios',
description:
"Exit transition when preview is tapped. Use 'pop' when navigating to another screen.",
},
]}
/>
```jsx line=1
<ContextMenu.Preview
// optional props:
preferredCommitStyle="pop" // or "dismiss"
backgroundColor={{
// or a color string directly
dark: 'black',
light: 'white',
}}
>
{() => <Preview />}
</ContextMenu.Preview>
```
## components/dialog/1.0.0
---
title: Dialog
description: Show a modal with configurable layout and accessible actions.
name: dialog
component: Dialog
package: dialog
demoName: Dialog
---
<HeroContainer showAnimationDriverControl>
<DialogDemo />
</HeroContainer>
```tsx hero template=Dialog
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible with dev-time checks to ensure ARIA props.`,
]}
/>
<Notice>
Dialog does not work on native, instead you can Adapt it to a Sheet as shown in the demo
code above.
</Notice>
## Installation
Dialog is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/dialog
```
In order to use this component independently of `tamagui`, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
## Anatomy
```tsx
import { Dialog } from 'tamagui' // or '@tamagui/dialog'
export default () => (
<Dialog>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
{/* Optional: Control focus behavior */}
<Dialog.FocusScope loop trapped focusOnIdle={true}>
<Dialog.FocusScope.Scope>
{/* Focus scope will be applied to children */}
</Dialog.FocusScope.Scope>
</Dialog.FocusScope>
</Dialog>
)
```
## API Reference
### Dialog
Contains every component for the dialog. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Dialog.Content`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'open',
type: 'boolean',
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
},
{
name: 'modal',
type: 'boolean',
default: 'true',
description: `Renders into root of app instead of inline`,
},
{
name: 'disableRemoveScroll',
type: 'boolean',
required: false,
description: `Used to disable the automatic removal of scrolling from the page when open.`,
},
]}
/>
### Dialog.Trigger
Just [Tamagui Props](/docs/intro/props).
### Dialog.Portal
Renders Dialog into appropriate container. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Dialog.Content
Main container for Dialog content, this is where you should apply animations.
Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Dialog.Overlay
Displays behind Content. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### Dialog.Title
Required. Can wrap in VisuallyHidden to hide.
Defaults to H2, see [Headings](/docs/components/headings).
### Dialog.Description
Required. Can wrap in VisuallyHidden to hide.
Defaults to Paragraph, see [Paragraph](/docs/components/text).
### Dialog.Close
Closes the Dialog, accepts the same props as YStack. Recommended to use with your own component and `asChild`.
<PropsTable
data={[
{
name: 'displayWhenAdapted',
type: 'boolean',
description: `By default Close elements hide when Adapt is active. If set to true, they will show when adapted.`,
},
]}
/>
Just [Tamagui Props](/docs/intro/props).
### Dialog.FocusScope
Provides access to the underlying FocusScope component used by Dialog for focus management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Dialog.Sheet
When used with `Adapt`, Dialog will render as a sheet when that breakpoint is active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Dialog.Sheet.Frame` to insert the contents given to `Dialog.Content`
```tsx
import { Dialog } from 'tamagui' // or '@tamagui/dialog'
export default () => (
<Dialog>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
{/* optionally change to sheet when small screen */}
<Dialog.Adapt when="maxMd">
<Dialog.Sheet>
<Dialog.Sheet.Frame>
<Dialog.Adapt.Contents />
</Dialog.Sheet.Frame>
<Dialog.Sheet.Overlay />
</Dialog.Sheet>
</Dialog.Adapt>
</Dialog>
)
```
<Notice>
Note that Dialog.Sheet currently doesn't preserve state of the contents when it
transitions between Sheet and Portal. In the future, we can do this on the web using
react-reparenting.
</Notice>
### PortalProvider
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Examples
### Inside native modals
If you're using native modals (maybe from react-navigation), you'll notice the Dialogs won't show up inside the modal. To get around this, you should wrap your screen inside `PortalProvider`, like so:
```tsx
import { PortalProvider } from 'tamagui'
// this component used in react-navigation/expo-router with `presentation: "modal"`
export function Page() {
return (
<PortalProvider>{/* rest of your page, including the Dialog... */}</PortalProvider>
)
}
```
## components/dialog/1.131.0
---
title: Dialog
description: Show a modal with configurable layout and accessible actions.
name: dialog
component: Dialog
package: dialog
demoName: Dialog
---
<HeroContainer showAnimationDriverControl>
<DialogDemo />
</HeroContainer>
```tsx hero template=Dialog
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible with dev-time checks to ensure ARIA props.`,
]}
/>
Dialog is a great way to show content inside a new floating window above
content. Be sure to open the code example above for a copy-paste implementation.
## Installation
Dialog is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/dialog
```
If you aren't using `tamagui` and instead using the `@tamagui/dialog` package
separately, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
## Anatomy
```tsx
import { Dialog } from 'tamagui' // or '@tamagui/dialog'
export default () => (
<Dialog>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
{/* Optional: Control focus behavior */}
<Dialog.FocusScope loop trapped focusOnIdle={true}>
<Dialog.FocusScope.Scope>
{/* Focus scope will be applied to children */}
</Dialog.FocusScope.Scope>
</Dialog.FocusScope>
</Dialog>
)
```
## Scoping
Dialog supports scoping which lets you mount one or more Dialog instances at the
root of your app, while having a deeply nested child Trigger or Content attach
to the proper parent Dialog instance.
In performance sensitive areas you may want to take advantage of this, it allows
you to only need to render the Dialog.Trigger inside the sensitive area as
Dialogs aren't the cheapest component - they have a lot of functionality.
Here's the basic anatomy of using `scope` and placing your Dialog higher up for
performance:
```tsx fileName=_layout.tsx
import { Dialog } from 'tamagui'
// in your root layout:
export default ({ children }) => (
<Dialog scope="user-profile">
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
{/* the rest of your app, note that it's inside of Dialog */}
{children}
</Dialog>
)
```
```tsx fileName=UserProfile.tsx
export default () => (
<Dialog.Trigger scope="user-profile">
<Button>Open Profile</Button>
</Dialog.Trigger>
)
```
Note that the `Trigger` scope ties to the `Dialog` scope.
## Dismissal Behavior
By default, dialogs can be dismissed by:
- Clicking outside the dialog content (on the overlay)
- Pressing the Escape key
- Clicking a Dialog.Close element
### Modal vs Non-Modal Dialogs
- **Modal dialogs** (`modal={true}`, which is the default):
- In v1, have `disableOutsidePointerEvents` set to `true` by default
- Still dismiss on outside click, but prevent interaction with elements behind the dialog
- Prevent right-click dismissal (right-clicks on the overlay are ignored)
- **Non-modal dialogs** (`modal={false}`):
- Allow interaction with elements behind the dialog
- Dismiss on any outside click
- Do not trap focus
### Preventing Outside Dismissal
To prevent a dialog from closing when clicking outside:
```tsx
<Dialog.Content
onPointerDownOutside={(event) => {
event.preventDefault()
}}
>
{/* Dialog contents */}
</Dialog.Content>
```
## API Reference
### Dialog
Contains every component for the dialog. Beyond
[Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Dialog.Content`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Passes size down to all sub-components when set for padding, arrow, borderRadius.`,
},
{
name: 'open',
type: 'boolean',
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
},
{
name: 'modal',
type: 'boolean',
default: 'true',
description: `Renders into root of app instead of inline`,
},
{
name: 'disableRemoveScroll',
type: 'boolean',
required: false,
description: `Used to disable the automatic removal of scrolling from the page when open.`,
},
]}
/>
### Dialog.Trigger
Just [Tamagui Props](/docs/intro/props).
### Dialog.Portal
Renders Dialog into appropriate container. Beyond
[Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Dialog.Content
Main container for Dialog content, this is where you should apply animations.
Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
{
name: 'disableOutsidePointerEvents',
type: 'boolean',
required: false,
description: `When true, hover/focus/click interactions will be disabled on elements outside the Dialog. Users will need to click twice on outside elements to interact with them: once to close the Dialog, and again to trigger the element. Note: In v1, modal dialogs have this set to true by default.`,
},
]}
/>
### Dialog.Overlay
Displays behind Content. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### Dialog.Title
Required. Can wrap in VisuallyHidden to hide.
Defaults to H2, see [Headings](/docs/components/headings).
### Dialog.Description
Required. Can wrap in VisuallyHidden to hide.
Defaults to Paragraph, see [Paragraph](/docs/components/text).
### Dialog.Close
Closes the Dialog, accepts the same props as YStack. Recommended to use with
your own component and `asChild`.
<PropsTable
data={[
{
name: 'displayWhenAdapted',
type: 'boolean',
description: `By default Close elements hide when Adapt is active. If set to true, they will show when adapted.`,
},
]}
/>
Just [Tamagui Props](/docs/intro/props).
### Dialog.FocusScope
Provides access to the underlying FocusScope component used by Dialog for focus
management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Dialog.Sheet
When used with `Adapt`, Dialog will render as a sheet when that breakpoint is
active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Dialog.Sheet.Frame` to insert the contents
given to `Dialog.Content`
```tsx
import { Dialog } from 'tamagui' // or '@tamagui/dialog'
export default () => (
<Dialog>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
{/* optionally change to sheet when small screen */}
<Dialog.Adapt when="maxMd">
<Dialog.Sheet>
<Dialog.Sheet.Frame>
<Dialog.Adapt.Contents />
</Dialog.Sheet.Frame>
<Dialog.Sheet.Overlay />
</Dialog.Sheet>
</Dialog.Adapt>
</Dialog>
)
```
<Notice>
Note that Dialog.Sheet currently doesn't preserve state of the contents when it
transitions between Sheet and Portal. In the future, we can do this on the web using
react-reparenting.
</Notice>
### PortalProvider
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Examples
### Inside native modals
If you're using native modals (maybe from react-navigation), you'll notice the
Dialogs won't show up inside the modal. To get around this, you should wrap your
screen inside `PortalProvider`, like so:
```tsx
import { PortalProvider } from 'tamagui'
// this component used in react-navigation/expo-router with `presentation: "modal"`
export function Page() {
return (
<PortalProvider>{/* rest of your page, including the Dialog... */}</PortalProvider>
)
}
```
## components/dialog/2.0.0
---
title: Dialog
description: Show a modal with configurable layout and accessible actions
name: dialog
component: Dialog
package: dialog
demoName: Dialog
---
<HeroContainer showAnimationDriverControl>
<DialogDemo />
</HeroContainer>
```tsx hero template=Dialog
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible with dev-time checks to ensure ARIA props.`,
]}
/>
Dialog is a great way to show content inside a new floating window above
content. Dialogs [automatically stack](/ui/z-index) above other overlays. Be sure to open the code example above for a copy-paste implementation.
## Installation
Dialog is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/dialog
```
<Notice theme="blue">
For native apps, we recommend [setting up native
portals](/docs/components/portal#native-portal-setup-recommended) to preserve React
context inside Dialog content.
</Notice>
## Anatomy
```tsx
import { Dialog } from 'tamagui' // or '@tamagui/dialog'
export default () => (
<Dialog>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
{/* Optional: Control focus behavior */}
<Dialog.FocusScope loop trapped focusOnIdle={true}>
<Dialog.FocusScope.Scope>
{/* Focus scope will be applied to children */}
</Dialog.FocusScope.Scope>
</Dialog.FocusScope>
</Dialog>
)
```
## Scoping
Dialog supports scoping which lets you mount one or more Dialog instances at the
root of your app, while having a deeply nested child Trigger or Content attach
to the proper parent Dialog instance.
In performance sensitive areas you may want to take advantage of this, it allows
you to only need to render the Dialog.Trigger inside the sensitive area as
Dialogs aren't the cheapest component - they have a lot of functionality.
Here's the basic anatomy of using `scope` and placing your Dialog higher up for
performance:
```tsx fileName=_layout.tsx
import { Dialog } from 'tamagui'
// in your root layout:
export default ({ children }) => (
<Dialog scope="user-profile">
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
{/* the rest of your app, note that it's inside of Dialog */}
{children}
</Dialog>
)
```
```tsx fileName=UserProfile.tsx
export default () => (
<Dialog.Trigger scope="user-profile">
<Button>Open Profile</Button>
</Dialog.Trigger>
)
```
Note that the `Trigger` scope ties to the `Dialog` scope.
## Dismissal Behavior
By default, dialogs can be dismissed by:
- Clicking outside the dialog content (on the overlay)
- Pressing the Escape key
- Clicking a Dialog.Close element
### Modal vs Non-Modal Dialogs
- **Modal dialogs** (`modal={true}`, which is the default):
- In v1, have `disableOutsidePointerEvents` set to `true` by default
- Still dismiss on outside click, but prevent interaction with elements behind the dialog
- Prevent right-click dismissal (right-clicks on the overlay are ignored)
- **Non-modal dialogs** (`modal={false}`):
- Allow interaction with elements behind the dialog
- Dismiss on any outside click
- Do not trap focus
### Preventing Outside Dismissal
To prevent a dialog from closing when clicking outside:
```tsx
<Dialog.Content
onPointerDownOutside={(event) => {
event.preventDefault()
}}
>
{/* Dialog contents */}
</Dialog.Content>
```
## API Reference
### Dialog
Contains every component for the dialog. Beyond
[Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Dialog.Content`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Passes size down to all sub-components when set for padding, arrow, borderRadius.`,
},
{
name: 'open',
type: 'boolean',
description: `Controlled open state of the dialog.`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Initial open state when uncontrolled.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called when the dialog opens or closes.`,
},
{
name: 'modal',
type: 'boolean',
default: 'true',
description: `Renders into root of app instead of inline.`,
},
{
name: 'keepChildrenMounted',
type: 'boolean',
default: 'false',
description: `When true, dialog content stays mounted in the DOM even when closed. Useful for preserving state across open/close cycles or when you need faster re-opening.`,
},
{
name: 'disableRemoveScroll',
type: 'boolean',
required: false,
description: `Used to disable the automatic removal of scrolling from the page when open.`,
},
]}
/>
### Dialog.Trigger
Just [Tamagui Props](/docs/intro/props).
### Dialog.Portal
Renders Dialog into appropriate container. Beyond
[Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Dialog.Content
Main container for Dialog content, this is where you should apply animations.
Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
{
name: 'disableOutsidePointerEvents',
type: 'boolean',
required: false,
description: `When true, hover/focus/click interactions will be disabled on elements outside the Dialog. Users will need to click twice on outside elements to interact with them: once to close the Dialog, and again to trigger the element. Note: In v1, modal dialogs have this set to true by default.`,
},
]}
/>
### Dialog.Overlay
Displays behind Content. Beyond [Tamagui Props](/docs/intro/props), adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
]}
/>
### Dialog.Title
Required. Can wrap in VisuallyHidden to hide.
Defaults to H2, see [Headings](/docs/components/headings).
### Dialog.Description
Required. Can wrap in VisuallyHidden to hide.
Defaults to Paragraph, see [Paragraph](/docs/components/text).
### Dialog.Close
Closes the Dialog, accepts the same props as YStack. Recommended to use with
your own component and `asChild`.
<PropsTable
data={[
{
name: 'displayWhenAdapted',
type: 'boolean',
description: `By default Close elements hide when Adapt is active. If set to true, they will show when adapted.`,
},
]}
/>
Just [Tamagui Props](/docs/intro/props).
### Dialog.FocusScope
Provides access to the underlying FocusScope component used by Dialog for focus
management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Dialog.Sheet
When used with `Adapt`, Dialog will render as a sheet when that breakpoint is
active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Dialog.Sheet.Frame` to insert the contents
given to `Dialog.Content`
```tsx
import { Dialog } from 'tamagui' // or '@tamagui/dialog'
export default () => (
<Dialog>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
{/* ... */}
</Dialog.Content>
</Dialog.Portal>
{/* optionally change to sheet when small screen */}
<Dialog.Adapt when="max-md">
<Dialog.Sheet>
<Dialog.Sheet.Frame>
<Dialog.Adapt.Contents />
</Dialog.Sheet.Frame>
<Dialog.Sheet.Overlay />
</Dialog.Sheet>
</Dialog.Adapt>
</Dialog>
)
```
<Notice>
Note that Dialog.Sheet currently doesn't preserve state of the contents when it
transitions between Sheet and Portal. In the future, we can do this on the web using
react-reparenting.
</Notice>
## components/focus-scope/1.128.0
---
title: FocusScope
description: Manage focus behavior within elements accessibly.
name: focus-scope
component: FocusScope
package: focus-scope
---
<IntroParagraph marginTop={-5} marginBottom={30}>
A utility component for managing keyboard focus within a container. Controls focus
trapping, auto-focus behavior, and focus cycling for accessible interactive components.
</IntroParagraph>
Note that this is a web-only component, on native it is a no-op.
<Highlights
features={[
'Trap focus within a container for modal-like behavior.',
'Auto-focus on mount and return focus on unmount.',
'Loop focus between first and last tabbable elements.',
'Prevent reflows during animations with focusOnIdle.',
]}
/>
## Installation
FocusScope is already installed in `tamagui`, or you can install it
independently:
```bash
npm install @tamagui/focus-scope
```
## Usage
Wrap any content that needs focus management:
```tsx
import { Button, FocusScope, XStack } from 'tamagui'
export default () => (
<FocusScope loop trapped>
<XStack space="$4">
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</XStack>
</FocusScope>
)
```
## Focus Trapping
Use `trapped` to prevent focus from escaping the scope:
```tsx
import { Button, Dialog, FocusScope, XStack, YStack } from 'tamagui'
export default () => (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Dialog</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
{/* key used by AnimatePresence to animate */}
<Dialog.Content key="content">
<FocusScope trapped>
<YStack space="$4">
<Dialog.Title>Focused Content</Dialog.Title>
<XStack space="$2">
<Button>Cancel</Button>
<Button>Confirm</Button>
</XStack>
</YStack>
</FocusScope>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
```
## Focus Looping
Enable `loop` to cycle focus between first and last elements:
```tsx
import { Button, FocusScope, XStack } from 'tamagui'
export default () => (
<FocusScope loop>
<XStack space="$4">
<Button>First</Button>
<Button>Second</Button>
<Button>Last</Button>
{/* Tab from "Last" goes to "First" */}
</XStack>
</FocusScope>
)
```
## Animation-Friendly Focusing
Use `focusOnIdle` to prevent reflows during animations:
```tsx
import { Button, FocusScope, XStack } from 'tamagui'
export default () => (
<FocusScope
focusOnIdle={true} // Wait for idle callback
// or focusOnIdle={200} // Wait 200ms
>
<XStack space="$4">
<Button>Animated</Button>
<Button>Content</Button>
</XStack>
</FocusScope>
)
```
## Advanced Control with FocusScopeController
Use the controller pattern for managing focus from parent components:
```tsx
import { Button, FocusScope, XStack, YStack } from 'tamagui'
import { useState } from 'react'
export default () => {
const [trapped, setTrapped] = useState(false)
return (
<YStack space="$4">
<Button onPress={() => setTrapped(!trapped)}>
{trapped ? 'Disable' : 'Enable'} Focus Trap
</Button>
<FocusScope.Controller trapped={trapped} loop>
<FocusScope>
<XStack space="$4">
<Button>Controlled</Button>
<Button>Focus</Button>
<Button>Behavior</Button>
</XStack>
</FocusScope>
</FocusScope.Controller>
</YStack>
)
}
```
## Function as Children
For advanced use cases, pass a function to get access to focus props:
```tsx
import { FocusScope, View } from 'tamagui'
export default () => (
<FocusScope loop>
{({ onKeyDown, tabIndex, ref }) => (
<View
ref={ref}
tabIndex={tabIndex}
onKeyDown={onKeyDown}
padding="$4"
borderWidth={1}
borderColor="$borderColor"
>
Custom focus container
</View>
)}
</FocusScope>
)
```
## API Reference
### FocusScope
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: 'Whether focus management is enabled',
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description:
'When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable',
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description:
'When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus',
},
{
name: 'focusOnIdle',
type: 'boolean | number | { min?: number; max?: number }',
default: 'false',
description:
'When true, waits for idle before focusing using requestIdleCallback. When a number, waits that many ms. Object sets a lower and upper bound. Helps to prevent reflows during animations, as focusing inputs easily blocks main thread.',
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: 'Event handler called when auto-focusing on mount. Can be prevented',
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: 'Event handler called when auto-focusing on unmount. Can be prevented',
},
{
name: 'forceUnmount',
type: 'boolean',
default: 'false',
description:
'If unmount is animated, you want to force re-focus at start of animation not after',
},
{
name: 'children',
type: 'React.ReactNode | ((props: FocusProps) => React.ReactNode)',
description:
'Content to apply focus management to, or function that receives focus props',
},
]}
/>
### FocusScope.Controller
Provides context-based control over FocusScope behavior:
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
description:
'Override enabled state for all child FocusScope.Controller.Scope components',
},
{
name: 'loop',
type: 'boolean',
description:
'Override loop state for all child FocusScope.Controller.Scope components',
},
{
name: 'trapped',
type: 'boolean',
description:
'Override trapped state for all child FocusScope.Controller.Scope components',
},
{
name: 'focusOnIdle',
type: 'boolean | number',
description:
'Override focusOnIdle behavior for all child FocusScope.Controller.Scope components',
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description:
'Override onMountAutoFocus handler for all child FocusScope.Controller.Scope components',
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description:
'Override onUnmountAutoFocus handler for all child FocusScope.Controller.Scope components',
},
{
name: 'forceUnmount',
type: 'boolean',
description:
'Override forceUnmount behavior for all child FocusScope.Controller.Scope components',
},
]}
/>
The FocusScope component automatically inherits props from the nearest
FocusScope.Controller, with controller props taking precedence over direct
props.
## Usage in Other Components
Many Tamagui components export FocusScope for advanced focus control:
```tsx
import { Dialog, Popover, Select } from 'tamagui'
// Available on:
<Dialog.FocusScope />
<Popover.FocusScope />
<Select.FocusScope />
// And more...
```
## Accessibility
FocusScope follows accessibility best practices:
- Manages `tabindex` appropriately for focus flow
- Respects user's reduced motion preferences
- Maintains focus visible indicators
- Provides proper ARIA support when used with other components
- Handles edge cases like disabled elements and hidden content
<Notice>
FocusScope is primarily designed for web platforms. On React Native, it renders children
without focus management since native platforms handle focus differently.
</Notice>
## Examples
### Modal Focus Management
```tsx
import { Button, Dialog, FocusScope, Input, XStack, YStack } from 'tamagui'
export default () => (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Modal</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<FocusScope trapped loop focusOnIdle={100}>
<YStack space="$4" padding="$4">
<Dialog.Title>User Details</Dialog.Title>
<Input placeholder="Name" />
<Input placeholder="Email" />
<XStack space="$2">
<Dialog.Close asChild>
<Button variant="outlined">Cancel</Button>
</Dialog.Close>
<Button>Save</Button>
</XStack>
</YStack>
</FocusScope>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
```
### Custom Focus Container
```tsx
import { Button, FocusScope, styled, XStack } from 'tamagui'
const FocusContainer = styled(XStack, {
borderWidth: 2,
borderColor: 'transparent',
borderRadius: '$4',
padding: '$4',
variants: {
focused: {
true: {
borderColor: '$blue10',
shadowColor: '$blue10',
shadowRadius: 10,
shadowOpacity: 0.3,
},
},
},
})
export default () => (
<FocusScope loop>
{({ onKeyDown, tabIndex, ref }) => (
<FocusContainer
ref={ref}
tabIndex={tabIndex}
onKeyDown={onKeyDown}
space="$4"
focused
>
<Button>Action 1</Button>
<Button>Action 2</Button>
<Button>Action 3</Button>
</FocusContainer>
)}
</FocusScope>
)
```
## components/focus-scope/2.0.0
---
title: FocusScope
description: Manage focus behavior within elements accessibly.
name: focus-scope
component: FocusScope
package: focus-scope
---
<IntroParagraph marginTop={-5} marginBottom={30}>
A utility component for managing keyboard focus within a container. Controls focus
trapping, auto-focus behavior, and focus cycling for accessible interactive components.
</IntroParagraph>
Note that this is a web-only component, on native it is a no-op.
<Highlights
features={[
'Trap focus within a container for modal-like behavior.',
'Auto-focus on mount and return focus on unmount.',
'Loop focus between first and last tabbable elements.',
'Prevent reflows during animations with focusOnIdle.',
]}
/>
## Installation
FocusScope is already installed in `tamagui`, or you can install it
independently:
```bash
npm install @tamagui/focus-scope
```
## Usage
Wrap any content that needs focus management:
```tsx
import { Button, FocusScope, XStack } from 'tamagui'
export default () => (
<FocusScope loop trapped>
<XStack space="$4">
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</XStack>
</FocusScope>
)
```
## Focus Trapping
Use `trapped` to prevent focus from escaping the scope:
```tsx
import { Button, Dialog, FocusScope, XStack, YStack } from 'tamagui'
export default () => (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Dialog</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
{/* key used by AnimatePresence to animate */}
<Dialog.Content key="content">
<FocusScope trapped>
<YStack space="$4">
<Dialog.Title>Focused Content</Dialog.Title>
<XStack space="$2">
<Button>Cancel</Button>
<Button>Confirm</Button>
</XStack>
</YStack>
</FocusScope>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
```
## Focus Looping
Enable `loop` to cycle focus between first and last elements:
```tsx
import { Button, FocusScope, XStack } from 'tamagui'
export default () => (
<FocusScope loop>
<XStack space="$4">
<Button>First</Button>
<Button>Second</Button>
<Button>Last</Button>
{/* Tab from "Last" goes to "First" */}
</XStack>
</FocusScope>
)
```
## Animation-Friendly Focusing
Use `focusOnIdle` to prevent reflows during animations:
```tsx
import { Button, FocusScope, XStack } from 'tamagui'
export default () => (
<FocusScope
focusOnIdle={true} // Wait for idle callback
// or focusOnIdle={200} // Wait 200ms
>
<XStack space="$4">
<Button>Animated</Button>
<Button>Content</Button>
</XStack>
</FocusScope>
)
```
## Advanced Control with FocusScopeController
Use the controller pattern for managing focus from parent components:
```tsx
import { Button, FocusScope, XStack, YStack } from 'tamagui'
import { useState } from 'react'
export default () => {
const [trapped, setTrapped] = useState(false)
return (
<YStack space="$4">
<Button onPress={() => setTrapped(!trapped)}>
{trapped ? 'Disable' : 'Enable'} Focus Trap
</Button>
<FocusScope.Controller trapped={trapped} loop>
<FocusScope>
<XStack space="$4">
<Button>Controlled</Button>
<Button>Focus</Button>
<Button>Behavior</Button>
</XStack>
</FocusScope>
</FocusScope.Controller>
</YStack>
)
}
```
## Function as Children
For advanced use cases, pass a function to get access to focus props:
```tsx
import { FocusScope, View } from 'tamagui'
export default () => (
<FocusScope loop>
{({ onKeyDown, tabIndex, ref }) => (
<View
ref={ref}
tabIndex={tabIndex}
onKeyDown={onKeyDown}
padding="$4"
borderWidth={1}
borderColor="$borderColor"
>
Custom focus container
</View>
)}
</FocusScope>
)
```
## API Reference
### FocusScope
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: 'Whether focus management is enabled',
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description:
'When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable',
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description:
'When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus',
},
{
name: 'focusOnIdle',
type: 'boolean | number | { min?: number; max?: number }',
default: 'false',
description:
'When true, waits for idle before focusing using requestIdleCallback. When a number, waits that many ms. Object sets a lower and upper bound. Helps to prevent reflows during animations, as focusing inputs easily blocks main thread.',
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: 'Event handler called when auto-focusing on mount. Can be prevented',
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: 'Event handler called when auto-focusing on unmount. Can be prevented',
},
{
name: 'forceUnmount',
type: 'boolean',
default: 'false',
description:
'If unmount is animated, you want to force re-focus at start of animation not after',
},
{
name: 'children',
type: 'React.ReactNode | ((props: FocusProps) => React.ReactNode)',
description:
'Content to apply focus management to, or function that receives focus props',
},
]}
/>
### FocusScope.Controller
Provides context-based control over FocusScope behavior:
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
description:
'Override enabled state for all child FocusScope.Controller.Scope components',
},
{
name: 'loop',
type: 'boolean',
description:
'Override loop state for all child FocusScope.Controller.Scope components',
},
{
name: 'trapped',
type: 'boolean',
description:
'Override trapped state for all child FocusScope.Controller.Scope components',
},
{
name: 'focusOnIdle',
type: 'boolean | number',
description:
'Override focusOnIdle behavior for all child FocusScope.Controller.Scope components',
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description:
'Override onMountAutoFocus handler for all child FocusScope.Controller.Scope components',
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description:
'Override onUnmountAutoFocus handler for all child FocusScope.Controller.Scope components',
},
{
name: 'forceUnmount',
type: 'boolean',
description:
'Override forceUnmount behavior for all child FocusScope.Controller.Scope components',
},
]}
/>
The FocusScope component automatically inherits props from the nearest
FocusScope.Controller, with controller props taking precedence over direct
props.
## Usage in Other Components
Many Tamagui components export FocusScope for advanced focus control:
```tsx
import { Dialog, Popover, Select } from 'tamagui'
// Available on:
<Dialog.FocusScope />
<Popover.FocusScope />
<Select.FocusScope />
// And more...
```
## Accessibility
FocusScope follows accessibility best practices:
- Manages `tabindex` appropriately for focus flow
- Respects user's reduced motion preferences
- Maintains focus visible indicators
- Provides proper ARIA support when used with other components
- Handles edge cases like disabled elements and hidden content
<Notice>
FocusScope is primarily designed for web platforms. On React Native, it renders children
without focus management since native platforms handle focus differently.
</Notice>
## Examples
### Modal Focus Management
```tsx
import { Button, Dialog, FocusScope, Input, XStack, YStack } from 'tamagui'
export default () => (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Modal</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<FocusScope trapped loop focusOnIdle={100}>
<YStack space="$4" padding="$4">
<Dialog.Title>User Details</Dialog.Title>
<Input placeholder="Name" />
<Input placeholder="Email" />
<XStack space="$2">
<Dialog.Close asChild>
<Button variant="outlined">Cancel</Button>
</Dialog.Close>
<Button>Save</Button>
</XStack>
</YStack>
</FocusScope>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
```
### Custom Focus Container
```tsx
import { Button, FocusScope, styled, XStack } from 'tamagui'
const FocusContainer = styled(XStack, {
borderWidth: 2,
borderColor: 'transparent',
borderRadius: '$4',
padding: '$4',
variants: {
focused: {
true: {
borderColor: '$blue10',
shadowColor: '$blue10',
shadowRadius: 10,
shadowOpacity: 0.3,
},
},
},
})
export default () => (
<FocusScope loop>
{({ onKeyDown, tabIndex, ref }) => (
<FocusContainer
ref={ref}
tabIndex={tabIndex}
onKeyDown={onKeyDown}
space="$4"
focused
>
<Button>Action 1</Button>
<Button>Action 2</Button>
<Button>Action 3</Button>
</FocusContainer>
)}
</FocusScope>
)
```
## components/form/1.3.0
---
title: Form
description: A simple form component for native and web.
name: form
component: Form
package: form
demoName: Form
---
<HeroContainer>
<FormsDemo />
</HeroContainer>
```tsx hero template=Forms
```
<Highlights
features={[
`Works on native and web.`,
`Outputs accessible forms.`,
`Works with every Tamagui prop.`,
]}
/>
## Installation
Form is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/form
```
## Anatomy
```tsx
import { Form } from 'tamagui' // or '@tamagui/form'
export default () => (
<Form>
{/* ... */}
<Form.Trigger asChild>
<Button />
</Form.Trigger>
</Form>
)
```
## API Reference
### Form
<PropsTable
data={[
{
name: 'onSubmit',
type: '() => void',
required: true,
description: `Must use Form.Trigger to ensure onSubmit will callback.`,
},
]}
/>
### Form.Trigger
Wrap this around your submitting element to make the form submit. We recommend using `asChild` to a child element of your choosing for more control.
Accepts [Tamagui Props](/docs/intro/props).
## components/form/2.0.0
---
title: Form
description: A simple form component for native and web
name: form
component: Form
package: form
demoName: Form
---
<HeroContainer>
<FormsDemo />
</HeroContainer>
```tsx hero template=Forms
```
<Highlights
features={[
`Works on native and web.`,
`Outputs accessible forms.`,
`Works with every Tamagui prop.`,
]}
/>
## Installation
Form is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/form
```
## Anatomy
```tsx
import { Form } from 'tamagui' // or '@tamagui/form'
export default () => (
<Form>
{/* ... */}
<Form.Trigger asChild>
<Button />
</Form.Trigger>
</Form>
)
```
## API Reference
### Form
<PropsTable
data={[
{
name: 'onSubmit',
type: '() => void',
required: true,
description: `Must use \`Form.Trigger\` to ensure \`onSubmit\` will be called.`,
},
]}
/>
### Form.Trigger
Wrap this around your submitting element to make the form submit. We recommend using `asChild` to a child element of your choosing for more control.
Accepts [Tamagui Props](/docs/intro/props).
## components/group/1.0.0
---
title: Group
description: Render horizontal or vertical groups easily.
name: group
component: Group
demoName: Group
---
# Group
<Description>Layout buttons and more with groups.</Description>
<HeroContainer>
<GroupDemo />
</HeroContainer>
```tsx hero template=Group
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Align vertically or horizontally.',
'Natural spacing and disabled props.',
'Use with or without Item for more control.',
]}
/>
## Usage
Instead of a generic `Group`, Tamagui prefers `XGroup` and `YGroup` for consistency with stacks.
By default, Groups will control the border radius of their children automatically - the first and last children will get their start/end radius set to match group radius. If it's a `YGroup`, it will adjust top/bottom radius. `XGroup` adjusts the left/right radius.
You can use Groups with or without `Group.Item`, depending on if you want to spacing and separators to be handled based on direct children, or on each Item rendered.
```tsx
import { Button, XGroup } from 'tamagui'
// usage with Item:
export default () => (
<XGroup>
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
For a simpler use case and backwards compat, you can also use it without `Group.Item`, which will just apply borders and spacing based on direct children. Note that Group will detect if any `Group.Item` is inside it, and automatically switch modes. If no Item, it spaces direct children:
```tsx
import { Button, XGroup } from 'tamagui'
// usage without Item:
export default () => (
<XGroup>
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</XGroup>
)
```
## Sizing
The `size` property will use your tokens to grab the appropriate radius for borderRadius values which it will pass to the first and last child as style props for borderRadius.
```tsx
import { Button, XGroup } from 'tamagui'
export default () => (
<XGroup size="$6">
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
## Disabled
The `disabled` property will pass to children
<HeroContainer>
<GroupDisabledDemo />
</HeroContainer>
## Group API
### Group
`XGroup` and `YGroup` extend [YStack](/docs/components/stacks), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'string | SizeTokens',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Pass disabled down to children.`,
},
{
name: 'disablePassBorderRadius',
required: false,
type: 'boolean',
description: `Disables passing border radius to first/last children.`,
},
{
name: 'vertical',
required: false,
type: 'boolean',
description: `Can be used to force an XGroup to render like a YGroup or vice-versa.`,
},
{
name: 'forceUseItem',
required: false,
type: 'boolean',
desctiption: `Can be used to force the group to assume it has Item components as children.`,
},
]}
/>
### Group.Item
Wrap each of `XGroup` or `YGroup`'s children in one of these. It lets Tamagui apply the needed styles to them.
Accepts only a `children` prop.
## components/group/1.11.2
---
title: Group
description: Render horizontal or vertical groups easily.
name: group
component: Group
demoName: Group
---
# Group
<Description>Layout buttons and more with groups</Description>
<HeroContainer>
<GroupDemo />
</HeroContainer>
```tsx hero template=Group
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Align vertically or horizontally.',
'Natural spacing and disabled props.',
'Use with or without Item for more control.',
]}
/>
## Usage
You can use `Group` by itself with the `orientation` property determining the direction it assumes.
By default, Groups will control the border radius of their children automatically - the first and last children will get their start/end radius set to match group radius. If it's a `YGroup`, it will adjust top/bottom radius. `XGroup` adjusts the left/right radius.
You can use Groups with or without `Group.Item`, depending on if you want to spacing and separators to be handled based on direct children, or on each Item rendered.
```tsx
import { Button, XGroup } from 'tamagui'
// usage with Item:
export default () => (
<XGroup>
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
For a simpler use case and backwards compat, you can also use it without `Group.Item`, which will just apply borders and spacing based on direct children. Note that Group will detect if any `Group.Item` is inside it, and automatically switch modes. If no Item, it spaces direct children:
```tsx
import { Button, XGroup } from 'tamagui'
// usage without Item:
export default () => (
<XGroup>
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</XGroup>
)
```
## Sizing
The `size` property will use your tokens to grab the appropriate radius for borderRadius values which it will pass to the first and last child as style props for borderRadius.
```tsx
import { Button, XGroup } from 'tamagui'
export default () => (
<XGroup size="$6">
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
## Disabled
The `disabled` property will pass to children
<HeroContainer>
<GroupDisabledDemo />
</HeroContainer>
## API Reference
### Group
`Group`, `XGroup` and `YGroup` extend [YStack](/docs/components/stacks), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'orientation',
required: false,
type: '"horizontal" | "vertical"',
description: `Forces applying the border radius styles to left/right vs top/bottom. Defaults to horizontal for XGroup and vertical for YGroup.`,
},
{
name: 'size',
required: false,
type: 'string | SizeTokens',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Pass disabled down to children.`,
},
{
name: 'disablePassBorderRadius',
required: false,
type: `boolean | 'bottom' | 'top' | 'start' | 'end'`,
description: `Disables passing border radius to first/last children.`,
},
{
name: 'forceUseItem',
required: false,
type: 'boolean',
desctiption: `Can be used to force the group to assume it has Item components as children.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Group.Item
Wrap each of `XGroup` or `YGroup`'s children in one of these. It lets Tamagui apply the needed styles to them.
Accepts only a `children` prop.
## components/group/1.56.1
---
title: Group
description: Render horizontal or vertical groups easily.
name: group
component: Group
demoName: Group
---
<HeroContainer>
<GroupDemo />
</HeroContainer>
```tsx hero template=Group
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Align vertically or horizontally.',
'Natural spacing and disabled props.',
'Use with or without Item for more control.',
]}
/>
## Installation
Group is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/group
```
## Usage
You can use `Group` by itself with the `orientation` property determining the direction it assumes.
By default, Groups will control the border radius of their children automatically - the first and last children will get their start/end radius set to match group radius. If it's a `YGroup`, it will adjust top/bottom radius. `XGroup` adjusts the left/right radius.
You can use Groups with or without `Group.Item`, depending on if you want to spacing and separators to be handled based on direct children, or on each Item rendered.
```tsx
import { Button, XGroup } from 'tamagui'
// usage with Item:
export default () => (
<XGroup>
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
For a simpler use case and backwards compat, you can also use it without `Group.Item`, which will just apply borders and spacing based on direct children. Note that Group will detect if any `Group.Item` is inside it, and automatically switch modes. If no Item, it spaces direct children:
```tsx
import { Button, XGroup } from 'tamagui'
// usage without Item:
export default () => (
<XGroup>
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</XGroup>
)
```
## Sizing
The `size` property will use your tokens to grab the appropriate radius for borderRadius values which it will pass to the first and last child as style props for borderRadius.
```tsx
import { Button, XGroup } from 'tamagui'
export default () => (
<XGroup size="$6">
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
## Disabled
The `disabled` property will pass to children
<HeroContainer>
<GroupDisabledDemo />
</HeroContainer>
## API Reference
### Group
`Group`, `XGroup` and `YGroup` extend [YStack](/docs/components/stacks), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'orientation',
required: false,
type: '"horizontal" | "vertical"',
description: `Forces applying the border radius styles to left/right vs top/bottom. Defaults to horizontal for XGroup and vertical for YGroup.`,
},
{
name: 'size',
required: false,
type: 'string | SizeTokens',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Pass disabled down to children.`,
},
{
name: 'disablePassBorderRadius',
required: false,
type: `boolean | 'bottom' | 'top' | 'start' | 'end'`,
description: `Disables passing border radius to first/last children.`,
},
{
name: 'forceUseItem',
required: false,
type: 'boolean',
description: `Can be used to force the group to assume it has Item components as children.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Group.Item
Wrap each of `XGroup` or `YGroup`'s children in one of these. It lets Tamagui apply the needed styles to them. It accepts the following props:
<PropsTable
data={[
{
name: 'children',
required: true,
type: 'ReactNode',
},
{
name: 'forcePlacement',
required: false,
type: '"first" | "center" | "last"',
description: `Forces the item to be a starting, center or ending item and gets the respective styles`,
},
]}
/>
## components/group/1.6.0
---
title: Group
description: Render horizontal or vertical groups easily.
name: group
component: Group
demoName: Group
---
# Group
<Description>Layout buttons and more with groups.</Description>
<HeroContainer>
<GroupDemo />
</HeroContainer>
```tsx hero template=Group
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Align vertically or horizontally.',
'Natural spacing and disabled props.',
'Use with or without Item for more control.',
]}
/>
## Usage
You can use `Group` by itself with the `axis` property determining the direction it assumes.
By default, Groups will control the border radius of their children automatically - the first and last children will get their start/end radius set to match group radius. If it's a `YGroup`, it will adjust top/bottom radius. `XGroup` adjusts the left/right radius.
You can use Groups with or without `Group.Item`, depending on if you want to spacing and separators to be handled based on direct children, or on each Item rendered.
```tsx
import { Button, XGroup } from 'tamagui'
// usage with Item:
export default () => (
<XGroup>
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
For a simpler use case and backwards compat, you can also use it without `Group.Item`, which will just apply borders and spacing based on direct children. Note that Group will detect if any `Group.Item` is inside it, and automatically switch modes. If no Item, it spaces direct children:
```tsx
import { Button, XGroup } from 'tamagui'
// usage without Item:
export default () => (
<XGroup>
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</XGroup>
)
```
## Sizing
The `size` property will use your tokens to grab the appropriate radius for borderRadius values which it will pass to the first and last child as style props for borderRadius.
```tsx
import { Button, XGroup } from 'tamagui'
export default () => (
<XGroup size="$6">
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
## Disabled
The `disabled` property will pass to children
<HeroContainer>
<GroupDisabledDemo />
</HeroContainer>
## Group API
### Group
`Group`, `XGroup`and`YGroup` extend [YStack](/docs/components/stacks), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'axis',
required: false,
type: '"horizontal" | "vertical"',
description: `Forces applying the border radius styles to left/right vs top/bottom. Defaults to horizontal for XGroup and vertical for YGroup.`,
},
{
name: 'size',
required: false,
type: 'string | SizeTokens',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Pass disabled down to children.`,
},
{
name: 'disablePassBorderRadius',
required: false,
type: 'boolean',
description: `Disables passing border radius to first/last children.`,
},
{
name: 'forceUseItem',
required: false,
type: 'boolean',
desctiption: `Can be used to force the group to assume it has Item components as children.`,
},
]}
/>
### Group.Item
Wrap each of `XGroup` or `YGroup`'s children in one of these. It lets Tamagui apply the needed styles to them.
Accepts only a `children` prop.
## components/group/1.7.0
---
title: Group
description: Render horizontal or vertical groups easily.
name: group
component: Group
demoName: Group
---
# Group
<Description>Layout buttons and more with groups.</Description>
<HeroContainer>
<GroupDemo />
</HeroContainer>
```tsx hero template=Group
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Align vertically or horizontally.',
'Natural spacing and disabled props.',
'Use with or without Item for more control.',
]}
/>
## Usage
You can use `Group` by itself with the `axis` property determining the direction it assumes.
By default, Groups will control the border radius of their children automatically - the first and last children will get their start/end radius set to match group radius. If it's a `YGroup`, it will adjust top/bottom radius. `XGroup` adjusts the left/right radius.
You can use Groups with or without `Group.Item`, depending on if you want to spacing and separators to be handled based on direct children, or on each Item rendered.
```tsx
import { Button, XGroup } from 'tamagui'
// usage with Item:
export default () => (
<XGroup>
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
For a simpler use case and backwards compat, you can also use it without `Group.Item`, which will just apply borders and spacing based on direct children. Note that Group will detect if any `Group.Item` is inside it, and automatically switch modes. If no Item, it spaces direct children:
```tsx
import { Button, XGroup } from 'tamagui'
// usage without Item:
export default () => (
<XGroup>
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</XGroup>
)
```
## Sizing
The `size` property will use your tokens to grab the appropriate radius for borderRadius values which it will pass to the first and last child as style props for borderRadius.
```tsx
import { Button, XGroup } from 'tamagui'
export default () => (
<XGroup size="$6">
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
## Disabled
The `disabled` property will pass to children
<HeroContainer>
<GroupDisabledDemo />
</HeroContainer>
## Group API
### Group
`Group`, `XGroup`and`YGroup` extend [YStack](/docs/components/stacks), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'axis',
required: false,
type: '"horizontal" | "vertical"',
description: `Forces applying the border radius styles to left/right vs top/bottom. Defaults to horizontal for XGroup and vertical for YGroup.`,
},
{
name: 'size',
required: false,
type: 'string | SizeTokens',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Pass disabled down to children.`,
},
{
name: 'disablePassBorderRadius',
required: false,
type: `boolean | 'bottom' | 'top' | 'start' | 'end'`,
description: `Disables passing border radius to first/last children.`,
},
{
name: 'forceUseItem',
required: false,
type: 'boolean',
desctiption: `Can be used to force the group to assume it has Item components as children.`,
},
]}
/>
### Group.Item
Wrap each of `XGroup` or `YGroup`'s children in one of these. It lets Tamagui apply the needed styles to them.
Accepts only a `children` prop.
## components/group/2.0.0
---
title: Group
description: Render horizontal or vertical groups easily
name: group
component: Group
package: group
demoName: Group
---
<HeroContainer>
<GroupDemo />
</HeroContainer>
```tsx hero template=Group
```
<Highlights
features={[
'Accepts size prop for border radius.',
'Align vertically or horizontally.',
'Children control their own sizing.',
'Disabled prop passes to children.',
]}
/>
## Installation
Group is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/group
```
## Usage
Use `Group` with `Group.Item` wrapping each child. The `orientation` property determines the layout direction.
Group zeros out the border radius on connecting sides of children - the first child keeps its start radius, the last keeps its end radius, and middle items have no radius on connecting sides. For `YGroup`, this affects top/bottom radius. For `XGroup`, left/right radius.
```tsx
import { Button, XGroup } from 'tamagui'
export default () => (
<XGroup>
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
## Sizing
In v2, children control their own sizing. Apply size props directly to the child components. For responsive sizing, use media queries on the children:
```tsx
import { Activity, Airplay } from '@tamagui/lucide-icons-2'
import { Button, XGroup } from 'tamagui'
export default () => (
<XGroup>
<XGroup.Item>
<Button size="$3" $gtSm={{ size: '$5' }} icon={Activity}>
First
</Button>
</XGroup.Item>
<XGroup.Item>
<Button size="$3" $gtSm={{ size: '$5' }} icon={Airplay}>
Second
</Button>
</XGroup.Item>
</XGroup>
)
```
The `size` prop on Group itself only affects the border radius of the group container:
```tsx
import { Button, XGroup } from 'tamagui'
export default () => (
<XGroup size="$6">
<XGroup.Item>
<Button>First</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
<XGroup.Item>
<Button>Third</Button>
</XGroup.Item>
</XGroup>
)
```
## Separators
Add separators manually between items:
```tsx
import { ListItem, Separator, YGroup } from 'tamagui'
export default () => (
<YGroup>
<YGroup.Item>
<ListItem title="First" />
</YGroup.Item>
<Separator />
<YGroup.Item>
<ListItem title="Second" />
</YGroup.Item>
<Separator />
<YGroup.Item>
<ListItem title="Third" />
</YGroup.Item>
</YGroup>
)
```
## Disabled
The `disabled` property will pass to children.
<HeroContainer>
<GroupDisabledDemo />
</HeroContainer>
## Custom Components & Nested Items
Automatic index detection only works when `Group.Item` is a **direct child** of `Group`. If you wrap `Group.Item` inside a custom component, the automatic first/last detection won't work.
You have a few options:
### Option 1: Use `forcePlacement`
If you know the position ahead of time, use the `forcePlacement` prop:
```tsx
function MyFirstItem({ children }) {
return <XGroup.Item forcePlacement="first">{children}</XGroup.Item>
}
export default () => (
<XGroup>
<MyFirstItem>
<Button>First</Button>
</MyFirstItem>
<XGroup.Item>
<Button>Second</Button>
</XGroup.Item>
</XGroup>
)
```
### Option 2: Use `useGroupItem` hook
For full control, use the `useGroupItem` hook directly in your custom component:
```tsx
import { useGroupItem } from '@tamagui/group'
function MyItem({ children, forcePlacement }) {
const groupItemProps = useGroupItem({ disabled: false }, forcePlacement)
return React.cloneElement(children, groupItemProps)
}
```
### Option 3: Pass placement from parent
Calculate positions in the parent and pass them down:
```tsx
const items = ['First', 'Second', 'Third']
export default () => (
<XGroup>
{items.map((item, i) => (
<XGroup.Item
key={item}
forcePlacement={i === 0 ? 'first' : i === items.length - 1 ? 'last' : 'center'}
>
<Button>{item}</Button>
</XGroup.Item>
))}
</XGroup>
)
```
## API Reference
### Group
`Group`, `XGroup` and `YGroup` extend [YStack](/docs/components/stacks), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'orientation',
required: false,
type: '"horizontal" | "vertical"',
description: `Forces applying the border radius styles to left/right vs top/bottom. Defaults to horizontal for XGroup and vertical for YGroup.`,
},
{
name: 'size',
required: false,
type: 'string | SizeTokens',
description: `Sets the border radius of the Group container using your size tokens.`,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Passes disabled state down to children.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Group.Item
Wrap each of `XGroup` or `YGroup`'s children in one of these. It lets Tamagui apply the needed styles to them. It accepts the following props:
<PropsTable
data={[
{
name: 'children',
required: true,
type: 'ReactNode',
description: `The child element to wrap.`,
},
{
name: 'forcePlacement',
required: false,
type: '"first" | "center" | "last"',
description: `Forces the item to be a starting, center, or ending item and applies the respective styles.`,
},
]}
/>
## components/headings/1.0.0
---
title: Headings
description: Heading components that mimic HTML equivalents
name: html
component: Headings
package: text
demoName: Headings
---
<HeroContainer>
<HeadingsDemo />
</HeroContainer>
```tsx hero template=Headings
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Define custom fonts with styles per-size.',
]}
/>
## Installation
Headings is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/text
```
```tsx
import { H1, H2, H3, H4, H5, H6, Heading } from 'tamagui'
export default () => (
<>
<H1>Heading 1</H1>
<H2>Heading 2</H2>
<H3>Heading 3</H3>
<H4>Heading 4</H4>
<H5>Heading 5</H5>
<H6>Heading 6</H6>
<Heading>Heading</Heading>
</>
)
```
The headings all extend from the base `Heading` component. Note, this is just our own theme for Inter headings, but you can change the styles for any font completely.
Tamagui expects for your [font.size](/docs/core/configuration#font-tokens) to have the keys 1-10 so headings work with your font tokens automatically.
## How it works
The `Heading` component is defined as follows:
```tsx
export const Heading = styled(Paragraph, {
tag: 'span',
name: 'Heading',
accessibilityRole: 'header',
fontFamily: '$heading',
size: '$8',
margin: 0,
})
```
Note that Heading, and H1-H6 all default to the `heading` font family that must be defined in your tamagui.config.ts.
Because [Paragraph](/docs/components/text#paragraph) extends [SizableText](/docs/components/text#sizabletext), you get automatic styles based on your font theme. Let's see how `SizableText` defines the size variant, roughly, which gives a good idea of how Tamagui works, and how you could create or change your own headings at a lower level.
```tsx
import { Text } from 'tamagui' // or '@tamagui/core'
const SizableText = styled(Text, {
name: 'SizableText',
fontFamily: '$body',
color: '$color',
variants: {
size: {
'...fontSize': (val, { font, props }) => {
const fontSize = font.size[val]
const lineHeight = font.lineHeight[val]
const fontWeight = font.weight[val]
const letterSpacing = font.letterSpacing[val]
const fontStyle = font.style?.[val]
const textTransform = font.transform?.[val]
return {
fontStyle,
textTransform,
fontWeight,
letterSpacing,
fontSize,
lineHeight,
}
},
},
},
defaultVariants: {
// note tamagui uses a generic "true" token that your sizes should set to be the same as the default on your scale
size: '$true',
},
})
```
## API Reference
### Heading
Headings extend [SizableText props](/docs/components/text#sizabletext) inheriting all the [Tamagui standard props](/docs/intro/props).
## components/headings/2.0.0
---
title: Headings
description: Heading components that mirror HTML equivalents
name: headings
component: Headings
package: text
demoName: Headings
---
<HeroContainer>
<HeadingsDemo />
</HeroContainer>
```tsx hero template=Headings
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Define custom fonts with styles per-size.',
]}
/>
## Installation
Headings is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/text
```
```tsx
import { H1, H2, H3, H4, H5, H6, Heading } from 'tamagui'
export default () => (
<>
<H1>Heading 1</H1>
<H2>Heading 2</H2>
<H3>Heading 3</H3>
<H4>Heading 4</H4>
<H5>Heading 5</H5>
<H6>Heading 6</H6>
<Heading>Heading</Heading>
</>
)
```
The headings all extend from the base `Heading` component. Note, this is just our own theme for Inter headings, but you can change the styles for any font completely.
Tamagui expects for your [font.size](/docs/core/configuration#font-tokens) to have the keys 1-10 so headings work with your font tokens automatically.
## How it works
The `Heading` component is defined as follows:
```tsx
export const Heading = styled(Paragraph, {
tag: 'span',
name: 'Heading',
accessibilityRole: 'header',
fontFamily: '$heading',
size: '$8',
margin: 0,
})
```
Note that Heading, and H1-H6 all default to the `heading` font family that must be defined in your tamagui.config.ts.
Because [Paragraph](/docs/components/text#paragraph) extends [SizableText](/docs/components/text#sizabletext), you get automatic styles based on your font theme. Let's see how `SizableText` defines the size variant, roughly, which gives a good idea of how Tamagui works, and how you could create or change your own headings at a lower level.
```tsx
import { Text } from 'tamagui' // or '@tamagui/core'
const SizableText = styled(Text, {
name: 'SizableText',
fontFamily: '$body',
color: '$color',
variants: {
size: {
'...fontSize': (val, { font, props }) => {
const fontSize = font.size[val]
const lineHeight = font.lineHeight[val]
const fontWeight = font.weight[val]
const letterSpacing = font.letterSpacing[val]
const fontStyle = font.style?.[val]
const textTransform = font.transform?.[val]
return {
fontStyle,
textTransform,
fontWeight,
letterSpacing,
fontSize,
lineHeight,
}
},
},
},
defaultVariants: {
// note tamagui uses a generic "true" token that your sizes should set to be the same as the default on your scale
size: '$true',
},
})
```
## API Reference
### Heading
Headings extend [SizableText props](/docs/components/text#sizabletext) inheriting all the [Tamagui standard props](/docs/intro/props).
## components/html-elements/1.0.0
---
title: HTML Elements
description: Render semantic HTML with these elements.
name: html-elements
component: Layouts
---
To assist in creating accessible web apps, the following components are exported, all mapping directly to DOM elements of the lowercase names:
- `Section` (`section`)
- `Article` (`article`)
- `Main` (`main`)
- `Header` (`header`)
- `Aside` (`aside`)
- `Footer` (`footer`)
- `Nav` (`nav`)
## Installation
It's exported by `tamagui`, or:
```bash
npm install @tamagui/elements
```
## HTML element props
All HTML components extend View, inheriting all the [Tamagui standard props](/docs/intro/props).
## components/html-elements/2.0.0
---
title: HTML Elements
description: Render semantic HTML with these elements
name: html-elements
component: Layouts
package: elements
---
To assist in creating accessible web apps, the following components are exported, all mapping directly to DOM elements of the lowercase names:
- `Section` (`section`)
- `Article` (`article`)
- `Main` (`main`)
- `Header` (`header`)
- `Aside` (`aside`)
- `Footer` (`footer`)
- `Nav` (`nav`)
## Installation
It's exported by `tamagui`, or:
```bash
npm install @tamagui/elements
```
## API Reference
All HTML components extend View, inheriting all the [Tamagui standard props](/docs/intro/props).
## components/image/1.0.0
---
title: Image
description: React Native Web Image + Tamagui style props.
name: html
component: Image
package: image
demoName: Image
---
# Image
<Description>React Native Web Image + Tamagui style props.</Description>
<HeroContainer noPad>
<ImageDemo />
</HeroContainer>
```tsx hero template=Image
```
<Highlights
features={['Supports SSR.', 'Works on native and web.', 'Accepts Tamagui style props.']}
/>
### Props
[Tamagui props](/docs/intro/props) + [React Native Web Image props](https://necolas.github.io/react-native-web/docs/image/).
## components/image/1.13.0
---
title: Image
description: React Native Web Image + Tamagui style props.
name: html
component: Image
package: image
demoName: Image
---
<HeroContainer noPad>
<ImageDemo />
</HeroContainer>
```tsx hero template=Image
```
<Highlights
features={['Supports SSR.', 'Works on native and web.', 'Accepts Tamagui style props.']}
/>
## Installation
Image is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/image
```
## Usage
Note that you need to set `source` like so - the `width` and `height` properties apply as styles around the image, but the actual image needs `source.width` and `source.height` for React Native:
```tsx
export default () => (
<Image
source={{ width: 200, height: 200, uri: 'https://...' }}
width="100%"
height="100%"
/>
)
```
## API Reference
### Image
[Tamagui props](/docs/intro/props) + [React Native Web Image props](https://necolas.github.io/react-native-web/docs/image/).
## components/image/2.0.0
---
title: Image
description: A pure, lightweight Image component with Tamagui style props
name: image
component: Image
package: image
demoName: Image
---
<HeroContainer noPad>
<ImageDemo />
</HeroContainer>
```tsx hero template=Image
```
<Highlights
features={[
'Supports SSR.',
'Works on native and web.',
'Accepts Tamagui style props.',
'Pluggable architecture with createImage for expo-image and more.',
]}
/>
## Installation
Image is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/image
```
## Usage
Use the `src` prop for the image URL:
```tsx
import { Image } from 'tamagui'
export default () => (
<Image src="https://example.com/photo.jpg" width={200} height={200} />
)
```
You can also use `objectFit` to control how the image fits its container:
```tsx
<Image src="https://example.com/photo.jpg" width="100%" height={300} objectFit="cover" />
```
### Sizing on native
`width` and `height` are Tamagui layout props. On native, they become React Native
Image styles. Remote images must resolve to a nonzero layout size, so use fixed
dimensions or place percentage-sized images inside a parent with defined dimensions:
```tsx
<View width={320} height={200}>
<Image
src="https://example.com/photo.jpg"
width="100%"
height="100%"
objectFit="cover"
/>
</View>
```
Tamagui converts a string `src` to `source={{ uri: src }}` on native. Layout
dimensions stay in `style`; they are not copied into the native source metadata.
React Native `source` objects remain supported for migration, but `src` is preferred.
## Using with expo-image
For better performance and features like blurhash placeholders and transitions,
you can use `createImage` to create a custom Image component with expo-image:
```tsx
import { Image as ExpoImage } from 'expo-image'
import { createImage } from '@tamagui/image'
export const Image = createImage({
Component: ExpoImage,
resizeModePropName: 'contentFit',
objectPositionPropName: 'contentPosition',
})
```
Now you can use all expo-image props alongside Tamagui's unified API:
```tsx
const blurhash = '|rF?hV%2WCj[ayj[a|j[az...'
export default () => (
<Image
src="https://example.com/photo.jpg"
width={200}
height={300}
objectFit="cover"
placeholder={{ blurhash }}
transition={300}
/>
)
```
### createImage Options
<SimpleTable
headers={['Option', 'Type', 'Default', 'Description']}
rows={[
[
'Component',
'`ComponentType`',
'-',
'The underlying image component (expo-image, react-native-fast-image, etc.)',
],
[
'resizeModePropName',
'`string`',
"`'resizeMode'`",
"The prop name for resize mode. expo-image uses `'contentFit'`",
],
[
'objectPositionPropName',
'`string`',
'`undefined`',
"The prop name for object position. expo-image uses `'contentPosition'`",
],
[
'mapObjectFitToResizeMode',
'`function`',
'Default mapper',
"Custom function to map `objectFit` values to the component's resize mode",
],
[
'transformSource',
'`function`',
'Default transformer',
'Custom function to transform source props',
],
]}
/>
## Accessibility
Always provide an `alt` attribute describing the image content. For decorative images, use an empty string (`alt=""`).
```tsx
// meaningful image
<Image
src="/team-photo.jpg"
alt="The Tamagui team at React Conf 2024"
width={400}
height={300}
/>
// decorative image
<Image
src="/decorative-bg.jpg"
alt=""
width={400}
height={300}
/>
```
On React Native, use `accessible` and `accessibilityLabel` for screen reader support. See the [React Native Accessibility docs](https://reactnative.dev/docs/accessibility) for details.
```tsx
<Image
src="/avatar.jpg"
accessible
accessibilityLabel="User profile photo"
width={64}
height={64}
/>
```
## API Reference
### Image
Extends View with [Tamagui props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'src',
type: 'string | number',
description:
'The image URL or native require() result. Preferred over `source` for better web alignment.',
},
{
name: 'alt',
type: 'string',
description:
'Alternative text describing the image for accessibility. Use empty string for decorative images.',
},
{
name: 'objectFit',
type: "'cover' | 'contain' | 'fill' | 'none' | 'scale-down'",
description:
'How the image should be resized to fit its container. Maps to CSS object-fit on web and resizeMode on native.',
},
{
name: 'objectPosition',
type: 'string',
description:
'How the image should be positioned within its container. On native, requires expo-image or similar for full support.',
},
{
name: 'source',
type: 'ImageSourcePropType',
description:
'Deprecated React Native source prop. It remains supported during migration; prefer `src`.',
},
{
name: 'resizeMode',
type: 'ImageResizeMode',
description: 'Deprecated. Use `objectFit` instead.',
},
]}
/>
### Web-only Props
These props pass through to the native `<img>` element on web:
<PropsTable
data={[
{
name: 'loading',
type: "'lazy' | 'eager'",
description:
'Lazy load images below the fold. Use eager (default) for above-the-fold images.',
},
{
name: 'decoding',
type: "'async' | 'sync' | 'auto'",
description:
'Hint for how the browser should decode the image. Use async for non-blocking decode.',
},
{
name: 'fetchPriority',
type: "'high' | 'low' | 'auto'",
description:
'Hint for fetch priority. Use high for LCP (largest contentful paint) images.',
},
{
name: 'srcSet',
type: 'string',
description:
'Comma-separated list of image sources with width descriptors for responsive images.',
},
{
name: 'sizes',
type: 'string',
description:
'Media conditions describing the image display size, used with srcSet.',
},
{
name: 'crossOrigin',
type: "'anonymous' | 'use-credentials'",
description: 'CORS setting for the image request.',
},
{
name: 'referrerPolicy',
type: 'string',
description: 'Referrer policy for the image request.',
},
]}
/>
### Accessibility Props
<PropsTable
data={[
{
name: 'alt',
type: 'string',
description:
'Alternative text for screen readers. Required for meaningful images, use empty string for decorative.',
},
{
name: 'accessible',
type: 'boolean',
description: 'Native only. Marks the image as an accessible element.',
},
{
name: 'accessibilityLabel',
type: 'string',
description:
'Native only. Label read by screen readers. Use with accessible={true}.',
},
{
name: 'aria-label',
type: 'string',
description: 'Overrides alt as the accessible name. Prefer using alt instead.',
},
{
name: 'aria-describedby',
type: 'string',
description: 'References an element providing extended description.',
},
{
name: 'aria-hidden',
type: 'boolean',
description: 'Hides the image from assistive technology. Alternative to alt="".',
},
{
name: 'role',
type: 'string',
description:
"Set to 'presentation' or 'none' for decorative images, or 'img' (default).",
},
]}
/>
<Notice theme="green">
Web props like `loading`, `srcSet`, and `fetchPriority` only apply on web. On native,
use expo-image via `createImage` for advanced features like blurhash placeholders and
priority loading.
</Notice>
## components/inputs/1.0.0
---
title: Input & Textarea
name: inputs
description: Flexible form fields in styled and unstyled forms.
component: Input
demoName: Inputs
---
<HeroContainer demoMultiple>
<InputsDemo />
</HeroContainer>
```tsx hero template=Inputs
```
Using the same base component TextInput, from [React Native](https://reactnative.dev/docs/textinput) or [React Native Web](https://necolas.github.io/react-native-web/docs/text-input/), Tamagui simply wraps these components to allow the full set of style props, as well as scaling all the styles up or down using the `size` property, much like Button.
## Installation
Input is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/input
```
## Input
A one-line input field:
```tsx
import { Input } from 'tamagui'
export const App = () => (
// Accepts size and style properties directly
<Input size="$4" borderWidth={2} />
)
```
## TextArea
For multi-line inputs:
```tsx
import { TextArea } from 'tamagui'
export const App = () => (
// Accepts size and style properties directly
<TextArea size="$4" borderWidth={2} />
)
```
## components/inputs/2.0.0
---
title: Input & TextArea
name: inputs
description: Flexible form fields in styled and unstyled forms
component: Input
package: input
demoName: Inputs
---
<HeroContainer demoMultiple>
<InputsDemo />
</HeroContainer>
```tsx hero template=Inputs
```
Using the same base component TextInput, from [React Native](https://reactnative.dev/docs/textinput) or [React Native Web](https://necolas.github.io/react-native-web/docs/text-input/), Tamagui simply wraps these components to allow the full set of style props, as well as scaling all the styles up or down using the `size` property, much like Button.
## Installation
Input is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/input
```
## Input
A one-line input field:
```tsx
import { Input } from 'tamagui'
export const App = () => (
// Accepts size and style properties directly
<Input size="$4" borderWidth={2} />
)
```
## TextArea
For multiline inputs:
```tsx
import { TextArea } from 'tamagui'
export const App = () => (
// Accepts size and style properties directly
<TextArea size="$4" borderWidth={2} />
)
```
## components/intro/1.0.0
---
title: Tamagui UI
---
<IntroParagraph>
Tamagui UI is a complete suite of components that render nicely on both React web and
React Native and come in both styled and unstyled forms.
</IntroParagraph>
You can install each component separately, or all of them at once with:
```bash
yarn add tamagui
```
## Setup
The package `tamagui` is a superset of `@tamagui/core`, so if you've already installed core you can change all the references to `tamagui`.
You'll need to add a provider to the root of your app (unlike core, where that is optional), as it will set up the root portal for components like dialogs and popovers.
```tsx
import { createTamagui, TamaguiProvider, View } from 'tamagui'
import { defaultConfig } from '@tamagui/config/v5' // for quick config install this
const config = createTamagui(defaultConfig)
export default () => (
<TamaguiProvider config={config}>
<View />
</TamaguiProvider>
)
```
For a full guide configuration with `createTamagui`, check [the core configuration docs](/docs/core/configuration).
## components/intro/2.0.0
---
title: Tamagui UI
description: React Native UI kit with copy-paste composable components
---
<IntroParagraph>
Tamagui UI is a complete suite of copy-paste composable components that render nicely on
both React Native and React web, in both styled and unstyled forms.
</IntroParagraph>
You can install each component separately, or all of them at once with:
```bash
yarn add tamagui
```
## Setup
The package `tamagui` is a superset of `@tamagui/core`, so if you've already installed core you can change all the references to `tamagui`.
You'll need to add a provider to the root of your app (unlike core, where that is optional), as it will set up the root portal for components like dialogs and popovers.
```tsx
import { createTamagui, TamaguiProvider, View } from 'tamagui'
import { defaultConfig } from '@tamagui/config/v5' // for quick config install this
const config = createTamagui(defaultConfig)
export default () => (
<TamaguiProvider config={config}>
<View />
</TamaguiProvider>
)
```
For a full configuration guide with `createTamagui`, check [the core configuration docs](/docs/core/configuration).
## components/label/1.0.0
---
title: Label
description: Label form elements with accessibility.
name: label
component: Label
package: label
demoName: Label
---
<HeroContainer>
<LabelDemo />
</HeroContainer>
```tsx hero template=Label
```
<Highlights
features={[
`Supports nested controls and custom controls.`,
`Sizable and styleable inline.`,
`Works on web with aria-labelledby.`,
]}
/>
## Installation
Label is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/label
```
## Usage
```tsx
import { Label } from 'tamagui'
export default () => (
<>
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="Nate Wienert" />
</>
)
```
## Accessibility
Use with Input or other form elements to automatically get correct labelling by id and aria-labelledby. You can also use the provided `useLabelContext` hook to build your own controls.
## API Reference
### Label
Labels extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'htmlFor',
type: 'string',
required: true,
description: `Matching to a Tamagui form element id.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
## components/label/2.0.0
---
title: Label
description: Label form elements with accessibility
name: label
component: Label
package: label
demoName: Label
---
<HeroContainer>
<LabelDemo />
</HeroContainer>
```tsx hero template=Label
```
<Highlights
features={[
`Supports nested controls and custom controls.`,
`Sizable and styleable inline.`,
`Works on web with aria-labelledby.`,
]}
/>
## Installation
Label is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/label
```
## Usage
```tsx
import { Label } from 'tamagui'
export default () => (
<>
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="Nate Wienert" />
</>
)
```
## Accessibility
Use with Input or other form elements to automatically get correct labelling by id and aria-labelledby. You can also use the provided `useLabelContext` hook to build your own controls.
Label supports all standard ARIA attributes for form accessibility. These work on both web and React Native - see the [React Native Accessibility docs](https://reactnative.dev/docs/accessibility) for native behavior.
### Required Fields
```tsx
<Label htmlFor="email" aria-required>
Email
</Label>
<Input id="email" aria-required />
```
### Invalid State
```tsx
<Label htmlFor="email" aria-invalid={hasError}>
Email
</Label>
<Input id="email" aria-invalid={hasError} aria-errormessage="email-error" />
{hasError && <Text id="email-error">Please enter a valid email</Text>}
```
### With Description
```tsx
<Label htmlFor="password" aria-describedby="password-hint">
Password
</Label>
<Input id="password" aria-describedby="password-hint" />
<Text id="password-hint">Must be at least 8 characters</Text>
```
## API Reference
### Label
Labels extend SizableText inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'htmlFor',
type: 'string',
required: true,
description: `Matches the \`id\` of a form element to associate with.`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Accessibility Props
Label passes through all ARIA attributes. These are the most commonly used for form labeling:
<PropsTable
data={[
{
name: 'aria-required',
type: 'boolean',
description: `Indicates user input is required before form submission.`,
},
{
name: 'aria-invalid',
type: 'boolean',
description: `Indicates the associated field has a validation error.`,
},
{
name: 'aria-disabled',
type: 'boolean',
description: `Indicates the associated control is disabled.`,
},
{
name: 'aria-describedby',
type: 'string',
description: `References an element providing additional description such as hint text or error messages.`,
},
{
name: 'aria-labelledby',
type: 'string',
description: `References another element that labels this one. \`Label\` sets this automatically on the target input.`,
},
{
name: 'aria-details',
type: 'string',
description: `References an element providing extended description or instructions.`,
},
]}
/>
<Notice theme="green">
These props work cross-platform. On web they render as standard ARIA attributes. On
React Native they map to the native accessibility system. See the [React Native
Accessibility docs](https://reactnative.dev/docs/accessibility) for details.
</Notice>
## components/linear-gradient/1.0.0-alpha
---
title: Linear Gradient
description: Linear Gradient that works with Tamagui style props.
name: html
component: LinearGradient
demoName: LinearGradient
---
# Linear Gradient
<Description>Linear Gradient that works with Tamagui style props.</Description>
<Notice>In beta, currently doesn't resolve theme values for the colors property.</Notice>
<HeroContainer>
<LinearGradientDemo />
</HeroContainer>
```tsx hero template=LinearGradient
```
<Highlights
features={[
'Works on native and web.',
'Bundles nicely with webpack.',
'Accepts Tamagui style props.',
]}
/>
### Usage
We've included `<LinearGradient />` so it can accept Tamagui style props, and to patch a bug with the current expo version with some Webpack configurations.
## components/linear-gradient/1.0.0
---
title: Linear Gradient
description: Linear gradients that work with Tamagui style props
name: html
component: LinearGradient
demoName: LinearGradient
---
<HeroContainer>
<LinearGradientDemo />
</HeroContainer>
```tsx hero template=LinearGradient
```
<Highlights
features={[
'Works on native and web.',
'Accepts Tamagui style props and theme colors.',
'Bundles easily with webpack.',
]}
/>
## Installation
LinearGradient is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/linear-gradient
```
To use this package you'll need to add `expo-linear-gradient` to your app. This works [with vanilla React Native](https://github.com/expo/expo/tree/sdk-47/packages/expo-linear-gradient) or Expo.
## Usage
Because LinearGradient requires a more complex to install native package, we've left it out of the `tamagui` export until Metro supports async import. Import it either separately or using the path `/linear-gradient`:
```tsx
import { LinearGradient } from '@tamagui/linear-gradient'
import { LinearGradient } from 'tamagui/linear-gradient'
```
LinearGradient is a YStack that accepts all Tamagui style props as well as theme colors, that places `expo-linear-gradient` inside it set to absoluteFill.
## API Reference
### LinearGradient
See the [expo docs](https://docs.expo.dev/versions/latest/sdk/linear-gradient/) for more complete information.
LinearGradient extends YStack, inheriting [Stack props](/docs/components/stacks) and therefore the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'colors',
required: true,
type: 'string[]',
description: `Two or more colors.`,
},
{
name: 'locations',
required: false,
type: 'number[] | null',
description: `An array that contains numbers ranging from 0 to 1, inclusive, and is the same length as the colors property. Each number indicates a color-stop location where each respective color should be located.`,
default: '[0.0, 1.0]',
},
{
name: 'start',
required: false,
type: 'LinearGradientPoint | null',
default: '{ x: 0.5, y: 0.0 }',
description: `For example, { x: 0.1, y: 0.2 } means that the gradient will start 10% from the left and 20% from the top.`,
},
{
name: 'end',
required: false,
type: 'LinearGradientPoint | null',
default: '{ x: 0.5, y: 1.0 }',
description: `For example, { x: 0.1, y: 0.2 } means that the gradient will end 10% from the left and 20% from the bottom.`,
},
]}
/>
## components/linear-gradient/2.0.0
---
title: Linear Gradient
description: Linear gradients that work with Tamagui style props
name: linear-gradient
component: LinearGradient
package: linear-gradient
demoName: LinearGradient
---
<HeroContainer>
<LinearGradientDemo />
</HeroContainer>
```tsx hero template=LinearGradient
```
<Highlights
features={[
'Works on native and web.',
'Accepts Tamagui style props and theme colors.',
'Bundles easily with webpack.',
]}
/>
## Installation
LinearGradient is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/linear-gradient
```
### Native Setup
On native, LinearGradient uses `expo-linear-gradient` for rendering. This works with vanilla React Native or Expo.
#### Step 1: Install expo-linear-gradient
```bash
npm install expo-linear-gradient
```
#### Step 2: Import the setup module
In your app's entry file (index.js or App.tsx), before any Tamagui imports:
```tsx
import '@tamagui/native/setup-expo-linear-gradient'
```
That's it! LinearGradient will automatically detect and use expo-linear-gradient when available.
#### Without expo-linear-gradient
If you don't set up expo-linear-gradient, LinearGradient will log a warning on native. On web, LinearGradient uses CSS gradients and doesn't require any additional setup.
## Usage
Because LinearGradient requires a native package, it is not included in the main `tamagui` export. Import it either separately or using the path `/linear-gradient`:
```tsx
import { LinearGradient } from '@tamagui/linear-gradient'
import { LinearGradient } from 'tamagui/linear-gradient'
```
LinearGradient is a YStack that accepts all Tamagui style props as well as theme colors. It places `expo-linear-gradient` inside set to `absoluteFill`.
## API Reference
### LinearGradient
See the [expo docs](https://docs.expo.dev/versions/latest/sdk/linear-gradient/) for more complete information.
#### Alternative: backgroundImage style prop
For most gradients, use the [`backgroundImage` style prop](/docs/intro/styles#backgroundimage):
```tsx
<View backgroundImage="linear-gradient(to bottom, $background, $color)" />
```
Use `<LinearGradient>` for precise color stop control.
---
LinearGradient extends YStack, inheriting [Stack props](/docs/components/stacks) and therefore the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'colors',
required: true,
type: 'string[]',
description: `Two or more colors.`,
},
{
name: 'locations',
required: false,
type: 'number[] | null',
description: `An array that contains numbers ranging from 0 to 1, inclusive, and is the same length as the colors property. Each number indicates a color-stop location where each respective color should be located.`,
default: '[0.0, 1.0]',
},
{
name: 'start',
required: false,
type: 'LinearGradientPoint | null',
default: '{ x: 0.5, y: 0.0 }',
description: `For example, { x: 0.1, y: 0.2 } means that the gradient will start 10% from the left and 20% from the top.`,
},
{
name: 'end',
required: false,
type: 'LinearGradientPoint | null',
default: '{ x: 0.5, y: 1.0 }',
description: `For example, { x: 0.1, y: 0.2 } means that the gradient will end 10% from the left and 20% from the bottom.`,
},
]}
/>
## components/list-item/1.0.0
---
title: ListItem
description: A component for showing columns of items.
name: list-item
component: ListItem
package: list-item
demoName: ListItem
---
<HeroContainer>
<ListItemDemo />
</HeroContainer>
```tsx hero template=ListItem
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Place an icon before or after.',
'Works with themes, animations, Group.',
]}
/>
## Installation
ListItem is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/list-item
```
## Usage
```tsx
import { ListItem } from 'tamagui'
export default () => <ListItem>Lorem ipsum</ListItem>
```
## Sizing
Sizing listItems provides a unique challenge especially for a compiler, because you need to adjust many different properties - not just on the outer frame, but on the text wrapped inside. Tamagui supports adjusting the padding, border radius, font size and icons sizes all in one with the `size` prop.
```tsx
import { ListItem } from 'tamagui'
export default () => <ListItem size="$6">Lorem ipsum</ListItem>
```
Given your theme defines a size `6`, the listItem will adjust all of the properties appropriately. You can also pass a plain number to get an arbitrary size.
## Icon Theming
You can pass icons as either elements or components. If passing components, Tamagui will automatically pass the `size` and `color` prop to them based on your theme.
## Customization
ListItem only supports a limited subset of text props directly, and doesn't accept `hoverStyle` text props. If you need more control, you can do a simple customization:
```tsx
import { forwardRef } from 'react'
import {
ListItemFrame,
ListItemText,
ListItemTitle,
ListItemSubtitle,
styled,
themeable,
useListItem,
} from 'tamagui'
const CustomListItemFrame = styled(ListItemFrame, {
backgroundColor: 'orange', // or "$color", etc.
})
const CustomListItemTitle = styled(ListItemTitle, {
color: 'blue',
})
const CustomListItemSubtitle = styled(ListItemSubtitle, {
color: 'pink',
})
const CustomListItemText = styled(ListItemText, {
color: 'red',
})
export const ListItem = CustomListItemFrame.styleable((propsIn, ref) => {
const { props } = useListItem(propsIn, {
Title: CustomListItemTitle,
Text: CustomListItemText,
Subtitle: CustomListItemSubtitle,
})
return <CustomListItemFrame {...props} ref={ref} />
})
```
There are 3 different components you can customize: `ListItemText`, `ListItemSubtitle` and `ListItemTitle`.
You can include whichever one you want to customize specifically.
If you only want to customize the the text pieces, you don't have to include `CustomListItemFrame`:
```tsx
// all the text changes from above, with a default ListItemFrame
export const ListItem = themeable(
forwardRef<TamaguiElement, ListItemProps>((propsIn, ref) => {
const { props } = useListItem(propsIn, {
Title: CustomListItemTitle,
Text: CustomListItemText,
Subtitle: CustomListItemSubtitle,
})
return <ListItemFrame {...props} ref={ref} />
})
)
```
## API Reference
### ListItem
ListItems extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'title',
required: false,
type: 'React.ReactNode',
description: `Can use either children or title + subTitle to set the contents.`,
},
{
name: 'subTitle',
required: false,
type: 'React.ReactNode',
description: `Sets a subTitle, recommended to use with title.`,
},
{
name: 'size',
required: false,
type: 'string | tokens.size',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'theme',
required: false,
type: 'string',
description: `Apply a theme just to the listItem and it's children`,
},
{
name: 'themeInverse',
required: false,
type: 'boolean',
description: `Helpful for "flipping" any theme between dark and light (including flipping a sub themes defined as [subtheme]-[dark/light]`,
},
{
name: 'noTextWrap',
required: false,
type: 'boolean',
description: `If true, ListItem won't wrap content with a Text element.`,
},
{
name: 'icon',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears before the text.`,
},
{
name: 'iconAfter',
required: false,
type: 'JSX.Element',
description: `Pass any React element, appears after the text.`,
},
{
name: 'scaleIcon',
required: false,
type: 'number',
description: `Scale the icon more than usual by this number.`,
},
{
name: 'scaleSpace',
required: false,
type: 'number',
description: `Scale the spacing more than usual by this number.`,
},
{
name: 'spaceFlex',
required: false,
type: `boolean`,
description: `Makes all space elements have a flex.`,
},
{
name: 'color',
required: false,
type: `SizableTextProps['color']`,
description: `Passes "color" down to the inner text component`,
},
{
name: 'fontWeight',
required: false,
type: `SizableTextProps['fontWeight']`,
description: `Passes "fontWeight" down to the inner text component`,
},
{
name: 'letterSpacing',
required: false,
type: `SizableTextProps['letterSpacing']`,
description: `Passes "letterSpacing" down to the inner text component`,
},
{
name: 'textAlign',
required: false,
type: `SizableTextProps['textAlign']`,
description: `Passes "textAlign" down to the inner text component`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### ListItem.Title
`ListItem.Title` extend `SizableText` inheriting all the [props](/docs/components/text#sizabletext).
### ListItem.Subtitle
`ListItem.Subtitle` extend `SizableText` inheriting all the [props](/docs/components/text#sizabletext).
### ListItem.Text
`ListItem.Text` extend `SizableText` inheriting all the [props](/docs/components/text#sizabletext).
## components/list-item/2.0.0
---
title: ListItem
description: A component for displaying rows of items
name: list-item
component: ListItem
package: list-item
demoName: ListItem
---
<HeroContainer>
<ListItemDemo />
</HeroContainer>
```tsx hero template=ListItem
```
<Highlights
features={[
'Accepts size prop that works on all styles.',
'Place an icon before or after.',
'Works with themes, animations, Group.',
'Supports variant prop (e.g., outlined).',
'Use Apply to pass color/size/variant to children.',
]}
/>
## Installation
ListItem is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/list-item
```
## Usage
Basic usage involves providing children or using the `title` and `subTitle` props.
```tsx
import { ListItem, Separator, YGroup } from 'tamagui'
import { ChevronRight } from '@tamagui/lucide-icons-2' // Example icon
export default () => (
<YGroup
self="center"
borderWidth={1}
borderColor="$borderColor"
rounded="$4"
width={240}
size="$4"
>
<YGroup.Item>
<ListItem title="Star" icon={ChevronRight} />
</YGroup.Item>
<YGroup.Item>
<ListItem title="Moon" subTitle="Subtitle for Moon" iconAfter={ChevronRight} />
</YGroup.Item>
<YGroup.Item>
<ListItem>Custom children content</ListItem>
</YGroup.Item>
</YGroup>
)
```
## Sizing
The `size` prop adjusts various properties of the ListItem, including padding, minimum height, and the default size for text elements within it. It accepts theme size tokens (e.g., `"$3"`, `"$4"`).
```tsx
import { ListItem } from 'tamagui'
export default () => <ListItem size="$5" title="Large List Item" />
```
## Title and SubTitle
Use the `title` and `subTitle` props for a structured layout. These props accept `ReactNode`, so you can pass strings or more complex JSX.
- If `title` or `subTitle` is provided, children will be rendered inside the main text area (under title/subtitle).
- `ListItem.Title` and `ListItem.Subtitle` components are used internally and can be targeted for styling (see Customization).
```tsx
import { ListItem, Avatar } from 'tamagui'
import { User } from '@tamagui/lucide-icons-2'
export default () => (
<ListItem
icon={
<Avatar circular size="$3">
<Avatar.Image src="/placeholder.png" />
<Avatar.Fallback bc="$backgroundFocus" />
</Avatar>
}
title="User Profile"
subTitle="View and edit your profile details."
iconAfter={User}
size="$4"
/>
)
```
## Icon Theming
Icons can be passed to `icon` (before content) or `iconAfter` (after content) props.
- **Automatic Sizing & Spacing**: Icons are automatically sized based on the ListItem's `size` prop. You can override this with the `iconSize` prop (accepts `SizeTokens`). Spacing between the icon and text is also automatically applied (40% of the icon's final size).
- **Scaling**: Use `scaleIcon` (number, default: 1) to further adjust the icon size relative to its base size.
- **Component Props**: If you pass a component as an icon, it will receive `size` (the calculated pixel size) as a prop.
```tsx
import { ListItem } from 'tamagui'
import { Star, Settings } from '@tamagui/lucide-icons-2'
export default () => (
<>
<ListItem icon={Star} title="Default Icon Size" size="$4" />
<ListItem
icon={Settings}
iconSize="$2"
title="Explicit Icon Size"
subTitle="iconSize='$2'"
size="$5"
/>
<ListItem
icon={Star}
scaleIcon={1.5}
title="Scaled Icon"
subTitle="scaleIcon={1.5}"
size="$4"
/>
</>
)
```
## Variant
ListItem supports a `variant` prop for different visual styles:
```tsx
import { ListItem } from 'tamagui'
export default () => <ListItem variant="outlined" title="Outlined Style" />
```
Currently supports `outlined` (transparent background with border).
## Apply (Context)
Use `ListItem.Apply` to pass `color`, `size`, and `variant` to multiple children via context:
```tsx
import { ListItem, YGroup } from 'tamagui'
import { Trash } from '@tamagui/lucide-icons-2'
export default () => (
<YGroup>
<ListItem.Apply color="$red10">
<YGroup.Item>
<ListItem icon={Trash} title="Delete item" />
</YGroup.Item>
<YGroup.Item>
<ListItem icon={Trash} title="Remove all" />
</YGroup.Item>
</ListItem.Apply>
</YGroup>
)
```
The `color` prop passed to `Apply` will be inherited by icons within children.
## Customization
For customization, Tamagui exports the building blocks: `ListItem.Frame`, `ListItem.Text`, `ListItem.Title`, `ListItem.Subtitle`, and `ListItem.Icon`.
```tsx
import { YStack, ListItem as TamaguiListItem } from 'tamagui'
import { GetProps, styled } from '@tamagui/web'
const CustomFrame = styled(TamaguiListItem.Frame, {
padding: '$5',
backgroundColor: '$backgroundHover',
})
const CustomTitle = styled(TamaguiListItem.Title, {
color: '$red10',
fontWeight: 'bold',
})
const CustomSubtitle = styled(TamaguiListItem.Subtitle, {
color: '$gray10',
fontStyle: 'italic',
})
// Recompose, for example, if you need a different internal structure
export const MyAdvancedListItem = ({
icon,
title,
subTitle,
children,
...frameProps
}) => {
return (
<CustomFrame {...frameProps}>
{icon && <TamaguiListItem.Icon>{icon}</TamaguiListItem.Icon>}
<YStack flex={1} justify="center">
{title && <CustomTitle>{title}</CustomTitle>}
{subTitle && <CustomSubtitle>{subTitle}</CustomSubtitle>}
{children && <TamaguiListItem.Text>{children}</TamaguiListItem.Text>}
</YStack>
{/* iconAfter could go here */}
</CustomFrame>
)
}
```
## API Reference
### ListItem
ListItems extend Stack views, inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'title',
required: false,
type: 'React.ReactNode',
description: 'Main text content of the list item. Renders using `ListItem.Title`.',
},
{
name: 'subTitle',
required: false,
type: 'React.ReactNode',
description:
'Secondary text content, rendered below the title. Renders using `ListItem.Subtitle`.',
},
{
name: 'size',
required: false,
type: 'SizeTokens',
description:
'Adjusts padding, min-height, and default font/icon sizes. Uses theme size tokens (e.g., "$3", "$4").',
},
{
name: 'variant',
required: false,
type: "'outlined'",
description:
'Visual variant style. Currently supports "outlined" (transparent background with border).',
},
{
name: 'icon',
required: false,
type: 'JSX.Element | React.ComponentType<{ size?: number }>',
description:
'Icon element or component displayed before the main content. Receives `size` prop if a component.',
},
{
name: 'iconAfter',
required: false,
type: 'JSX.Element | React.ComponentType<{ size?: number }>',
description:
'Icon element or component displayed after the main content. Receives `size` prop if a component.',
},
{
name: 'iconSize',
required: false,
type: 'SizeTokens',
description:
'Explicitly set the size of the icon, overriding the default size derived from the ListItem\'s `size` prop. Uses theme size tokens (e.g., "$2").',
},
{
name: 'scaleIcon',
required: false,
type: 'number',
description:
'Scale factor for the icon (default: 1). Applied after `iconSize` or default size calculation.',
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: 'If true, reduces opacity and disables pointer events.',
},
{
name: 'unstyled',
required: false,
type: 'boolean',
description:
'Removes all default Tamagui styles (padding, background, etc.). Default is `false` unless `TAMAGUI_HEADLESS` is set.',
},
{
name: '// Styling Text',
type: '---',
description:
'To style the `title`, `subTitle`, or general `children` text, use `styled(ListItem, { ... })` to target nested `ListItem.Title`, `ListItem.Subtitle`, or `ListItem.Text` components, or compose your own with these parts. Direct text styling props are not passed for simplicity and performance.',
},
]}
/>
### ListItem.Apply
A context provider that passes `color`, `size`, and `variant` to all ListItem children.
<PropsTable
data={[
{
name: 'color',
required: false,
type: 'ColorTokens | string',
description: 'Color token to pass to icons in children ListItems.',
},
{
name: 'size',
required: false,
type: 'SizeTokens',
description: 'Size token to apply to children ListItems.',
},
{
name: 'variant',
required: false,
type: "'outlined'",
description: 'Variant to apply to children ListItems.',
},
]}
/>
### ListItem.Frame
The base `View` component for the ListItem. Inherits `Stack` props.
### ListItem.Text
Used for wrapping `children` when `title` or `subTitle` are not used. Extends `SizableText`.
### ListItem.Title
Used for rendering the `title` prop. Extends `SizableText`.
### ListItem.Subtitle
Used for rendering the `subTitle` prop. Extends `SizableText`. Its font size is automatically set to one step smaller than the ListItem's main `size`.
### ListItem.Icon
A helper component for rendering icons within ListItem, handling size and color.
Typically used internally but can be leveraged in custom compositions.
## components/lucide-icons/1.0.0
---
title: Lucide Icons
description: Cross-platform compatible SVG based icons
demoName: LucideIcons
---
<HeroContainer noPad>
<LucideIconsDemo />
</HeroContainer>
## Installation
```sh
yarn add react-native-svg @tamagui/lucide-icons-2
```
## Usage
Use them as regular React components
```tsx
import { Button } from 'tamagui'
import { Plus } from '@tamagui/lucide-icons-2'
// Button will automatically pass size/theme to icon
export default () => <Button icon={Plus}>Hello world</Button>
// or you can control it
export default () => <Button icon={<Plus size="$4" />}>Hello world</Button>
```
They accept your tokens/theme keys for color and size.
## Credit
The great [Lucide Icons](https://lucide.dev/), a superset of the wonderful [Feather Icons](https://feathericons.com/).
## components/lucide-icons/2.0.0
---
title: Lucide Icons
description: Cross-platform compatible SVG-based icons
name: lucide-icons
component: LucideIcons
package: lucide-icons
demoName: LucideIcons
---
<HeroContainer noPad>
<LucideIconsDemo />
</HeroContainer>
## Installation
```sh
yarn add react-native-svg @tamagui/lucide-icons-2
```
<Notice>
If you're using icons in a monorepo, install `react-native-svg` in the parent
app or workspace root rather than a leaf package.
</Notice>
## Usage
Use them as regular React components:
```tsx
import { Button } from 'tamagui'
import { Plus } from '@tamagui/lucide-icons-2'
// Button will automatically pass size/theme to icon
export default () => <Button icon={Plus}>Hello world</Button>
// or you can control it
export default () => <Button icon={<Plus size="$4" />}>Hello world</Button>
```
They accept your tokens/theme keys for color and size.
## Credit
The great [Lucide Icons](https://lucide.dev/), a superset of the wonderful [Feather Icons](https://feathericons.com/).
## components/menu/2.0.0
---
title: Menu
description: A selectable list in a popover with nested submenus
name: dropdown menu
component: Menu
package: menu
---
<HeroContainer>
<MenuDemo />
</HeroContainer>
```tsx hero template=Menu
```
<Highlights
features={[
'Full keyboard navigation.',
'Submenus, items, icons, images, checkboxes, groups, and more.',
'Modal and non-modal modes.',
'Native menus on native platforms.',
]}
/>
Menu displays a list of actions or options in a floating panel triggered by a button. It supports nested submenus, keyboard navigation, native platform menus, and [automatically stacks](/ui/z-index) above other content.
## Installation
Menu is already installed in `tamagui`, or you can install it independently:
```bash
yarn add @tamagui/menu
```
If you want to use native menus, add these dependencies:
```sh
yarn add @react-native-menu/menu
yarn add react-native-ios-context-menu
yarn add react-native-ios-utilities
yarn add zeego
yarn add sf-symbols-typescript
```
Then add the setup import at your app entry point:
```tsx
import '@tamagui/native/setup-zeego'
```
<Notice theme="blue">
On native, `Menu.Trigger` coordinates with parent Tamagui pressables
automatically. You should not need extra wrapper views or empty `onPress`
handlers just to prevent a parent `pressStyle` from activating.
</Notice>
<Notice theme="yellow">
**Expo Router users:** This import must run _before_ `expo-router/entry`. Create an `index.js` at your project root that imports the setup first, then expo-router, and update your `package.json` main field to `"index.js"`. See the [upgrade guide](/docs/guides/how-to-upgrade#expo-router-entry-point) for details.
</Notice>
## Anatomy
Import all parts and piece them together.
```jsx
import { Menu } from 'tamagui' // or '@tamagui/menu'
export default () => (
<Menu>
<Menu.Trigger asChild>
<Button />
</Menu.Trigger>
<Menu.Portal zIndex={100}>
<Menu.Content>
<Menu.Item>
<Menu.ItemTitle>About Notes</Menu.ItemTitle>
</Menu.Item>
<Menu.Item>
<Menu.ItemTitle>Settings</Menu.ItemTitle>
</Menu.Item>
{/* when title is nested inside a React element then you need to use `textValue` */}
<Menu.Item textValue="Calendar">
<Menu.ItemTitle>
<Text>Calendar</Text>
</Menu.ItemTitle>
<Menu.ItemIcon>
<Calendar color="gray" size="$1" />
</Menu.ItemIcon>
</Menu.Item>
<Menu.Separator />
<Menu.Sub>
<Menu.SubTrigger>
<Menu.ItemTitle>Actions</Menu.ItemTitle>
</Menu.SubTrigger>
<Menu.Portal zIndex={200}>
<Menu.SubContent>
<Menu.Label fontSize={'$1'}>Note settings</Menu.Label>
<Menu.Item onSelect={onSelect} key="create-note">
<Menu.ItemTitle>Create note</Menu.ItemTitle>
</Menu.Item>
<Menu.Item onSelect={onSelect} key="delete-all">
<Menu.ItemTitle>Delete all notes</Menu.ItemTitle>
</Menu.Item>
<Menu.Item onSelect={onSelect} key="sync-all">
<Menu.ItemTitle>Sync notes</Menu.ItemTitle>
</Menu.Item>
</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
</Menu.Content>
</Menu.Portal>
</Menu>
)
```
## API Reference
### Menu
Contains every component for the Menu.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'Menu parts: Trigger, Portal, Content, Items, etc.',
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `Where the menu appears relative to the trigger. Options: 'top' | 'right' | 'bottom' | 'left' with optional '-start' | '-end' alignment.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: 'Controlled open state for the menu.',
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: 'Initial open state when uncontrolled.',
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
description: 'Called when the menu opens or closes.',
},
{
name: 'onOpenWillChange',
type: '(open: boolean) => void',
required: false,
platform: 'ios',
description: 'Called before the open/close animation begins.',
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description:
'When true, traps focus inside the menu and blocks outside scroll/interactions.',
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
default: '{ padding: 10 }',
description: 'Shifts the menu horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).',
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: 'Flips the menu to the opposite side if there is insufficient space.',
},
{
name: 'offset',
type: 'OffsetOptions',
default: '10',
required: false,
description: 'Distance between the menu and its trigger.',
},
{
name: 'resize',
type: 'boolean',
default: 'true',
required: false,
description: 'Constrains the menu to fit within available viewport space. When enabled, use Menu.ScrollView inside Content for scrollable overflow.',
},
{
name: 'unstyled',
required: false,
type: 'boolean',
description: 'Removes all default Tamagui styles.',
},
]}
/>
### Menu.Portal
Required for rendering the menu content.
<PropsTable
data={[
{
name: 'zIndex',
required: false,
type: 'number',
description: 'Stacking order of the portal layer.',
},
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'Content to render inside the portal.',
},
{
name: 'forceMount',
type: 'true',
required: false,
description:
'Forces the portal to stay mounted, useful for controlling animations.',
},
]}
/>
### Menu.Trigger
The menu will only be triggered when the user right-clicks or long-presses
within the trigger area.
<PropsTable
data={[
{
name: 'action',
required: false,
type: 'press|longPress',
default: 'longPress',
platform: 'android/ios',
description:
"Works with the native prop and accepts 'press' or 'longPress'. The default is 'longPress' for Menu.",
},
]}
/>
### Menu.Content
Contains the content of the menu.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'Menu items, groups, labels, separators, and submenus.',
},
{
name: 'loop',
type: 'boolean',
required: false,
default: 'false',
description: 'Whether keyboard navigation wraps from last to first item.',
},
{
name: 'forceMount',
type: 'true',
required: false,
description:
'Forces the content to stay mounted, useful for controlling animations.',
},
{
name: 'onCloseAutoFocus',
type: '(event: Event) => void',
required: false,
description: 'Called when focus returns to the trigger after closing.',
},
{
name: 'onEscapeKeyDown',
type: '(event: KeyboardEvent) => void',
required: false,
description: 'Called when the escape key is pressed. Can be prevented.',
},
{
name: 'onPointerDownOutside',
type: '(event: PointerEvent) => void',
required: false,
description: 'Called when a pointer event occurs outside the content.',
},
{
name: 'onInteractOutside',
type: '(event: Event) => void',
required: false,
description: 'Called when any interaction occurs outside the content.',
},
]}
/>
### Menu.Item
A selectable menu item that triggers an action when selected.
<PropsTable
data={[
{
name: 'key',
type: 'string',
required: true,
description: 'Unique identifier for the item.',
},
{
name: 'disabled',
type: 'boolean',
required: false,
default: 'false',
description: 'Prevents interaction and dims the item.',
},
{
name: 'destructive',
type: 'boolean',
required: false,
platform: 'ios/android',
description:
'Renders the item in red on iOS to indicate a dangerous action (e.g. delete). No effect on web.',
},
{
name: 'hidden',
type: 'boolean',
required: false,
description: 'Hides the item from the menu.',
},
{
name: 'onSelect',
type: '(event?: Event) => void',
required: false,
description: 'Called when the item is selected via click or keyboard.',
},
{
name: 'onFocus',
type: '() => void',
required: false,
description: 'Called when the item receives focus.',
},
{
name: 'onBlur',
type: '() => void',
required: false,
description: 'Called when the item loses focus.',
},
{
name: 'textValue',
type: 'string',
required: false,
platform: 'ios/android',
description:
'Text used for typeahead and native menus. Required when ItemTitle contains a React node instead of a string.',
},
]}
/>
### Menu.ItemTitle
Renders the title of the menu item.
<PropsTable
data={[
{
name: 'children',
type: 'string | React.ReactNode',
required: true,
description: 'The title text or element to display.',
},
]}
/>
<Notice>
You can directly pass a text node to the ItemTitle. However, if you use a nested
React node like `<Text>`, you need to pass `textValue` to the `<Item>` so that it
works with native menus.
</Notice>
### Menu.ItemIcon
A component to render an icon. For non-native menus, you can pass an icon
component. For native menus, you can pass platform-specific icons to the
`android` and `ios` props.
On iOS, it renders the native
[SF Symbols](https://github.com/andrewtavis/sf-symbols-online) icons.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: false,
description: 'Fallback icon for web when native icons are not available.',
},
{
name: 'ios',
type: 'object',
required: false,
platform: 'ios',
description:
'SF Symbol configuration: name, weight, scale, hierarchicalColor, paletteColors.',
},
{
name: 'android',
type: 'object',
required: false,
platform: 'android',
description: 'Android resource drawable name.',
},
]}
/>
```jsx line=1
<Menu.ItemIcon
ios={{
name: '0.circle.fill', // required
pointSize: 5,
weight: 'semibold',
scale: 'medium',
// can also be a color string. Requires iOS 15+
hierarchicalColor: {
dark: 'blue',
light: 'green',
},
// alternative to hierarchical color. Requires iOS 15+
paletteColors: [
{
dark: 'blue',
light: 'green',
},
],
}}
>
<CircleIcon />
</Menu.ItemIcon>
```
### Menu.ItemImage
A component to render an item image. For native menus, it only works on iOS. It
takes the same props as `@tamagui/image`.
### Menu.ItemSubtitle
A component to render a subtitle for the menu item. For native menus, it only
works on iOS.
<PropsTable
data={[
{
name: 'children',
type: 'string',
required: true,
description: 'The subtitle text to display below the title.',
},
]}
/>
### Menu.Group
A component that groups multiple menu items together.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'Menu items to group together.',
},
]}
/>
### Menu.CheckboxItem
A menu item with a checkbox that can be toggled on and off.
<PropsTable
data={[
{
name: 'key',
type: 'string',
required: true,
description: 'Unique identifier for the checkbox item.',
},
{
name: 'disabled',
type: 'boolean',
required: false,
default: 'false',
description: 'Prevents interaction and dims the item.',
},
{
name: 'destructive',
type: 'boolean',
required: false,
platform: 'ios/android',
description:
'Renders the item in red on iOS to indicate a dangerous action. No effect on web.',
},
{
name: 'hidden',
type: 'boolean',
required: false,
description: 'Hides the item from the menu.',
},
{
name: 'onFocus',
type: '() => void',
required: false,
description: 'Called when the item receives focus.',
},
{
name: 'onBlur',
type: '() => void',
required: false,
description: 'Called when the item loses focus.',
},
{
name: 'textValue',
type: 'string',
required: false,
platform: 'ios/android',
description:
'Text for native menus. Required when ItemTitle contains a React node.',
},
{
name: 'value',
type: "'on' | 'off' | 'mixed'",
required: false,
platform: 'ios/android',
description: 'Controlled checked state for native menus.',
},
{
name: 'onValueChange',
type: '(state, prevState) => void',
required: false,
platform: 'ios/android',
description: 'Called when checked state changes on native menus.',
},
{
name: 'checked',
type: 'boolean',
required: false,
description: 'Controlled checked state for web menus.',
},
{
name: 'onCheckedChange',
type: '(checked: boolean) => void',
required: false,
description: 'Called when checked state changes on web.',
},
]}
/>
### Menu.RadioGroup
Groups a set of `Menu.RadioItem` and manages the selected value. Works on web
and native — on native the group is rendered as a native menu group and each
`RadioItem` is mapped onto the OS checkmark state so exactly one item shows as
selected at a time.
```tsx
const [color, setColor] = React.useState('blue')
<Menu.RadioGroup value={color} onValueChange={setColor}>
<Menu.RadioItem key="red" value="red">
<Menu.ItemIndicator>
<Check size={16} />
</Menu.ItemIndicator>
<Menu.ItemTitle>Red</Menu.ItemTitle>
</Menu.RadioItem>
<Menu.RadioItem key="blue" value="blue">
<Menu.ItemIndicator>
<Check size={16} />
</Menu.ItemIndicator>
<Menu.ItemTitle>Blue</Menu.ItemTitle>
</Menu.RadioItem>
</Menu.RadioGroup>
```
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'One or more Menu.RadioItem components.',
},
{
name: 'value',
type: 'string',
required: false,
description: 'The controlled value of the currently selected item.',
},
{
name: 'onValueChange',
type: '(value: string) => void',
required: false,
description: 'Called when the selected value changes.',
},
]}
/>
### Menu.RadioItem
An item that can be selected inside a `Menu.RadioGroup`. Render a
`Menu.ItemIndicator` inside to show a checkmark for the active value on web;
on native the OS draws its own checkmark and the `ItemIndicator` child is
dropped automatically.
<PropsTable
data={[
{
name: 'key',
type: 'string',
required: true,
description: 'Unique identifier for the radio item.',
},
{
name: 'value',
type: 'string',
required: true,
description:
'The value of this item. When it matches the parent RadioGroup value, the item is selected.',
},
{
name: 'disabled',
type: 'boolean',
required: false,
default: 'false',
description: 'Prevents interaction and dims the item.',
},
{
name: 'hidden',
type: 'boolean',
required: false,
description: 'Hides the item from the menu.',
},
{
name: 'onFocus',
type: '() => void',
required: false,
description: 'Called when the item receives focus.',
},
{
name: 'onBlur',
type: '() => void',
required: false,
description: 'Called when the item loses focus.',
},
]}
/>
### Menu.ItemIndicator
Use inside `Menu.CheckboxItem` or `Menu.RadioItem` to indicate when an item is checked.
This allows you to conditionally render a checkmark.
```jsx line=1
<Menu.ItemIndicator>
<CheckmarkIcon /> {/* This does not work with the native prop. */}
</Menu.ItemIndicator>
```
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: false,
description: 'Custom checkmark icon. Only works on web.',
},
{
name: 'forceMount',
type: 'true',
required: false,
description: 'Forces the indicator to stay mounted for animation control.',
},
]}
/>
### Menu.Label
Renders a non-focusable label for a group of items. On native menus, only one
label is supported per menu and submenu.
<PropsTable
data={[
{
name: 'children',
type: 'string',
required: true,
description: 'The label text.',
},
{
name: 'textValue',
type: 'string',
required: false,
platform: 'ios/android',
description: 'Text for native menus when children is a React node.',
},
]}
/>
### Menu.Arrow
Renders an arrow pointing to the trigger.
<PropsTable
data={[
{
name: 'size',
type: 'number | SizeToken',
required: false,
description: 'Width and height of the arrow.',
},
{
name: 'unstyled',
type: 'boolean',
required: false,
description: 'Removes default arrow styles.',
},
]}
/>
### Menu.Separator
Renders a visual divider between menu items. Web only.
### Menu.Sub
A container for nested submenu components.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: 'SubTrigger, Portal, and SubContent components.',
},
{
name: 'open',
type: 'boolean',
required: false,
description: 'Controlled open state for the submenu.',
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
description: 'Called when the submenu opens or closes.',
},
]}
/>
### Menu.SubContent
Renders the content of a submenu. Same props as `Menu.Content`, excluding `side` and
`align`.
### Menu.SubTrigger
A menu item that opens a submenu on hover or focus. Accepts the same props as
`Menu.Item`.
### Menu.ScrollView
A scrollable container for menu items. Use this inside `Menu.Content` when you have many items that may overflow. The menu automatically constrains to available viewport space (via `resize` prop), and ScrollView handles the overflow. Scrollbars are hidden by default.
```jsx
<Menu.Content>
<Menu.ScrollView>
{/* Many menu items */}
</Menu.ScrollView>
</Menu.Content>
```
## Styling
### Item Highlight Behavior
Menu items use `focusStyle` for highlighting rather than `hoverStyle`. This ensures a unified highlight experience when switching between mouse and keyboard navigation - only one item is ever highlighted at a time.
When you hover over an item, it receives focus, which triggers the `focusStyle`. When you use arrow keys to navigate, focus moves to the new item, removing the highlight from the previous one.
```jsx
// Default behavior - uses focusStyle for highlight
<Menu.Item>
<Menu.ItemTitle>Settings</Menu.ItemTitle>
</Menu.Item>
// Custom highlight styling - use focusStyle, not hoverStyle
<Menu.Item
focusStyle={{ backgroundColor: '$blue5' }}
>
<Menu.ItemTitle>Custom Highlight</Menu.ItemTitle>
</Menu.Item>
```
<Notice>
Avoid using `hoverStyle` for background highlights on Menu.Item. This can cause "double
highlighting" when switching between mouse and keyboard - where both the hovered item
and the focused item appear highlighted simultaneously.
</Notice>
## components/native/2.0.0
---
title: Native UI
description: Optional native integrations for better performance and UX
name: native
---
# Native Integrations
Tamagui components work great out of the box, but several offer optional native
integrations for improved performance and platform-native UX. All native
integrations are opt-in - components work without them, just with slightly
different behavior.
## @tamagui/native Package
The `@tamagui/native` package provides setup utilities for native integrations.
Import the setup modules at your app's entry point, before any Tamagui imports.
```bash
npm install @tamagui/native
```
<Notice>
`@tamagui/native` is a light setup package. When you use one of its optional
integrations, make sure the matching native dependency is installed in your
app as well, such as `react-native-gesture-handler`, `react-native-teleport`,
`zeego`, or `burnt`. In a monorepo, install those native packages in the
parent app or workspace root rather than a leaf package.
</Notice>
## Portal
**Native portals preserve React context** - the default implementation on native
uses a JS-based approach that breaks React context, or requires you to hoist data
awkwardly. With native portals, your custom contexts (navigation, app state)
work inside Sheet, Dialog, Popover, and other portaled content.
### Setup
```bash
npm install react-native-teleport
```
```tsx
// App.tsx - before any Tamagui imports
import '@tamagui/native/setup-teleport'
```
See [Portal docs](/docs/components/portal) for full details.
## Gesture Handler
**Native gesture handling** - powers two features:
1. **Press events** - `onPress` uses RNGH for more reliable press detection with
proper gesture coordination. Matches RN Pressable semantics where only the
innermost pressable fires (no double-firing with nested buttons).
2. **Sheet gestures** - makes sheets feel smoother and not get confused with
scrollable content during drags.
### Setup
```bash
npm install react-native-gesture-handler
```
```tsx
// App.tsx - before any Tamagui imports
import '@tamagui/native/setup-gesture-handler'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>{/* Your app */}</GestureHandlerRootView>
)
}
```
### Selective Features
If you only want RNGH for specific features, use the configurable setup:
```tsx
import { setupGestureHandler } from '@tamagui/native/setup-gesture-handler'
// only use RNGH for sheets, not press events
setupGestureHandler({ pressEvents: false, sheet: true })
// only use RNGH for press events, not sheets
setupGestureHandler({ pressEvents: true, sheet: false })
```
### Press Event Behavior
With RNGH enabled, `onPress` follows RN Pressable semantics:
- **Nested pressables** - only the innermost fires (no event bubbling)
- **Web parity** - `onPress` calls `stopPropagation()` automatically
- **Use `onClick` on web** if you need standard DOM event bubbling
This aligns to the React Native default behavior of Pressable.
### PressBoundary
If you need a non-Tamagui native wrapper to block a parent Tamagui pressable,
use `PressBoundary` from `@tamagui/native/gesture-handler`.
This is useful when mixing Tamagui pressables with React Native `Pressable`,
custom native wrappers, or other components that don't use Tamagui's native
press ownership automatically.
```tsx
import { PressBoundary } from '@tamagui/native/gesture-handler'
import { Pressable } from 'react-native'
export function AvatarLink() {
return (
<PressBoundary debugName="AvatarLink">
<Pressable onPress={() => {}}>
{/* your content */}
</Pressable>
</PressBoundary>
)
}
```
`PressBoundary` only coordinates with Tamagui's native press ownership. It does
not monkey-patch React Native bubbling semantics in general.
Native `Menu.Trigger` and `ContextMenu.Trigger` already do this automatically,
so you should not need extra wrappers or empty `onPress` handlers around them.
See [Sheet docs](/docs/components/sheet) for full details.
## Menu & ContextMenu
**Native menus** - renders platform-native context menus instead of custom
overlays. Uses iOS's native menu API for haptics, blur effects, and system
integration.
### Setup
```bash
npm install zeego @react-native-menu/menu react-native-ios-context-menu react-native-ios-utilities sf-symbols-typescript
```
```tsx
// App.tsx - before any Tamagui imports
import '@tamagui/native/setup-zeego'
```
Then use the `native` prop:
```tsx
<Menu native>{/* menu content */}</Menu>
```
See [Menu docs](/docs/components/menu) and
[ContextMenu docs](/docs/components/context-menu) for full details.
## Toast
**Native toasts** - uses platform-native toast implementations:
- iOS: SPIndicator (system-style indicators)
- Android: ToastAndroid
- Web: Notification API
### Setup
```bash
npm install burnt
```
```tsx
// App.tsx - before any Tamagui imports
import '@tamagui/native/setup-burnt'
```
Then use the `native` prop on ToastProvider:
```tsx
<Toast.Provider native>{/* your app */}</Toast.Provider>
```
See [Toast docs](/docs/components/toast) for full details.
## LinearGradient
**Native gradients** - uses `expo-linear-gradient` for high-performance native gradient rendering.
### Setup
```bash
npm install expo-linear-gradient
```
```tsx
// App.tsx - before any Tamagui imports
import '@tamagui/native/setup-expo-linear-gradient'
```
See [LinearGradient docs](/docs/components/linear-gradient) for full details.
## components/new-inputs/1.0.0
---
title: Input & Textarea
name: inputs
component: Input
demoName: Inputs
---
<HeroContainer demoMultiple>
<NewInputsDemo />
</HeroContainer>
```tsx hero template=Inputs
```
Using Web APIs and relying on bare Tamagui with no `react-native-web` depedency on web compared to old Input component, support scaling all the styles up or down using the `size` property, and full `theme` support.
## Installation
LinearGradient is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/input
```
## Input
A one-line input field:
```tsx
import { Input } from 'tamagui'
export const App = () => (
// Accepts size and style properties directly
<Input gap="$4" borderWidth={2} />
)
```
## TextArea
For multi-line inputs:
```tsx
import { TextArea } from 'tamagui'
export const App = () => (
// Accepts size and style properties directly
<TextArea gap="$4" borderWidth={2} />
)
```
## components/new-inputs/2.0.0
---
title: Input & TextArea
description: Single-line and multi-line text inputs with web-first API
name: inputs
component: Input
package: input
demoName: Inputs
---
<HeroContainer demoMultiple>
<NewInputsDemo />
</HeroContainer>
```tsx hero template=Inputs
```
The v2 Input uses web-standard HTML APIs as the primary interface, with automatic conversion to React Native equivalents on native platforms. This means you write web-first code that works everywhere.
## Installation
Input is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/input
```
## Usage
A single-line text input:
```tsx
import { Input } from 'tamagui'
export default () => <Input placeholder="Enter text..." size="$4" />
```
For multi-line text input, use `TextArea`:
```tsx
import { TextArea } from 'tamagui'
export default () => <TextArea placeholder="Enter long text..." rows={4} />
```
## Web-First API
The v2 Input uses standard HTML input attributes. Here are the key props:
### Input Types
Use the standard HTML `type` attribute. On native, these automatically map to the appropriate keyboard and behavior:
```tsx
// Password input
<Input type="password" />
// Email input (shows email keyboard on native)
<Input type="email" />
// Phone number (shows phone pad on native)
<Input type="tel" />
// Number input (shows numeric keyboard on native)
<Input type="number" />
// URL input
<Input type="url" />
// Search input
<Input type="search" />
```
**Cross-platform mapping:**
| Web `type` | Native behavior |
| ---------- | ------------------------------ |
| `password` | `secureTextEntry={true}` |
| `email` | `keyboardType="email-address"` |
| `tel` | `keyboardType="phone-pad"` |
| `number` | `keyboardType="numeric"` |
| `url` | `keyboardType="url"` |
| `search` | `inputMode="search"` |
### Enter Key Behavior
Use `enterKeyHint` to control the enter/return key label on virtual keyboards:
```tsx
<Input enterKeyHint="search" />
<Input enterKeyHint="send" />
<Input enterKeyHint="done" />
<Input enterKeyHint="go" />
<Input enterKeyHint="next" />
```
On native, this automatically maps to `returnKeyType`.
### Form Attributes
Standard HTML form attributes work as expected:
```tsx
<Input
name="email"
placeholder="Enter email"
required
maxLength={100}
minLength={3}
pattern="[a-z@.]+"
autoComplete="email"
autoFocus
readOnly={false}
disabled={false}
/>
```
### Events
Use standard web events:
```tsx
<Input
onChange={(e) => console.log(e.target.value)}
onFocus={(e) => console.log('focused')}
onBlur={(e) => console.log('blurred')}
onKeyDown={(e) => {
if (e.key === 'Enter') {
// handle enter
}
}}
/>
```
For convenience, `onChangeText` is still supported but deprecated:
```tsx
// Deprecated - use onChange instead
<Input onChangeText={(text) => setText(text)} />
// Preferred
<Input onChange={(e) => setText(e.target.value)} />
```
### Submit Handling
For handling form submission on enter:
```tsx
<Input
onSubmitEditing={(e) => {
console.log('Submitted:', e.nativeEvent.text)
}}
/>
```
## Styling
Input accepts all Tamagui style props and supports the `size` prop for consistent scaling:
```tsx
<Input
size="$4"
borderWidth={2}
borderColor="$borderColor"
borderRadius="$4"
backgroundColor="$background"
color="$color"
placeholderTextColor="$placeholderColor"
selectionColor="$accentColor"
hoverStyle={{
borderColor: '$borderColorHover',
}}
focusStyle={{
borderColor: '$borderColorFocus',
}}
/>
```
## Native-Only Props
Some props only apply on native platforms and have no web equivalent:
**keyboardAppearance (iOS)**: Controls the keyboard color scheme on iOS:
```tsx
<Input keyboardAppearance="dark" />
<Input keyboardAppearance="light" />
<Input keyboardAppearance="default" />
```
**textContentType (iOS)**: Provides hints for iOS autofill:
```tsx
<Input textContentType="emailAddress" />
<Input textContentType="password" />
<Input textContentType="newPassword" />
<Input textContentType="oneTimeCode" />
<Input textContentType="telephoneNumber" />
<Input textContentType="username" />
```
For web autofill, use the standard `autoComplete` attribute instead.
## Cross-Platform Text Behavior
These props work on both web and native with automatic value conversion:
### autoCorrect
Controls automatic spelling correction:
```tsx
// Boolean values (work everywhere)
<Input autoCorrect={false} />
<Input autoCorrect={true} />
// String values (web-style, converted on native)
<Input autoCorrect="off" />
<Input autoCorrect="on" />
```
### autoCapitalize
Controls automatic text capitalization. Native values provide more granular control:
```tsx
// Native-style values (recommended - work everywhere)
<Input autoCapitalize="none" /> // No capitalization
<Input autoCapitalize="sentences" /> // Capitalize first letter of sentences
<Input autoCapitalize="words" /> // Capitalize first letter of each word
<Input autoCapitalize="characters" /> // Capitalize all characters
// Web-style values (mapped on native)
<Input autoCapitalize="off" /> // Maps to "none" on native
<Input autoCapitalize="on" /> // Maps to "sentences" on native
```
## Migration from v1
If you're migrating from v1 Input, here are the key changes:
| v1 (React Native style) | v2 (Web style) |
| ------------------------------ | ---------------------------- |
| `secureTextEntry` | `type="password"` |
| `keyboardType="email-address"` | `type="email"` |
| `keyboardType="phone-pad"` | `type="tel"` |
| `keyboardType="numeric"` | `type="number"` |
| `keyboardType="url"` | `type="url"` |
| `returnKeyType="search"` | `enterKeyHint="search"` |
| `editable={false}` | `readOnly` or `disabled` |
| `onChangeText` | `onChange` |
| `multiline` | Use `TextArea` or `rows > 1` |
| `numberOfLines` | `rows` |
The v1 props still work but are deprecated. We recommend updating to the web-standard props for better cross-platform consistency.
## API Reference
### Input Props
Accepts all HTML `<input>` attributes plus Tamagui style props.
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'SizeTokens',
description: 'Scale the input size consistently.',
},
{
name: 'type',
required: false,
type: '"text" | "password" | "email" | "tel" | "number" | "url" | "search"',
description: 'HTML input type. Automatically maps to native keyboard types.',
},
{
name: 'enterKeyHint',
required: false,
type: '"enter" | "done" | "go" | "next" | "previous" | "search" | "send"',
description: 'Hint for the enter key label. Maps to returnKeyType on native.',
},
{
name: 'placeholderTextColor',
required: false,
type: 'ColorTokens',
description: 'Color of placeholder text. Accepts Tamagui color tokens.',
},
{
name: 'selectionColor',
required: false,
type: 'ColorTokens',
description: 'Color of text selection. Accepts Tamagui color tokens.',
},
{
name: 'onSubmitEditing',
required: false,
type: '(e: { nativeEvent: { text: string } }) => void',
description: 'Called when enter/return is pressed.',
},
{
name: 'keyboardAppearance',
required: false,
type: '"default" | "light" | "dark"',
description: 'iOS only. Controls keyboard color scheme.',
},
{
name: 'textContentType',
required: false,
type: 'string',
description: 'iOS only. Hints for autofill. Use autoComplete for web.',
},
]}
/>
### TextArea Props
Accepts all Input props plus:
<PropsTable
data={[
{
name: 'rows',
required: false,
type: 'number',
description: 'Number of visible text lines.',
},
]}
/>
## components/popover/1.0.0
---
title: Popover
description: A simple popover component
name: popover
component: Popover
package: popover
demoName: Popover
---
# Popover
<Description>Show content with a trigger in a floating pane.</Description>
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is pressed, floating above the current content.
<Notice>
Support for native was contributed by a community member, but not guaranteed to work
until we get time to fully test and improve native interactions. We don't recommend the
Popover pattern for phone-sized devices either way, and instead you can adapt it to a
Sheet.
</Notice>
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
## Anatomy
```tsx
import { Popover } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
{/* optionally change to sheet when small screen */}
<Popover.Adapt when="maxMd">
<Popover.Sheet>
<Popover.Sheet.Overlay />
<Popover.Sheet.Frame>
<Popover.Sheet.ScrollView>
<Popover.Adapt.Contents />
</Popover.Sheet.ScrollView>
</Popover.Sheet.Frame>
</Popover.Sheet>
</Popover.Adapt>
</Popover>
)
```
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description: `Renders into root of app instead of inline.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
description: `Keeps the Popover inside the frame, see floating-ui shift().`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack, see [Stacks](/docs/components/stacks).
### Popover.Content
Renders as SizableStack which is just a YStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
Used to display the content of the popover.
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover attaches, use Anchor. When used, Anchor is where the Popover will attach, while Trigger will open it.
### Popover.Sheet
When used with `Adapt`, Popover will render as a sheet when that breakpoint is active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Popover.Sheet.Frame` to insert the contents given to `Popover.Content`
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove this ScrollView when swapping into the Sheet, as the Sheet must use it's own ScrollView that handles special logic for interactions with dragging.
## components/popover/1.110.0
---
title: Popover
description: Show content in a floating pane.
name: popover
component: Popover
package: popover
demoName: Popover
---
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is pressed, floating above the current content.
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
### PortalProvider
When rendering into root of app instead of inline, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Popover, Adapt } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
{/* optionally change to sheet when small screen */}
{/* you can also use <Popover.Adapt /> */}
<Adapt when="maxMd">
<Popover.Sheet>
<Popover.Sheet.Overlay />
<Popover.Sheet.Frame>
<Popover.Sheet.ScrollView>
<Adapt.Contents />
</Popover.Sheet.ScrollView>
</Popover.Sheet.Frame>
</Popover.Sheet>
</Adapt>
</Popover>
)
```
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'keepChildrenMounted',
type: 'boolean',
required: false,
description: `By default, Popover removes children from DOM/rendering when fully hidden. Setting true will keep children mounted even when hidden. This can be beneficial for performance if your popover content is expensive to render.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
description: `Keeps the Popover inside the frame, see floating-ui shift().`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'hoverable',
type: 'boolean | UseFloatingProps',
required: false,
description: `Allows hovering on the trigger to open the popover. See UseFloatingProps from floating-ui: accepts boolean or object of { delay: number, restMs: number, handleClose: Function, mouseOnly: boolean, move: boolean }`,
},
]}
/>
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/popover/1.83.0#portalprovider) for more information.
</Notice>
### Popover.Arrow
Popover.Arrow can be used to show an arrow that points at the Trigger element. In order for the Arrow to show you must have a Trigger element within your Popover. Arrows extend YStack, see [Stacks](/docs/components/stacks).
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack, see [Stacks](/docs/components/stacks).
### Popover.Content
Renders as SizableStack which is just a YStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
Used to display the content of the popover.
<PropsTable
data={[
{
name: 'size',
type: 'SizeTokens',
required: false,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover attaches, use Anchor. When used, Anchor is where the Popover will attach, while Trigger will open it.
### Popover.Sheet
When used with `Adapt`, Popover will render as a sheet when that breakpoint is active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Popover.Sheet.Frame` to insert the contents given to `Popover.Content`
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove this ScrollView when swapping into the Sheet, as the Sheet must use it's own ScrollView that handles special logic for interactions with dragging.
## components/popover/1.125.0
---
title: Popover
description: Show content with a trigger in a floating pane.
name: popover
component: Popover
package: popover
demoName: Popover
---
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is pressed, floating above the current content.
<Notice>
Note: Popovers are not a recommended pattern for mobile apps, and so aren't supported on
native. Instead you can use Adapt and render them as a Sheet, or just conditionally
render them. We landed support for them at one point, but we need the community to
contribute tests in order to support them for mobile again.
</Notice>
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
### PortalProvider
When rendering into root of app instead of inline, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Popover, Adapt } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
{/* optionally change to sheet when small screen */}
{/* you can also use <Popover.Adapt /> */}
<Adapt when="maxMd">
<Popover.Sheet>
<Popover.Sheet.Overlay />
<Popover.Sheet.Frame>
<Popover.Sheet.ScrollView>
<Adapt.Contents />
</Popover.Sheet.ScrollView>
</Popover.Sheet.Frame>
</Popover.Sheet>
</Adapt>
</Popover>
)
```
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'keepChildrenMounted',
type: 'boolean',
required: false,
description: `By default, Popover removes children from DOM/rendering when fully hidden. Setting true will keep children mounted even when hidden. This can be beneficial for performance if your popover content is expensive to render.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
description: `Keeps the Popover inside the frame, see floating-ui shift().`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'hoverable',
type: 'boolean | UseFloatingProps',
required: false,
description: `Allows hovering on the trigger to open the popover. See UseFloatingProps from floating-ui: accepts boolean or object of { delay: number, restMs: number, handleClose: Function, mouseOnly: boolean, move: boolean }`,
},
{
name: 'resize',
type: 'SizeProps | boolean',
required: false,
description: `Will set maxWidth and maxHeight of Content to fit inside outer window when it won't fit, see floating-ui size().`,
},
]}
/>
For most of these properties, you'll want to reference the [floating-ui docs](https://floating-ui.com/docs/getting-started).
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/popover/1.83.0#portalprovider) for more information.
</Notice>
### Popover.Arrow
Popover.Arrow can be used to show an arrow that points at the Trigger element. In order for the Arrow to show you must have a Trigger element within your Popover. Arrows extend YStack, see [Stacks](/docs/components/stacks).
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack, see [Stacks](/docs/components/stacks).
### Popover.Content
Renders as SizableStack which is just a YStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
Used to display the content of the popover.
<PropsTable
data={[
{
name: 'size',
type: 'SizeTokens',
required: false,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover attaches, use Anchor. When used, Anchor is where the Popover will attach, while Trigger will open it.
### Popover.Sheet
When used with `Adapt`, Popover will render as a sheet when that breakpoint is active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Popover.Sheet.Frame` to insert the contents given to `Popover.Content`
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove this ScrollView when swapping into the Sheet, as the Sheet must use it's own ScrollView that handles special logic for interactions with dragging.
## components/popover/1.128.0
---
title: Popover
description: Show content with a trigger in a floating pane.
name: popover
component: Popover
package: popover
demoName: Popover
---
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is
pressed, floating above the current content.
<Notice>
Note: Popovers are not a recommended pattern for mobile apps, and so aren't supported on
native. Instead you can use Adapt and render them as a Sheet, or just conditionally
render them. We landed support for them at one point, but we need the community to
contribute tests in order to support them for mobile again.
</Notice>
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
### PortalProvider
When rendering into root of app instead of inline, you'll first need to install
the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Popover, Adapt } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
{/* Optional: Control focus behavior */}
<Popover.FocusScope loop trapped focusOnIdle={true}>
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
</Popover.FocusScope>
{/* optionally change to sheet when small screen */}
{/* you can also use <Popover.Adapt /> */}
<Adapt when="maxMd">
<Popover.Sheet>
<Popover.Sheet.Overlay />
<Popover.Sheet.Frame>
<Popover.Sheet.ScrollView>
<Adapt.Contents />
</Popover.Sheet.ScrollView>
</Popover.Sheet.Frame>
</Popover.Sheet>
</Adapt>
</Popover>
)
```
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'keepChildrenMounted',
type: 'boolean',
required: false,
description: `By default, Popover removes children from DOM/rendering when fully hidden. Setting true will keep children mounted even when hidden. This can be beneficial for performance if your popover content is expensive to render.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
description: `Keeps the Popover inside the frame, see floating-ui shift().`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'hoverable',
type: 'boolean | UseFloatingProps',
required: false,
description: `Allows hovering on the trigger to open the popover. See UseFloatingProps from floating-ui: accepts boolean or object of { delay: number, restMs: number, handleClose: Function, mouseOnly: boolean, move: boolean }`,
},
{
name: 'resize',
type: 'SizeProps | boolean',
required: false,
description: `Will set maxWidth and maxHeight of Content to fit inside outer window when it won't fit, see floating-ui size().`,
},
]}
/>
For most of these properties, you'll want to reference the
[floating-ui docs](https://floating-ui.com/docs/getting-started).
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/popover/1.83.0#portalprovider) for more information.
</Notice>
### Popover.Arrow
Popover.Arrow can be used to show an arrow that points at the Trigger element.
In order for the Arrow to show you must have a Trigger element within your
Popover. Arrows extend YStack, see [Stacks](/docs/components/stacks).
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack,
see [Stacks](/docs/components/stacks).
### Popover.Content
Extends PopperContent which extends SizableStack which extends a YStack (see
[Stacks](/docs/components/stacks)).
Also extends
Used to display the content of the popover.
<PropsTable
data={[
{
name: 'enableAnimationForPositionChange',
type: 'boolean',
description:
'Disabled animate presence animations in favor of regular animation, useful for doing sliding popovers.',
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: 'Controls default padding/borderRadius when unstyled is false.',
},
{
name: 'unstyled',
required: false,
type: `boolean`,
default: false,
description: `Removes all default Tamagui styles.`,
},
{
name: 'trapFocus',
type: 'boolean',
default: false,
description: 'Whether focus should be trapped within the `Popover`',
},
{
name: 'disableFocusScope',
type: 'boolean',
default: false,
description: 'Whether popover should not focus contents on open',
},
{
name: 'onOpenAutoFocus',
type: `FocusScopeProps['onMountAutoFocus']`,
default: false,
description: 'Event handler called when auto-focusing on open. Can be prevented.',
},
{
name: 'onCloseAutoFocus',
type: `FocusScopeProps['onUnmountAutoFocus'] | false`,
default: false,
description: 'Event handler called when auto-focusing on close. Can be prevented.',
},
{
name: 'lazyMount',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover
attaches, use Anchor. When used, Anchor is where the Popover will attach, while
Trigger will open it.
### Popover.Sheet
When used with `Adapt`, Popover will render as a sheet when that breakpoint is
active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Popover.Sheet.Frame` to insert the
contents given to `Popover.Content`
### Popover.FocusScope
Provides access to the underlying FocusScope component used by Popover for focus
management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If
used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove
this ScrollView when swapping into the Sheet, as the Sheet must use it's own
ScrollView that handles special logic for interactions with dragging.
## components/popover/1.129.0
---
title: Popover
description: Show content with a trigger in a floating pane.
name: popover
component: Popover
package: popover
demoName: Popover
---
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is
pressed, floating above the current content.
<Notice>
Note: Popovers are not a recommended pattern for mobile apps, and so aren't supported on
native. Instead you can use Adapt and render them as a Sheet, or just conditionally
render them. We landed support for them at one point, but we need the community to
contribute tests in order to support them for mobile again.
</Notice>
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
### PortalProvider
When rendering into root of app instead of inline, you'll first need to install
the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Popover, Adapt } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
{/* Optional: Control focus behavior */}
<Popover.FocusScope loop trapped focusOnIdle={true}>
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
</Popover.FocusScope>
{/* optionally change to sheet when small screen */}
{/* you can also use <Popover.Adapt /> */}
<Adapt when="maxMd">
<Popover.Sheet>
<Popover.Sheet.Overlay />
<Popover.Sheet.Frame>
<Popover.Sheet.ScrollView>
<Adapt.Contents />
</Popover.Sheet.ScrollView>
</Popover.Sheet.Frame>
</Popover.Sheet>
</Adapt>
</Popover>
)
```
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'keepChildrenMounted',
type: 'boolean | "lazy"',
required: false,
description: `By default, Popover removes children from DOM/rendering when fully hidden. Setting true will keep children mounted even when hidden. This can be beneficial for performance if your popover content is expensive to render. The "lazy" value will only initially mount the children after a React startTransition, and then keep them mounted thereafter.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
description: `Keeps the Popover inside the frame, see floating-ui shift().`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'hoverable',
type: 'boolean | UseFloatingProps',
required: false,
description: `Allows hovering on the trigger to open the popover. See UseFloatingProps from floating-ui: accepts boolean or object of { delay: number, restMs: number, handleClose: Function, mouseOnly: boolean, move: boolean }`,
},
{
name: 'resize',
type: 'SizeProps | boolean',
required: false,
description: `Will set maxWidth and maxHeight of Content to fit inside outer window when it won't fit, see floating-ui size().`,
},
]}
/>
For most of these properties, you'll want to reference the
[floating-ui docs](https://floating-ui.com/docs/getting-started).
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/popover/1.83.0#portalprovider) for more information.
</Notice>
### Popover.Arrow
Popover.Arrow can be used to show an arrow that points at the Trigger element.
In order for the Arrow to show you must have a Trigger element within your
Popover. Arrows extend YStack, see [Stacks](/docs/components/stacks).
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack,
see [Stacks](/docs/components/stacks).
### Popover.Content
Extends PopperContent which extends SizableStack which extends a YStack (see
[Stacks](/docs/components/stacks)).
Also extends
Used to display the content of the popover.
<PropsTable
data={[
{
name: 'enableAnimationForPositionChange',
type: 'boolean',
description:
'Disabled animate presence animations in favor of regular animation, useful for doing sliding popovers.',
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: 'Controls default padding/borderRadius when unstyled is false.',
},
{
name: 'unstyled',
required: false,
type: `boolean`,
default: false,
description: `Removes all default Tamagui styles.`,
},
{
name: 'trapFocus',
type: 'boolean',
default: false,
description: 'Whether focus should be trapped within the `Popover`',
},
{
name: 'disableFocusScope',
type: 'boolean',
default: false,
description: 'Whether popover should not focus contents on open',
},
{
name: 'onOpenAutoFocus',
type: `FocusScopeProps['onMountAutoFocus']`,
default: false,
description: 'Event handler called when auto-focusing on open. Can be prevented.',
},
{
name: 'onCloseAutoFocus',
type: `FocusScopeProps['onUnmountAutoFocus'] | false`,
default: false,
description: 'Event handler called when auto-focusing on close. Can be prevented.',
},
{
name: 'lazyMount',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover
attaches, use Anchor. When used, Anchor is where the Popover will attach, while
Trigger will open it.
### Popover.Sheet
When used with `Adapt`, Popover will render as a sheet when that breakpoint is
active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Popover.Sheet.Frame` to insert the
contents given to `Popover.Content`
### Popover.FocusScope
Provides access to the underlying FocusScope component used by Popover for focus
management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If
used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove
this ScrollView when swapping into the Sheet, as the Sheet must use it's own
ScrollView that handles special logic for interactions with dragging.
## components/popover/1.131.0
---
title: Popover
description: Show content with a trigger in a floating pane.
name: popover
component: Popover
package: popover
demoName: Popover
---
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is
pressed, floating above the current content. Be sure to open the code example
above for a copy-paste implementation.
<Notice>
Note: Popovers are not a recommended pattern for mobile apps. Instead you can use Adapt
and render them as a Sheet, or just conditionally render them to some native UI.
</Notice>
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
### PortalProvider
_Only_ if you are not using `tamagui` and installing popover independently -
you'll need to set up the portal for it with the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Popover, Adapt } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
{/* Optional: Control focus behavior */}
<Popover.FocusScope loop trapped focusOnIdle={true}>
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
</Popover.FocusScope>
{/* optionally change to sheet when small screen */}
{/* you can also use <Popover.Adapt /> */}
<Adapt when="maxMd">
<Popover.Sheet>
<Popover.Sheet.Overlay />
<Popover.Sheet.Frame>
<Popover.Sheet.ScrollView>
<Adapt.Contents />
</Popover.Sheet.ScrollView>
</Popover.Sheet.Frame>
</Popover.Sheet>
</Adapt>
</Popover>
)
```
## Scoping
Popover supports scoping which lets you mount one or more Popover instances at
the root of your app, while having a deeply nested child Trigger or Content
attach to the proper parent Popover instance.
In performance sensitive areas you may want to take advantage of this as it allows
you to only render the Popover.Trigger inside the sensitive area.
Popover isn't the cheapest component - it has a lot of functionality
inside of it like scroll management, focus management, and tracking position.
Here's the basic anatomy of using `scope` and placing your Popover higher up
for performance:
```tsx fileName=_layout.tsx
import { Popover } from 'tamagui'
// in your root layout:
export default ({ children }) => (
<Popover scope="user-avatar">
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
</Popover.Content>
{/* the rest of your app, note that it's inside of Popover */}
{children}
</Popover>
)
```
```tsx fileName=UserAvatar.tsx
export default () => (
<Popover.Trigger scope="user-avatar">
<Avatar />
</Popover.Trigger>
)
```
Note that the `Trigger` scope ties to the `Popover` scope.
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down to all sub-components when set for padding, arrow, borderRadius.`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'keepChildrenMounted',
type: 'boolean | "lazy"',
required: false,
description: `By default, Popover removes children from DOM/rendering when fully hidden. Setting true will keep children mounted even when hidden. This can be beneficial for performance if your popover content is expensive to render. The "lazy" value will only initially mount the children after a React startTransition, and then keep them mounted thereafter.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
description: `Keeps the Popover inside the frame, see floating-ui shift().`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'hoverable',
type: 'boolean | UseFloatingProps',
required: false,
description: `Allows hovering on the trigger to open the popover. See UseFloatingProps from floating-ui: accepts boolean or object of { delay: number, restMs: number, handleClose: Function, mouseOnly: boolean, move: boolean }`,
},
{
name: 'resize',
type: 'SizeProps | boolean',
required: false,
description: `Will set maxWidth and maxHeight of Content to fit inside outer window when it won't fit, see floating-ui size().`,
},
]}
/>
For most of these properties, you'll want to reference the
[floating-ui docs](https://floating-ui.com/docs/getting-started).
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](#portalprovider) for more information.
</Notice>
### Popover.Arrow
Popover.Arrow can be used to show an arrow that points at the Trigger element.
In order for the Arrow to show you must have a Trigger element within your
Popover. Arrows extend YStack, see [Stacks](/docs/components/stacks).
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack,
see [Stacks](/docs/components/stacks).
### Popover.Content
Extends PopperContent which extends SizableStack which extends a YStack (see
[Stacks](/docs/components/stacks)).
Also extends
Used to display the content of the popover.
<PropsTable
data={[
{
name: 'enableAnimationForPositionChange',
type: 'boolean',
description:
'Disabled animate presence animations in favor of regular animation, useful for doing sliding popovers.',
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: 'Controls default padding/borderRadius when unstyled is false.',
},
{
name: 'unstyled',
required: false,
type: `boolean`,
default: false,
description: `Removes all default Tamagui styles.`,
},
{
name: 'trapFocus',
type: 'boolean',
default: false,
description: 'Whether focus should be trapped within the `Popover`',
},
{
name: 'disableFocusScope',
type: 'boolean',
default: false,
description: 'Whether popover should not focus contents on open',
},
{
name: 'onOpenAutoFocus',
type: `FocusScopeProps['onMountAutoFocus']`,
default: false,
description: 'Event handler called when auto-focusing on open. Can be prevented.',
},
{
name: 'onCloseAutoFocus',
type: `FocusScopeProps['onUnmountAutoFocus'] | false`,
default: false,
description: 'Event handler called when auto-focusing on close. Can be prevented.',
},
{
name: 'lazyMount',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover
attaches, use Anchor. When used, Anchor is where the Popover will attach, while
Trigger will open it.
### Popover.Sheet
When used with `Adapt`, Popover will render as a sheet when that breakpoint is
active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Popover.Sheet.Frame` to insert the
contents given to `Popover.Content`
### Popover.FocusScope
Provides access to the underlying FocusScope component used by Popover for focus
management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If
used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove
this ScrollView when swapping into the Sheet, as the Sheet must use its own
ScrollView that handles special logic for interactions with dragging.
## components/popover/1.43.0
---
title: Popover
description: A simple popover component
name: popover
component: Popover
package: popover
demoName: Popover
---
# Popover
<Description>Show content with a trigger in a floating pane</Description>
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is pressed, floating above the current content.
<Notice>
Support for native was contributed by a community member, but not guaranteed to work
until we get time to fully test. We don't recommend the Popover pattern for phone-sized
devices, and you can adapt it to a Sheet instead.
</Notice>
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
## Anatomy
```tsx
import { Popover } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
{/* optionally change to sheet when small screen */}
<Popover.Adapt when="maxMd">
<Popover.Sheet>
<Popover.Sheet.Overlay />
<Popover.Sheet.Frame>
<Popover.Sheet.ScrollView>
<Popover.Adapt.Contents />
</Popover.Sheet.ScrollView>
</Popover.Sheet.Frame>
</Popover.Sheet>
</Popover.Adapt>
</Popover>
)
```
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description: `Renders into root of app instead of inline.`,
},
{
name: 'keepChildrenMounted',
type: 'boolean',
required: false,
description: `By default, Popover removes children from DOM/rendering when fully hidden. Setting true will keep children mounted even when hidden. This can be beneficial for performance if your popover content is expensive to render.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
description: `Keeps the Popover inside the frame, see floating-ui shift().`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
]}
/>
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack, see [Stacks](/docs/components/stacks).
### Popover.Content
Renders as SizableStack which is just a YStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
Used to display the content of the popover.
<PropsTable
data={[
{
name: 'size',
type: 'SizeTokens',
required: false,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover attaches, use Anchor. When used, Anchor is where the Popover will attach, while Trigger will open it.
### Popover.Sheet
When used with `Adapt`, Popover will render as a sheet when that breakpoint is active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Popover.Sheet.Frame` to insert the contents given to `Popover.Content`
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove this ScrollView when swapping into the Sheet, as the Sheet must use it's own ScrollView that handles special logic for interactions with dragging.
## components/popover/1.83.0
---
title: Popover
description: A simple popover component
name: popover
component: Popover
package: popover
demoName: Popover
---
# Popover
<Description>Show content with a trigger in a floating pane</Description>
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is pressed, floating above the current content.
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
### PortalProvider
When rendering into root of app instead of inline, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Popover, Adapt } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
{/* optionally change to sheet when small screen */}
{/* you can also use <Popover.Adapt /> */}
<Adapt when="maxMd">
<Popover.Sheet>
<Popover.Sheet.Overlay />
<Popover.Sheet.Frame>
<Popover.Sheet.ScrollView>
<Adapt.Contents />
</Popover.Sheet.ScrollView>
</Popover.Sheet.Frame>
</Popover.Sheet>
</Adapt>
</Popover>
)
```
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description: `Renders into root of app instead of inline.`,
},
{
name: 'keepChildrenMounted',
type: 'boolean',
required: false,
description: `By default, Popover removes children from DOM/rendering when fully hidden. Setting true will keep children mounted even when hidden. This can be beneficial for performance if your popover content is expensive to render.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
description: `Keeps the Popover inside the frame, see floating-ui shift().`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'hoverable',
type: 'boolean | UseFloatingProps',
required: false,
description: `Allows hovering on the trigger to open the popover. See UseFloatingProps from floating-ui: accepts boolean or object of { delay: number, restMs: number, handleClose: Function, mouseOnly: boolean, move: boolean }`,
},
]}
/>
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/popover/1.83.0#portalprovider) for more information.
</Notice>
### Popover.Arrow
Popover.Arrow can be used to show an arrow that points at the Trigger element. In order for the Arrow to show you must have a Trigger element within your Popover. Arrows extend YStack, see [Stacks](/docs/components/stacks).
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack, see [Stacks](/docs/components/stacks).
### Popover.Content
Renders as SizableStack which is just a YStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
Used to display the content of the popover.
<PropsTable
data={[
{
name: 'size',
type: 'SizeTokens',
required: false,
},
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover attaches, use Anchor. When used, Anchor is where the Popover will attach, while Trigger will open it.
### Popover.Sheet
When used with `Adapt`, Popover will render as a sheet when that breakpoint is active.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Popover.Sheet.Frame` to insert the contents given to `Popover.Content`
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove this ScrollView when swapping into the Sheet, as the Sheet must use it's own ScrollView that handles special logic for interactions with dragging.
## components/popover/2.0.0
---
title: Popover
description: Show content with a trigger in a floating pane
name: popover
component: Popover
package: popover
demoName: Popover
---
<HeroContainer showAnimationDriverControl>
<PopoverDemo />
</HeroContainer>
```tsx hero template=Popover
```
<Highlights
features={[
`Optional arrow to point to content.`,
`Keeps within bounds of page.`,
`Can be placed into 12 anchor positions.`,
]}
/>
Popovers are a great way to show content that's only visible when trigger is
pressed, floating above the current content. They [automatically stack](/ui/z-index) above other content. Be sure to open the code example above for a copy-paste implementation.
<Notice>
Note: Popovers are not a recommended pattern for mobile apps. Instead you can use Adapt
and render them as a Sheet, or just conditionally render them to some native UI.
</Notice>
## Installation
Popover is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/popover
```
<Notice theme="blue">
For native apps, we recommend [setting up native
portals](/docs/components/portal#native-portal-setup-recommended) to preserve React
context inside Popover content.
</Notice>
## Anatomy
```tsx
import { Popover, Adapt, Sheet } from 'tamagui' // or '@tamagui/popover'
export default () => (
<Popover>
<Popover.Trigger />
{/* Optional: Control focus behavior */}
<Popover.FocusScope loop trapped focusOnIdle={true}>
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
{/* ScrollView is optional, can just put any contents inside if not scrollable */}
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
{/* ... */}
</Popover.Content>
</Popover.FocusScope>
{/* optionally change to sheet when small screen */}
{/* you can also use <Popover.Adapt /> */}
<Adapt when="max-md">
<Sheet>
<Sheet.Overlay />
<Sheet.Frame>
<Sheet.ScrollView>
<Adapt.Contents />
</Sheet.ScrollView>
</Sheet.Frame>
</Sheet>
</Adapt>
</Popover>
)
```
## Scoping
Popover supports scoping which lets you mount one or more Popover instances at
the root of your app, while having a deeply nested child Trigger or Content
attach to the proper parent Popover instance.
In performance sensitive areas you may want to take advantage of this as it allows
you to only render the Popover.Trigger inside the sensitive area.
Popover isn't the cheapest component - it has a lot of functionality
inside of it like scroll management, focus management, and tracking position.
Here's the basic anatomy of using `scope` and placing your Popover higher up
for performance:
```tsx fileName=_layout.tsx
import { Popover } from 'tamagui'
// in your root layout:
export default ({ children }) => (
<Popover scope="user-avatar">
<Popover.Content>
<Popover.Arrow />
<Popover.Close />
<Popover.ScrollView>{/* ... */}</Popover.ScrollView>
</Popover.Content>
{/* the rest of your app, note that it's inside of Popover */}
{children}
</Popover>
)
```
```tsx fileName=UserAvatar.tsx
export default () => (
<Popover.Trigger scope="user-avatar">
<Avatar />
</Popover.Trigger>
)
```
Note that the `Trigger` scope ties to the `Popover` scope.
## API Reference
### Popover
Contains every component for the popover.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down to all sub-components when set for padding, arrow, borderRadius.`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `Controlled open state of the popover.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `Initial open state when uncontrolled.`,
},
{
name: 'onOpenChange',
type: `(open: boolean, via?: 'hover' | 'press') => void`,
required: false,
description: `Called when the popover opens or closes. The optional second argument indicates how it was triggered - 'hover' for hover events (when using the hoverable prop) or 'press' for click/press events.`,
},
{
name: 'keepChildrenMounted',
type: 'boolean | "lazy"',
required: false,
description: `By default, Popover removes children from DOM/rendering when fully hidden. Setting true will keep children mounted even when hidden. This can be beneficial for performance if your popover content is expensive to render. The "lazy" value will only initially mount the children after a React startTransition, and then keep them mounted thereafter.`,
},
{
name: 'disableDismissable',
type: 'boolean',
required: false,
description: `Disables the dismissable layer (escape key, outside click handling). Useful when using keepChildrenMounted for popovers that stay mounted but are visually hidden - set this to true when the popover is hidden to prevent it from capturing escape key presses.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
default: '{ padding: 10 }',
description: `Shifts the popover horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).`,
},
{
name: 'allowFlip',
type: 'FlipProps | boolean',
required: false,
description: `Moves the Popover to other sides when space allows it, see floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'hoverable',
type: 'boolean | UseFloatingProps',
required: false,
description: `Allows hovering on the trigger to open the popover. See UseFloatingProps from floating-ui: accepts boolean or object of { delay: number, restMs: number, handleClose: Function, mouseOnly: boolean, move: boolean }`,
},
{
name: 'resize',
type: 'SizeProps | boolean',
required: false,
description: `Will set maxWidth and maxHeight of Content to fit inside outer window when it won't fit, see floating-ui size().`,
},
{
name: 'zIndex',
type: 'number',
required: false,
description: `Override the automatic z-index stacking. By default, Tamagui automatically stacks overlays so later-opened content appears above earlier content. Only set this if you need to override the automatic behavior.`,
},
]}
/>
For most of these properties, you'll want to reference the
[floating-ui docs](https://floating-ui.com/docs/getting-started).
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](#portalprovider) for more information.
</Notice>
### Popover.Arrow
Popover.Arrow can be used to show an arrow that points at the Trigger element.
In order for the Arrow to show you must have a Trigger element within your
Popover. Arrows extend YStack, see [Stacks](/docs/components/stacks).
<PropsTable
data={[
{
name: 'animatePosition',
type: 'boolean',
description: 'Enable smooth animation when the arrow position changes.',
},
{
name: 'size',
type: 'SizeTokens',
description: 'Size of the arrow.',
},
{
name: 'offset',
type: 'number',
description: 'Offset from the content edge.',
},
]}
/>
### Popover.Trigger
Used to trigger opening of the popover when uncontrolled, just renders a YStack,
see [Stacks](/docs/components/stacks).
### Popover.Content
Extends PopperContent which extends SizableStack which extends a YStack (see
[Stacks](/docs/components/stacks)). Used to display the content of the popover.
<PropsTable
data={[
{
name: 'animatePosition',
type: "boolean | 'even-when-repositioning'",
description:
'Enable smooth animation when the content position changes (e.g., when flipping sides).',
},
{
name: 'transformOrigin',
type: 'boolean',
default: 'true',
description:
'Automatically sets CSS transform-origin based on placement and arrow position. Updates when the popover flips. Enables natural scale animations that grow from the arrow point.',
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: 'Controls default padding/borderRadius when unstyled is false.',
},
{
name: 'unstyled',
required: false,
type: `boolean`,
default: false,
description: `Removes all default Tamagui styles.`,
},
{
name: 'trapFocus',
type: 'boolean',
default: false,
description: 'Whether focus should be trapped within the `Popover`',
},
{
name: 'disableFocusScope',
type: 'boolean',
default: false,
description: 'Whether popover should not focus contents on open',
},
{
name: 'onOpenAutoFocus',
type: `FocusScopeProps['onMountAutoFocus']`,
default: false,
description: 'Event handler called when auto-focusing on open. Can be prevented.',
},
{
name: 'onCloseAutoFocus',
type: `FocusScopeProps['onUnmountAutoFocus'] | false`,
default: false,
description: 'Event handler called when auto-focusing on close. Can be prevented.',
},
{
name: 'lazyMount',
required: false,
type: `boolean`,
description: `Delays mounting content until first open.`,
},
]}
/>
### Popover.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Popover
attaches, use Anchor. When used, Anchor is where the Popover will attach, while
Trigger will open it.
### Sheet (with Adapt)
When used with `Adapt`, you can render a Sheet when that breakpoint is
active. Import `Sheet` directly from `tamagui` or `@tamagui/sheet`.
See [Sheet](/docs/components/sheet) for more props.
Must use `Adapt.Contents` inside the `Sheet.Frame` to insert the
contents given to `Popover.Content`
### Popover.FocusScope
Provides access to the underlying FocusScope component used by Popover for focus
management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Popover.ScrollView
Must be nested inside Content. Renders as a plain React Native ScrollView. If
used alongside `<Adapt />` and Sheet, Tamagui will automatically know to remove
this ScrollView when swapping into the Sheet, as the Sheet must use its own
ScrollView that handles special logic for interactions with dragging.
## Utility Functions
These functions allow you to programmatically manage open popovers.
```tsx
import {
closeOpenPopovers,
closeLastOpenedPopover,
hasOpenPopovers,
} from '@tamagui/popover'
```
### closeOpenPopovers
Closes all currently open popovers. Returns `true` if any popovers were closed, `false` if none were open.
```tsx
const didClose = closeOpenPopovers()
```
### closeLastOpenedPopover
Closes only the most recently opened popover. Returns `true` if a popover was closed, `false` if none were open.
```tsx
const didClose = closeLastOpenedPopover()
```
### hasOpenPopovers
Returns `true` if there are any open popovers, `false` otherwise.
```tsx
if (hasOpenPopovers()) {
// handle open popovers
}
```
## components/portal/1.0.0
---
title: Portal
description: Send items to other areas of the tree.
name: portal
component: Portal
package: portal
---
Portal is included in `tamagui` as it's used by a few components. For now, it's simply using [@gorhom/portal](https://github.com/gorhom/react-native-portal).
## components/portal/2.0.0
---
title: Portal
description: Send items to other areas of the tree
name: portal
component: Portal
package: portal
---
Portal is included in `tamagui` and is used by [Sheet](/docs/components/sheet),
[Dialog](/docs/components/dialog), [Popover](/docs/components/popover),
[Select](/docs/components/select), and [Toast](/docs/components/toast). See [Z-Index & Stacking](/ui/z-index) for how overlay layering works.
## Native Portal Setup (Recommended)
On web, React's built-in `createPortal` preserves context automatically. On
native, the default portal implementation doesn't preserve React context.
Tamagui automatically re-propagates its own contexts (theme, configuration), but
your custom contexts like navigation or app state won't be available inside
portaled content. The re-propagation also adds some overhead.
We recommend using
[react-native-teleport](https://github.com/kirillzyusko/react-native-teleport)
to solve this. It uses React Native's native portal API to preserve context
automatically.
### Step 1: Install react-native-teleport
```bash
npm install react-native-teleport
```
### Step 2: Import the setup module
In your app's entry file (index.js or App.tsx), before any Tamagui imports:
```tsx
import '@tamagui/native/setup-teleport'
```
That's it! All portal-using components will now preserve context automatically
on native. Without native portals, your custom context from parent components
won't be available inside portaled content:
```tsx
const MyContext = createContext('default')
function App() {
return (
<MyContext.Provider value="from-provider">
<Sheet modal open>
<Sheet.Frame>
{/* Without native portal: you'd not have context here on native */}
<MyConsumer />
</Sheet.Frame>
</Sheet>
</MyContext.Provider>
)
}
```
## Alternative Approaches
If you can't use react-native-teleport, there are other ways to handle context
in portals:
### Component Scoping
For Dialog, Popover, and Tooltip, use the `scope` prop to mount a single
instance at your app root. This avoids portals entirely on native:
```tsx
// _layout.tsx - mount once at root with all your providers
<TamaguiProvider>
<Tooltip scope="global">
<Tooltip.Content>
<Tooltip.Arrow />
<Paragraph>{/* label set by trigger */}</Paragraph>
</Tooltip.Content>
{/* rest of your app */}
<Slot />
</Tooltip>
</TamaguiProvider>
// anywhere in your app - just the trigger
<Tooltip.Trigger scope="global" aria-label="Settings">
<Button icon={Settings} />
</Tooltip.Trigger>
```
This pattern is also a performance win for lists or tables with many interactive
elements.
### Manual Context Re-propagation
Wrap portal children with the necessary providers:
```tsx
const theme = useTheme()
const myValue = useContext(MyContext)
<Sheet>
<Sheet.Frame>
<ThemeProvider theme={theme}>
<MyContext.Provider value={myValue}>
{/* content that needs context */}
</MyContext.Provider>
</ThemeProvider>
</Sheet.Frame>
</Sheet>
```
This is more verbose and error-prone as you need to remember to re-propagate
every context you use.
## API Reference
### Portal
<PropsTable
data={[
{
name: 'zIndex',
type: 'number',
description: `Fixed z-index value for the portal.`,
},
{
name: 'stackZIndex',
type: `boolean | number | 'global'`,
description: (
<span>
Enable automatic z-index stacking. Tamagui intelligently stacks z-index both
horizontally (based on mount order) and vertically (nested content is always
above parent but below sibling content that comes after). <code>true</code>{' '}
enables stacking, a number adds to the stacked value, <code>'global'</code> only
stacks horizontally without nesting logic.
</span>
),
},
{
name: 'passThrough',
type: 'boolean',
description: `When true, renders children directly without portal behavior.`,
},
]}
/>
This automatic stacking is already enabled by default in Dialog, Popover, Sheet, and other overlay
components. If you open a Popover from within a Dialog, the Popover will automatically have a
higher z-index than the Dialog without any configuration needed.
## Technical Details
[react-native-teleport](https://github.com/nicksrandall/react-native-teleport)
uses `ReactNativeFabricUIManager.createPortal` (Fabric) or
`UIManager.createPortal` (Paper) to create true native portals that preserve the
React fiber tree.
The default portal implementation, by contrast, uses a JS-based approach with
context providers and a reducer to manage portal state. While compatible with
older RN versions, it breaks React context because it re-renders content in a
separate provider tree.
Tamagui includes a `needsPortalRepropagation()` helper that returns `true` when
using the default portal implementation and `false` when using native portals,
so library authors can conditionally re-propagate context only when needed.
## components/progress/1.0.0
---
title: Progress
description: Display a bar to indicate percent completion.
name: progress
component: Progress
package: progress
demoName: Progress
---
# Progress
<Description>Show percent completion with a progress bar</Description>
<HeroContainer showAnimationDriverControl>
<ProgressDemo />
</HeroContainer>
```tsx hero template=Progress
```
<Highlights
features={[
'Sizable, themeable, animatable.',
'Compound component API for complete control.',
<span>
Adheres to the{' '}
<a href="https://www.w3.org/TR/wai-aria-1.2/#progressbar">
progressbar role requirements
</a>
</span>,
]}
/>
## Installation
Progress is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/progress
```
## Usage
The `value` property controls the percent, but you can override it by adjusting the `x` property.
```tsx
import { Button, Progress } from 'tamagui'
export default () => (
<Progress value={60}>
<Progress.Indicator animation="bouncy" />
</Progress>
)
```
## API Reference
### Progress
Progress extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: '"small" | "large"',
},
{
name: 'value',
required: false,
type: 'number | null',
description: `Control the percent progress.`,
},
{
name: 'max',
required: false,
type: 'number',
},
]}
/>
### Progress.Indicator
`Progress.Indicator` extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props).
## components/progress/1.48.0
---
title: Progress
description: Show percent completion with a progress bar.
name: progress
component: Progress
package: progress
demoName: Progress
---
<HeroContainer showAnimationDriverControl>
<ProgressDemo />
</HeroContainer>
```tsx hero template=Progress
```
<Highlights
features={[
'Sizable, themeable, animatable.',
'Compound component API for complete control.',
<span>
Adheres to the{' '}
<Link href="https://www.w3.org/TR/wai-aria-1.2/#progressbar">
progressbar role requirements
</Link>
</span>,
]}
/>
## Installation
Progress is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/progress
```
## Usage
The `value` property controls the percent, but you can override it by adjusting the `x` property.
```tsx
import { Button, Progress } from 'tamagui'
export default () => (
<Progress value={60}>
<Progress.Indicator animation="bouncy" />
</Progress>
)
```
## API Reference
### Progress
Progress extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: '"small" | "large"',
},
{
name: 'value',
required: false,
type: 'number | null',
description: `Control the percent progress.`,
},
{
name: 'max',
required: false,
type: 'number',
},
{
name: 'unstyled',
required: false,
type: 'boolean',
description: `When true will remove all default styles`,
},
]}
/>
### Progress.Indicator
`Progress.Indicator` extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props).
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: 'boolean',
description: `When true will remove all default styles`,
},
]}
/>
## components/progress/2.0.0
---
title: Progress
description: Show percent completion with a progress bar
name: progress
component: Progress
package: progress
demoName: Progress
---
<HeroContainer showAnimationDriverControl>
<ProgressDemo />
</HeroContainer>
```tsx hero template=Progress
```
<Highlights
features={[
'Sizable, themeable, animatable.',
'Compound component API for complete control.',
<span>
Adheres to the{' '}
<Link href="https://www.w3.org/TR/wai-aria-1.2/#progressbar">
progressbar role requirements
</Link>
</span>,
]}
/>
## Installation
Progress is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/progress
```
## Usage
The `value` property controls the percent, but you can override it by adjusting the `x` property.
```tsx
import { Button, Progress } from 'tamagui'
export default () => (
<Progress value={60}>
<Progress.Indicator transition="bouncy" />
</Progress>
)
```
## API Reference
### Progress
Progress extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: '"small" | "large"',
description: `Size preset for the progress bar.`,
},
{
name: 'value',
required: false,
type: 'number | null',
description: `Controls the percent progress.`,
},
{
name: 'max',
required: false,
type: 'number',
description: `Maximum value for calculating the percentage.`,
},
{
name: 'unstyled',
required: false,
type: 'boolean',
description: `When true, removes all default styles.`,
},
]}
/>
### Progress.Indicator
`Progress.Indicator` extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props).
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: 'boolean',
description: `When true, removes all default styles.`,
},
]}
/>
## components/radio-group/1.2.0
---
title: RadioGroup
description: A group of radio buttons
name: radio-group
component: RadioGroup
package: radio-group
demoName: RadioGroup
---
# RadioGroup
<Description>Choose one option from a list in a form.</Description>
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Tamagui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer showAnimationDriverControl>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<RadioGroupDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<RadioGroupUnstyledDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<RadioGroupHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=RadioGroup
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=RadioGroupUnstyled
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=RadioGroupHeadless
````
</Tabs.Content>
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Sizable & works controlled or uncontrolled.`,
`Ability to opt-out to native radio button on web.`,
]}
/>
## Installation
<Tabs.Content value="styled">
RadioGroup is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/radio-group
````
</Tabs.Content>
<Tabs.Content value="unstyled">
RadioGroup is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/radio-group
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless radio group, you want to import it from the
`@tamagui/radio-headless` package. This package has no dependency on
`@tamagui/core`, but still works off the react-native APIs.
This means can bring your own style library.
```bash
npm install @tamagui/radio-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { RadioGroup } from 'tamagui'
export default () => (
<RadioGroup value="foo" gap="$2">
<RadioGroup.Item value="foo" id="foo-radio-item">
<RadioGroup.Indicator />
</RadioGroup.Item>
<RadioGroup.Item value="bar" id="bar-radio-item">
<RadioGroup.Indicator />
</RadioGroup.Item>
</RadioGroup>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Use the `createRadioGroup` export to create a fully custom RadioGroup that still
uses the Tamagui styling system. This is similar to setting `unstyled`, but goes
a bit further. It doesn't add any types for `size` or `unstyled`, and it won't
automatically apply the `active` theme. If does pass the `checked` prop down as
indicated in the types of `createRadioGroup`.
```tsx template=RadioGroupUnstyled
```
</Tabs.Content>
<Tabs.Content value="headless">
Using the `useRadioGroup` and `useRadioGroupItem` API, you can make your own RadioGroup from scratch.
```tsx template=RadioGroupHeadless
```
</Tabs.Content>
<Tabs.Content value="styled">
## API Reference
### RadioGroup
RadioGroup extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Set aria-disabled on web, and disable touch on native for all children items`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native radio button on web.`,
default: false,
},
{
name: 'onValueChange',
type: '(value: string) => void',
},
{
name: 'accentColor',
type: 'string',
description: 'Sets `accent-color` style when `native` prop is enabled',
},
]}
/>
### RadioGroup.Item
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by on web`,
},
{
name: 'value',
type: 'string',
description: `Input value for the radio button.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Set aria-disabled on web, and disable touch on native`,
},
{
name: 'id',
type: 'string',
description: `Id used on the web`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `The Tamagui size tokens should map to the height of a button at any given step. This means you want somewhat smaller checkboxes typically.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### RadioGroup.Indicator
RadioGroup.Indicator appears only when the parent Item is checked. it extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props) adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
</Tabs.Content>
<Tabs.Content value="unstyled">
## API Reference
### RadioGroup
RadioGroup extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Set aria-disabled on web, and disable touch on native for all children items`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native radio button on web.`,
default: false,
},
{
name: 'onValueChange',
type: '(value: string) => void',
},
{
name: 'accentColor',
type: 'string',
description: 'Sets `accent-color` style when `native` prop is enabled',
},
]}
/>
### RadioGroup.Item
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by on web`,
},
{
name: 'value',
type: 'string',
description: `Input value for the radio button.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Set aria-disabled on web, and disable touch on native`,
},
{
name: 'id',
type: 'string',
description: `Id used on the web`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `The Tamagui size tokens should map to the height of a button at any given step. This means you want somewhat smaller checkboxes typically.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### RadioGroup.Indicator
RadioGroup.Indicator appears only when the parent Item is checked. it extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props) adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
</Tabs.Content>
</Tabs>
## components/radio-group/1.3.0
---
title: RadioGroup
description: Use in a form to allow selecting one option from multiple.
name: radio-group
component: RadioGroup
package: radio-group
demoName: RadioGroup
---
<HeroContainer>
<RadioGroupDemo />
</HeroContainer>
```tsx hero template=RadioGroup
```
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Sizable & works controlled or uncontrolled.`,
`Ability to opt-out to native radio button on web.`,
]}
/>
## Installation
RadioGroup is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/radio-group
```
## Usage
```tsx
import { RadioGroup } from 'tamagui'
export default () => (
<RadioGroup value="foo" gap="$2">
<RadioGroup.Item value="foo" id="foo-radio-item">
<RadioGroup.Indicator />
</RadioGroup.Item>
<RadioGroup.Item value="bar" id="bar-radio-item">
<RadioGroup.Indicator />
</RadioGroup.Item>
</RadioGroup>
)
```
## API Reference
### RadioGroup
RadioGroup extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Set aria-disabled on web, and disable touch on native for all children items`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native radio button on web.`,
default: false,
},
{
name: 'onValueChange',
type: '(value: string) => void',
},
{
name: 'accentColor',
type: 'string',
description: 'Sets `accent-color` style when `native` prop is enabled',
},
]}
/>
### RadioGroup.Item
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by on web`,
},
{
name: 'value',
type: 'string',
description: `Input value for the radio button.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Set aria-disabled on web, and disable touch on native`,
},
{
name: 'id',
type: 'string',
description: `Id used on the web`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `The Tamagui size tokens should map to the height of a button at any given step. This means you want somewhat smaller checkboxes typically.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### RadioGroup.Indicator
RadioGroup.Indicator appears only when the parent Item is checked. it extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props) adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
## components/radio-group/2.0.0
---
title: RadioGroup
description: Use in a form to allow selecting one option from multiple
name: radio-group
component: RadioGroup
package: radio-group
demoName: RadioGroup
---
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Tamagui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<RadioGroupDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<RadioGroupDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<RadioGroupHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=RadioGroup
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=RadioGroup
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=RadioGroupHeadless
````
</Tabs.Content>
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Sizable & works controlled or uncontrolled.`,
`Ability to opt-out to native radio button on web.`,
]}
/>
## Installation
<Tabs.Content value="styled">
RadioGroup is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/radio-group
````
</Tabs.Content>
<Tabs.Content value="unstyled">
RadioGroup is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/radio-group
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless radio group, import from the `@tamagui/radio-headless` package. This package has no dependency on `@tamagui/core` and provides hooks for building custom radio groups with any styling solution.
```bash
npm install @tamagui/radio-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { RadioGroup } from 'tamagui'
export default () => (
<RadioGroup value="foo" gap="$2">
<RadioGroup.Item value="foo" id="foo-radio-item">
<RadioGroup.Indicator />
</RadioGroup.Item>
<RadioGroup.Item value="bar" id="bar-radio-item">
<RadioGroup.Indicator />
</RadioGroup.Item>
</RadioGroup>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Use the `createRadioGroup` export to create a fully custom radio group that still uses the Tamagui styling system. You provide your own styled components and get back a fully functional radio group component.
```tsx
import { createRadioGroup, RadioGroupContext } from '@tamagui/radio-group'
import { styled, View } from 'tamagui'
const CustomFrame = styled(View, {
// your styles
})
const CustomItem = styled(View, {
context: RadioGroupContext,
// your styles
})
const CustomIndicator = styled(View, {
context: RadioGroupContext,
// your styles
})
export const CustomRadioGroup = createRadioGroup({
Frame: CustomFrame,
Item: CustomItem,
Indicator: CustomIndicator,
})
```
</Tabs.Content>
<Tabs.Content value="headless">
The `@tamagui/radio-headless` package provides three hooks for building custom radio groups:
- `useRadioGroup` - for the container/group
- `useRadioGroupItem` - for individual radio items
- `useRadioGroupItemIndicator` - for the indicator (checked state visual)
### Basic Usage
```tsx
import {
useRadioGroup,
useRadioGroupItem,
useRadioGroupItemIndicator,
RadioGroupContextValue,
RadioGroupItemContextValue,
} from '@tamagui/radio-headless'
import { createContext, useContext } from 'react'
// Create contexts for the group and item
const RadioGroupContext = createContext<RadioGroupContextValue>({})
const RadioGroupItemContext = createContext<RadioGroupItemContextValue>({
checked: false,
})
function RadioGroup({ children, ...props }) {
const { providerValue, frameAttrs } = useRadioGroup({
orientation: 'vertical',
...props,
})
return (
<RadioGroupContext.Provider value={providerValue}>
<div {...frameAttrs} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{children}
</div>
</RadioGroupContext.Provider>
)
}
function RadioItem({ value, children }) {
const { checked, providerValue, frameAttrs, bubbleInput } = useRadioGroupItem({
radioGroupContext: RadioGroupContext,
value,
})
return (
<RadioGroupItemContext.Provider value={providerValue}>
<button
{...frameAttrs}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: 8,
border: '1px solid #ccc',
borderRadius: 4,
background: checked ? '#e0f2fe' : 'white',
}}
>
<RadioIndicator />
{children}
</button>
{bubbleInput}
</RadioGroupItemContext.Provider>
)
}
function RadioIndicator() {
const { checked, ...indicatorProps } = useRadioGroupItemIndicator({
radioGroupItemContext: RadioGroupItemContext,
})
return (
<div
{...indicatorProps}
style={{
width: 16,
height: 16,
borderRadius: '50%',
border: '2px solid #3b82f6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{checked && (
<div
style={{ width: 8, height: 8, borderRadius: '50%', background: '#3b82f6' }}
/>
)}
</div>
)
}
// Usage
function App() {
return (
<RadioGroup defaultValue="option1" onValueChange={console.log}>
<RadioItem value="option1">Option 1</RadioItem>
<RadioItem value="option2">Option 2</RadioItem>
<RadioItem value="option3">Option 3</RadioItem>
</RadioGroup>
)
}
```
</Tabs.Content>
## API Reference
### RadioGroup
<Tabs.Content value="styled">
RadioGroup extends Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
</Tabs.Content>
<Tabs.Content value="unstyled">
When using `createRadioGroup`, you provide styled components:
- `Frame`: The root container component
- `Item`: The radio item component
- `Indicator`: The indicator component
</Tabs.Content>
<Tabs.Content value="headless">
The `useRadioGroup` hook accepts these options:
</Tabs.Content>
<PropsTable
data={[
{
name: 'name',
type: 'string',
description: `Equivalent to input name attribute.`,
},
{
name: 'value',
type: 'string',
description: `Controlled value of the selected radio item.`,
},
{
name: 'defaultValue',
type: 'string',
description: `Default value for uncontrolled usage.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required attribute.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Sets aria-disabled on web, and disables touch on native for all children items.`,
},
{
name: 'native',
type: 'boolean',
description: `Renders native radio button on web.`,
default: 'false',
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: 'Called when the selected value changes.',
},
{
name: 'orientation',
type: '"horizontal" | "vertical"',
description: 'Orientation of the radio group.',
},
{
name: 'accentColor',
type: 'string',
description: 'Sets `accent-color` style when `native` prop is enabled.',
},
]}
/>
<Tabs.Content value="styled">
### RadioGroup.Item
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Sets aria-labelledby on web.`,
},
{
name: 'value',
type: 'string',
description: `Input value for the radio button.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Sets aria-disabled on web, and disables touch on native.`,
},
{
name: 'id',
type: 'string',
description: `ID used on the web.`,
},
{
name: 'scaleSize',
type: 'number',
default: '0.5',
description: `Scale factor for the radio item size. Tamagui size tokens map to button heights, so you typically want smaller radio buttons.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, removes all default Tamagui styling.`,
},
]}
/>
### RadioGroup.Indicator
RadioGroup.Indicator appears only when the parent Item is checked. It extends [ThemeableStack](/docs/components/stacks#themeablestack), getting [Tamagui standard props](/docs/intro/props) adding:
<PropsTable
data={[
{
name: 'unstyled',
required: false,
type: `boolean`,
description: `Removes all default Tamagui styles.`,
},
]}
/>
</Tabs.Content>
<Tabs.Content value="headless">
### useRadioGroup Return Value
| Property | Type | Description |
| ----------------------- | ------------------------ | --------------------------------------------------------------------- |
| `providerValue` | `RadioGroupContextValue` | Value to pass to your context provider |
| `frameAttrs` | `object` | Props to spread on the group container (role, aria-orientation, etc.) |
| `rovingFocusGroupAttrs` | `object` | Props for roving focus behavior (optional) |
### useRadioGroupItem
Hook for individual radio items. Requires a context containing the group state.
```tsx
const {
checked,
isFormControl,
providerValue,
bubbleInput,
native,
frameAttrs,
rovingFocusGroupAttrs,
} = useRadioGroupItem({
radioGroupContext: YourRadioGroupContext,
value: 'option1',
id: 'option1-id',
labelledBy: 'label-id',
disabled: false,
onPress: () => {},
onKeyDown: () => {},
onFocus: () => {},
})
```
| Property | Type | Description |
| --------------- | ----------- | --------------------------------------- |
| `checked` | `boolean` | Whether this item is currently selected |
| `providerValue` | `object` | Context value for the indicator |
| `frameAttrs` | `object` | Props to spread on the item element |
| `bubbleInput` | `ReactNode` | Hidden input for form compatibility |
| `native` | `boolean` | Whether using native radio |
| `isFormControl` | `boolean` | Whether inside a form |
### useRadioGroupItemIndicator
Hook for the visual indicator of checked state.
```tsx
const { checked, ...dataAttrs } = useRadioGroupItemIndicator({
radioGroupItemContext: YourRadioGroupItemContext,
disabled: false,
})
```
| Property | Type | Description |
| --------------- | --------------------- | ---------------------------------- |
| `checked` | `boolean` | Whether the parent item is checked |
| `data-state` | `string` | 'checked' or 'unchecked' |
| `data-disabled` | `string \| undefined` | Present when disabled |
</Tabs.Content>
</Tabs>
## components/roving-focus/2.0.0
---
title: RovingFocusGroup
description: Manage keyboard navigation within a group of focusable elements.
name: roving-focus
component: RovingFocusGroup
package: roving-focus
---
<IntroParagraph marginTop={-5} marginBottom={30}>
A utility component for managing keyboard focus within a group of elements using the
roving tabindex pattern. Enables arrow key navigation between focusable items while
maintaining a single tab stop for the group.
</IntroParagraph>
Note that this is primarily a web component. On native it renders children without keyboard navigation management.
<Highlights
features={[
'Arrow key navigation between items (respects orientation).',
'Single tab stop for the entire group.',
'Optional looping from last to first item.',
'RTL direction support.',
'Tracks active/current item state.',
]}
/>
## Installation
RovingFocusGroup is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/roving-focus
```
## Usage
Wrap focusable items with RovingFocusGroup and use RovingFocusGroup.Item for each focusable element:
```tsx
import { Button, RovingFocusGroup, XStack } from 'tamagui'
export default () => (
<RovingFocusGroup orientation="horizontal" loop>
<XStack gap="$2">
<RovingFocusGroup.Item asChild>
<Button>First</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item asChild>
<Button>Second</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item asChild>
<Button>Third</Button>
</RovingFocusGroup.Item>
</XStack>
</RovingFocusGroup>
)
```
## How It Works
The roving tabindex pattern allows a group of focusable elements to act as a single tab stop. When the user tabs into the group, focus moves to the currently active item. Arrow keys then navigate between items within the group.
This improves keyboard navigation by:
- Reducing the number of tab stops on a page
- Providing intuitive arrow key navigation within related controls
- Maintaining focus state across the group
## Orientation
Set `orientation` to control which arrow keys navigate:
```tsx
import { Button, RovingFocusGroup, YStack } from 'tamagui'
export default () => (
// Up/Down arrows navigate, Left/Right are ignored
<RovingFocusGroup orientation="vertical">
<YStack gap="$2">
<RovingFocusGroup.Item asChild>
<Button>Option 1</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item asChild>
<Button>Option 2</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item asChild>
<Button>Option 3</Button>
</RovingFocusGroup.Item>
</YStack>
</RovingFocusGroup>
)
```
## Looping Navigation
Enable `loop` to wrap focus from the last item back to the first:
```tsx
import { Button, RovingFocusGroup, XStack } from 'tamagui'
export default () => (
<RovingFocusGroup loop>
<XStack gap="$2">
<RovingFocusGroup.Item asChild>
<Button>First</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item asChild>
<Button>Middle</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item asChild>
<Button>Last</Button>
</RovingFocusGroup.Item>
{/* Arrow right from "Last" goes to "First" */}
</XStack>
</RovingFocusGroup>
)
```
## Controlled Tab Stop
Control which item is the current tab stop:
```tsx
import { Button, RovingFocusGroup, XStack } from 'tamagui'
import { useState } from 'react'
export default () => {
const [currentId, setCurrentId] = useState<string | null>('item-2')
return (
<RovingFocusGroup
currentTabStopId={currentId}
onCurrentTabStopIdChange={setCurrentId}
>
<XStack gap="$2">
<RovingFocusGroup.Item tabStopId="item-1" asChild>
<Button>First</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item tabStopId="item-2" asChild>
<Button>Second (default)</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item tabStopId="item-3" asChild>
<Button>Third</Button>
</RovingFocusGroup.Item>
</XStack>
</RovingFocusGroup>
)
}
```
## Non-Focusable Items
Mark items as non-focusable to skip them during keyboard navigation:
```tsx
import { Button, RovingFocusGroup, XStack } from 'tamagui'
export default () => (
<RovingFocusGroup>
<XStack gap="$2">
<RovingFocusGroup.Item asChild>
<Button>Enabled</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item focusable={false} asChild>
<Button disabled>Disabled</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item asChild>
<Button>Enabled</Button>
</RovingFocusGroup.Item>
</XStack>
</RovingFocusGroup>
)
```
## Entry Focus Control
Handle focus when the group first receives focus:
```tsx
import { Button, RovingFocusGroup, XStack } from 'tamagui'
export default () => (
<RovingFocusGroup
onEntryFocus={(event) => {
// Prevent default focus behavior
// event.preventDefault()
console.log('Group received focus')
}}
>
<XStack gap="$2">
<RovingFocusGroup.Item asChild>
<Button>First</Button>
</RovingFocusGroup.Item>
<RovingFocusGroup.Item asChild>
<Button>Second</Button>
</RovingFocusGroup.Item>
</XStack>
</RovingFocusGroup>
)
```
## API Reference
### RovingFocusGroup
<PropsTable
data={[
{
name: 'orientation',
type: '"horizontal" | "vertical"',
description:
'The orientation of the group. Determines which arrow keys are used for navigation (left/right vs up/down).',
},
{
name: 'dir',
type: '"ltr" | "rtl"',
description:
'The reading direction. When set to rtl, left and right arrow keys are reversed.',
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description:
'When true, keyboard navigation will loop from last to first and vice versa.',
},
{
name: 'currentTabStopId',
type: 'string | null',
description: 'The controlled id of the current tab stop.',
},
{
name: 'defaultCurrentTabStopId',
type: 'string',
description: 'The default id of the current tab stop for uncontrolled usage.',
},
{
name: 'onCurrentTabStopIdChange',
type: '(tabStopId: string | null) => void',
description: 'Callback when the current tab stop changes.',
},
{
name: 'onEntryFocus',
type: '(event: Event) => void',
description:
'Callback when focus enters the group via keyboard. Call event.preventDefault() to prevent default focus behavior.',
},
{
name: 'asChild',
type: 'boolean',
default: 'false',
description: 'When true, renders as a Slot, merging props onto the child element.',
},
]}
/>
### RovingFocusGroup.Item
<PropsTable
data={[
{
name: 'tabStopId',
type: 'string',
description: 'A unique identifier for this item. Auto-generated if not provided.',
},
{
name: 'focusable',
type: 'boolean',
default: 'true',
description:
'Whether this item should be focusable. Set to false to skip during keyboard navigation.',
},
{
name: 'active',
type: 'boolean',
default: 'false',
description:
'Whether this item is considered active. Active items receive focus priority when the group is entered.',
},
{
name: 'asChild',
type: 'boolean',
default: 'false',
description: 'When true, renders as a Slot, merging props onto the child element.',
},
]}
/>
## Keyboard Navigation
| Key | Action |
| ------------------- | ------------------------------------------------- |
| `Tab` | Move focus into/out of the group |
| `Arrow Left/Right` | Move focus between items (horizontal orientation) |
| `Arrow Up/Down` | Move focus between items (vertical orientation) |
| `Home` / `Page Up` | Move focus to first item |
| `End` / `Page Down` | Move focus to last item |
## Usage in Components
RovingFocusGroup is used internally by several Tamagui components to provide keyboard navigation:
- [Tabs](/docs/components/tabs) - Navigate between tab triggers
- [RadioGroup](/docs/components/radio-group) - Navigate between radio options
- [ToggleGroup](/docs/components/toggle-group) - Navigate between toggle options
<Notice>
RovingFocusGroup is primarily designed for web platforms. On React Native, it renders
children without keyboard navigation management since native platforms handle focus
differently.
</Notice>
## components/scroll-view/1.0.0
---
title: ScrollView
description: React Native ScrollView with Tamagui props.
name: scrollView
component: ScrollView
package: scroll-view
demoName: ScrollView
---
<HeroContainer>
<ScrollViewDemo />
</HeroContainer>
```tsx hero template=ScrollView
```
<Highlights
features={[
`All the features of React Native ScrollView.`,
`Adds all the style properties of Tamagui.`,
]}
/>
## Installation
ScrollView is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/scroll-view
```
## Usage
```tsx
import { ScrollView, YStack, ListItem } from 'tamagui'
export default () => (
<ScrollView>
<YStack>
<ListItem>1</ListItem>
<ListItem>2</ListItem>
<ListItem>3</ListItem>
<ListItem>4</ListItem>
</YStack>
</ScrollView>
)
```
## API Reference
### ScrollView
See [React Native ScrollView](https://reactnative.dev/docs/scrollview) and [Tamagui style props](https://tamagui.dev/docs/intro/props).
## components/scroll-view/2.0.0
---
title: ScrollView
description: React Native ScrollView with Tamagui props
name: scrollView
component: ScrollView
package: scroll-view
demoName: ScrollView
---
<HeroContainer>
<ScrollViewDemo />
</HeroContainer>
```tsx hero template=ScrollView
```
<Highlights
features={[
`All the features of React Native ScrollView.`,
`Adds all the style properties of Tamagui.`,
]}
/>
## Installation
ScrollView is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/scroll-view
```
## Usage
```tsx
import { ScrollView, YStack, ListItem } from 'tamagui'
export default () => (
<ScrollView>
<YStack>
<ListItem>1</ListItem>
<ListItem>2</ListItem>
<ListItem>3</ListItem>
<ListItem>4</ListItem>
</YStack>
</ScrollView>
)
```
## API Reference
### ScrollView
See [React Native ScrollView](https://reactnative.dev/docs/scrollview) and [Tamagui style props](https://tamagui.dev/docs/intro/props).
## components/select/1.0.0
---
title: Select
description: A simple select component
name: select
component: Select
package: select
demoName: Select
---
# Select
<Description>Show a menu of items users can select from one of.</Description>
<HeroContainer>
<SelectDemo />
</HeroContainer>
```tsx hero template=Select
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible, keyboard navigable, full-featured.`,
]}
/>
### Anatomy
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
<Notice>
Note that Select only works on Native using the Adapt component to adapt it to a Sheet.
See below for docs.
</Notice>
### API
#### <Select />
Contains every component for the select:
<PropsTable
data={[
{
name: 'id',
type: 'string',
description: `Optional for usage with Label`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Set the size of itself and pass to all inner elements`,
},
{
name: 'children',
type: 'React.ReactNode',
description: `Select children API components`,
},
{
name: 'value',
type: 'string',
description: `Controlled value`,
},
{
name: 'defaultValue',
type: 'string',
description: `Default value`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `Callback on value change`,
},
{
name: 'open',
type: 'boolean',
description: `Controlled open value`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Default open value`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Callback on open change`,
},
{
name: 'dir',
type: 'Direction',
description: `Direction of text display`,
},
{
name: 'name',
type: 'string',
description: `For use in forms`,
},
]}
/>
#### <Trigger />
Extends [ListItem](/docs/components/list-item) to give sizing, icons, and more.
#### <Value />
Extends [Paragraph](/docs/components/text), adding:
<PropsTable
data={[
{
name: 'placeholder',
type: 'string',
description: `Optional placeholder to show when no value selected`,
},
]}
/>
#### <Content />
Main container for Select content, used to contain the up/down arrows, no API beyond children.
#### <ScrollUpButton />
Inside Content first, displays when you can scroll up, stuck to the top.
Extends [YStack](/docs/components/stacks).
#### <ScrollDownButton />
Inside Content last, displays when you can scroll down, stuck to the bottom.
Extends [YStack](/docs/components/stacks).
#### <Viewport />
Extends [ThemeableStack](/docs/components/stacks#themeablestack). Contains scrollable content items as children.
<PropsTable
data={[
{
name: 'disableScroll',
type: 'boolean',
description: `Removes ability to scroll and all style and functionality related to scrolling`,
},
]}
/>
#### <Group />
Extends [YStack](/docs/components/stacks). Use only when grouping together items, alongside a Label as the first child.
#### <Label />
Extends [ListItem](/docs/components/list-item). Used to label Groups.
#### <Item />
Extends [ListItem](/docs/components/list-item). Used to add selectable values ot the list. Must provide an index as React Native doesn't give any escape hatch for us to configure that automatically.
<PropsTable
data={[
{
name: 'index',
type: 'number',
required: true,
description: `Incrementally starting from 0, matching its appearance in the list.`,
},
{
name: 'value',
type: 'string',
description: `Provide a value that will be passed on selection.`,
},
]}
/>
#### <ItemText />
Extends [Paragraph](/docs/components/text). Used inside Item to provide unselectable text that will show above once selected in the parent Select.
#### <Sheet />
When used alongside `<Adapt />`, Select will render as a sheet when that breakpoint is active.
This is the only way to render a Select on Native for now, as mobile apps tend to show Select very differently from web and Tamagui wants to present the right abstractions for each platform.
See [Sheet](/docs/components/sheet) for more props.
Must use `Select.SheetContents` inside the `Select.Sheet.Frame` to insert the contents given to `Select.Content`
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Adapt when="maxMd" platform="touch">
{/* or <Select.Sheet> */}
<Sheet>
<Sheet.Frame>
<SheetContents />
</Sheet.Frame>
<Sheet.Overlay />
</Sheet>
</Adapt>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
## components/select/1.128.0
---
title: Select
description: Show a menu of items that users can select from.
name: select
component: Select
package: select
demoName: Select
---
<HeroContainer>
<SelectDemo />
</HeroContainer>
```tsx hero template=Select
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible, keyboard navigable, full-featured.`,
]}
/>
## Installation
Select is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/select
```
## Anatomy
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
{/* Optional: Control focus behavior */}
<Select.FocusScope loop trapped focusOnIdle={true}>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select.FocusScope>
</Select>
)
```
<Notice>
Note that Select only works on Native using the Adapt component to adapt it to a Sheet.
See below for docs.
</Notice>
## API Reference
### Select
Contains every component for the select:
<PropsTable
data={[
{
name: 'id',
type: 'string',
description: `Optional for usage with Label`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Set the size of itself and pass to all inner elements`,
},
{
name: 'children',
type: 'React.ReactNode',
description: `Select children API components`,
},
{
name: 'value',
type: 'string',
description: `Controlled value`,
},
{
name: 'defaultValue',
type: 'string',
description: `Default value`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `Callback on value change`,
},
{
name: 'open',
type: 'boolean',
description: `Controlled open value`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Default open value`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Callback on open change`,
},
{
name: 'dir',
type: 'Direction',
description: `Direction of text display`,
},
{
name: 'name',
type: 'string',
description: `For use in forms`,
},
{
name: 'native',
type: 'NativeValue',
description: `If passed, will render a native component instead of the custom one. Currently only \`web\` is supported.`,
},
]}
/>
### Select.Trigger
Extends [ListItem](/docs/components/list-item) to give sizing, icons, and more.
#### Select.Value
Extends [Paragraph](/docs/components/text), adding:
<PropsTable
data={[
{
name: 'placeholder',
type: 'string',
description: `Optional placeholder to show when no value selected`,
},
]}
/>
#### SelectContent
Main container for Select content, used to contain the up/down arrows, no API beyond children.
#### Select.ScrollUpButton
Inside Content first, displays when you can scroll up, stuck to the top.
Extends [YStack](/docs/components/stacks).
### Select.ScrollDownButton
Inside Content last, displays when you can scroll down, stuck to the bottom.
Extends [YStack](/docs/components/stacks).
### Select.Viewport
Extends [ThemeableStack](/docs/components/stacks#themeablestack). Contains scrollable content items as children.
<PropsTable
data={[
{
name: 'disableScroll',
type: 'boolean',
description: `Removes ability to scroll and all style and functionality related to scrolling`,
},
{
name: 'unstyled',
type: 'boolean',
description: `Removes all default styles`,
},
]}
/>
<Notice>
Make sure to not pass `height` prop as that is managed internally because of UX reasons
and having a fixed height will break that behavior
</Notice>
### Select.Group
Extends [YStack](/docs/components/stacks). Use only when grouping together items, alongside a Label as the first child.
### Select.Label
Extends [ListItem](/docs/components/list-item). Used to label Groups.
### Select.Item
Extends [ListItem](/docs/components/list-item). Used to add selectable values to the list. Must provide an index as React Native doesn't give any escape hatch for us to configure that automatically.
<PropsTable
data={[
{
name: 'index',
type: 'number',
required: true,
description: `Incrementally starting from 0, matching its appearance in the list.`,
},
{
name: 'value',
type: 'string',
description: `Provide a value that will be passed on selection.`,
},
]}
/>
### Select.ItemText
Extends [Paragraph](/docs/components/text). Used inside Item to provide unselectable text that will show above once selected in the parent Select.
### Select.FocusScope
Provides access to the underlying FocusScope component used by Select for focus management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Select.Sheet
When used alongside `<Adapt />`, Select will render as a sheet when that breakpoint is active.
This is the only way to render a Select on Native for now, as mobile apps tend to show Select very differently from web and Tamagui wants to present the right abstractions for each platform.
See [Sheet](/docs/components/sheet) for more props.
Must use `Select.Adapt.Contents` inside the `Select.Sheet.Frame` to insert the contents given to `Select.Content`
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Adapt when="maxMd" platform="touch">
{/* or <Select.Sheet> */}
<Sheet>
<Sheet.Frame>
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay />
</Sheet>
</Adapt>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
## components/select/1.141.0
---
title: Select
description: Show a menu of items that users can select from.
name: select
component: Select
package: select
demoName: Select
---
<HeroContainer>
<SelectDemo />
</HeroContainer>
```tsx hero template=Select
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible, keyboard navigable, full-featured.`,
]}
/>
## Installation
Select is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/select
```
## Anatomy
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
{/* Optional: Control focus behavior */}
<Select.FocusScope loop trapped focusOnIdle={true}>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select.FocusScope>
</Select>
)
```
<Notice>
Note that Select only works on Native using the Adapt component to adapt it to a Sheet.
See below for docs.
</Notice>
## Server-Side Rendering (SSR)
By default, Select uses a portal-based mechanism to display the selected item's text in the trigger. This happens in a client-side effect, which can cause hydration mismatches when using SSR.
To fix this, use the `renderValue` prop to provide the label synchronously during render:
```tsx
const items = [
{ value: 'apple', label: 'Apple' },
{ value: 'orange', label: 'Orange' },
]
// Create a lookup function
const getLabel = (value: string) => items.find((item) => item.value === value)?.label
export function MySelect() {
const [value, setValue] = React.useState('apple')
return (
<Select value={value} onValueChange={setValue} renderValue={getLabel}>
<Select.Trigger>
<Select.Value placeholder="Select a fruit..." />
</Select.Trigger>
<Select.Content>
<Select.Viewport>
{items.map((item, i) => (
<Select.Item key={item.value} value={item.value} index={i}>
<Select.ItemText>{item.label}</Select.ItemText>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select>
)
}
```
When `renderValue` is provided, it's called synchronously during render, ensuring the server and client render the same content.
## API Reference
### Select
Contains every component for the select:
<PropsTable
data={[
{
name: 'id',
type: 'string',
description: `Optional for usage with Label`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Set the size of itself and pass to all inner elements`,
},
{
name: 'children',
type: 'React.ReactNode',
description: `Select children API components`,
},
{
name: 'value',
type: 'string',
description: `Controlled value`,
},
{
name: 'defaultValue',
type: 'string',
description: `Default value`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `Callback on value change`,
},
{
name: 'open',
type: 'boolean',
description: `Controlled open value`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Default open value`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Callback on open change`,
},
{
name: 'dir',
type: 'Direction',
description: `Direction of text display`,
},
{
name: 'name',
type: 'string',
description: `For use in forms`,
},
{
name: 'native',
type: 'NativeValue',
description: `If passed, will render a native component instead of the custom one. Currently only \`web\` is supported.`,
},
{
name: 'renderValue',
type: '(value: string) => React.ReactNode',
description: `Render function for the selected value. Use this for SSR support to avoid hydration mismatches. When provided, this is called synchronously during render to display the selected value.`,
},
]}
/>
### Select.Trigger
Extends [ListItem](/docs/components/list-item) to give sizing, icons, and more.
#### Select.Value
Extends [Paragraph](/docs/components/text), adding:
<PropsTable
data={[
{
name: 'placeholder',
type: 'string',
description: `Optional placeholder to show when no value selected`,
},
]}
/>
#### SelectContent
Main container for Select content, used to contain the up/down arrows, no API beyond children.
#### Select.ScrollUpButton
Inside Content first, displays when you can scroll up, stuck to the top.
Extends [YStack](/docs/components/stacks).
### Select.ScrollDownButton
Inside Content last, displays when you can scroll down, stuck to the bottom.
Extends [YStack](/docs/components/stacks).
### Select.Viewport
Extends [ThemeableStack](/docs/components/stacks#themeablestack). Contains scrollable content items as children.
<PropsTable
data={[
{
name: 'disableScroll',
type: 'boolean',
description: `Removes ability to scroll and all style and functionality related to scrolling`,
},
{
name: 'unstyled',
type: 'boolean',
description: `Removes all default styles`,
},
]}
/>
<Notice>
Make sure to not pass `height` prop as that is managed internally because of UX reasons
and having a fixed height will break that behavior
</Notice>
### Select.Group
Extends [YStack](/docs/components/stacks). Use only when grouping together items, alongside a Label as the first child.
### Select.Label
Extends [ListItem](/docs/components/list-item). Used to label Groups.
### Select.Item
Extends [ListItem](/docs/components/list-item). Used to add selectable values to the list. Must provide an index as React Native doesn't give any escape hatch for us to configure that automatically.
<PropsTable
data={[
{
name: 'index',
type: 'number',
required: true,
description: `Incrementally starting from 0, matching its appearance in the list.`,
},
{
name: 'value',
type: 'string',
description: `Provide a value that will be passed on selection.`,
},
]}
/>
### Select.ItemText
Extends [Paragraph](/docs/components/text). Used inside Item to provide unselectable text that will show above once selected in the parent Select.
### Select.FocusScope
Provides access to the underlying FocusScope component used by Select for focus management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented`,
},
]}
/>
### Select.Sheet
When used alongside `<Adapt />`, Select will render as a sheet when that breakpoint is active.
This is the only way to render a Select on Native for now, as mobile apps tend to show Select very differently from web and Tamagui wants to present the right abstractions for each platform.
See [Sheet](/docs/components/sheet) for more props.
Must use `Select.Adapt.Contents` inside the `Select.Sheet.Frame` to insert the contents given to `Select.Content`
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Adapt when="maxMd" platform="touch">
{/* or <Select.Sheet> */}
<Sheet>
<Sheet.Frame>
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay />
</Sheet>
</Adapt>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
## components/select/1.19.0
---
title: Select
description: A simple select component
name: select
component: Select
package: select
demoName: Select
---
# Select
<Description>Show a menu of items users can select from one of</Description>
<HeroContainer>
<SelectDemo />
</HeroContainer>
```tsx hero template=Select
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible, keyboard navigable, full-featured.`,
]}
/>
## Anatomy
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
<Notice>
Note that Select only works on Native using the Adapt component to adapt it to a Sheet.
See below for docs.
</Notice>
## API Reference
### Select
Contains every component for the select:
<PropsTable
data={[
{
name: 'id',
type: 'string',
description: `Optional for usage with Label`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Set the size of itself and pass to all inner elements`,
},
{
name: 'children',
type: 'React.ReactNode',
description: `Select children API components`,
},
{
name: 'value',
type: 'string',
description: `Controlled value`,
},
{
name: 'defaultValue',
type: 'string',
description: `Default value`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `Callback on value change`,
},
{
name: 'open',
type: 'boolean',
description: `Controlled open value`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Default open value`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Callback on open change`,
},
{
name: 'dir',
type: 'Direction',
description: `Direction of text display`,
},
{
name: 'name',
type: 'string',
description: `For use in forms`,
},
{
name: 'native',
type: 'NativeValue',
description: `If passed, will render a native component instead of the custom one. Currently only \`web\` is supported.`,
},
]}
/>
### Select.Trigger
Extends [ListItem](/docs/components/list-item) to give sizing, icons, and more.
#### Select.Value
Extends [Paragraph](/docs/components/text), adding:
<PropsTable
data={[
{
name: 'placeholder',
type: 'string',
description: `Optional placeholder to show when no value selected`,
},
]}
/>
#### SelectContent
Main container for Select content, used to contain the up/down arrows, no API beyond children.
#### Select.ScrollUpButton
Inside Content first, displays when you can scroll up, stuck to the top.
Extends [YStack](/docs/components/stacks).
### Select.ScrollDownButton
Inside Content last, displays when you can scroll down, stuck to the bottom.
Extends [YStack](/docs/components/stacks).
### Select.Viewport
Extends [ThemeableStack](/docs/components/stacks#themeablestack). Contains scrollable content items as children.
<PropsTable
data={[
{
name: 'disableScroll',
type: 'boolean',
description: `Removes ability to scroll and all style and functionality related to scrolling`,
},
]}
/>
### Select.Group
Extends [YStack](/docs/components/stacks). Use only when grouping together items, alongside a Label as the first child.
### Select.Label
Extends [ListItem](/docs/components/list-item). Used to label Groups.
### Select.Item
Extends [ListItem](/docs/components/list-item). Used to add selectable values ot the list. Must provide an index as React Native doesn't give any escape hatch for us to configure that automatically.
<PropsTable
data={[
{
name: 'index',
type: 'number',
required: true,
description: `Incrementally starting from 0, matching its appearance in the list.`,
},
{
name: 'value',
type: 'string',
description: `Provide a value that will be passed on selection.`,
},
]}
/>
### Select.ItemText
Extends [Paragraph](/docs/components/text). Used inside Item to provide unselectable text that will show above once selected in the parent Select.
### Select.Sheet
When used alongside `<Adapt />`, Select will render as a sheet when that breakpoint is active.
This is the only way to render a Select on Native for now, as mobile apps tend to show Select very differently from web and Tamagui wants to present the right abstractions for each platform.
See [Sheet](/docs/components/sheet) for more props.
Must use `Select.SheetContents` inside the `Select.Sheet.Frame` to insert the contents given to `Select.Content`
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Adapt when="maxMd" platform="touch">
{/* or <Select.Sheet> */}
<Sheet>
<Sheet.Frame>
<SheetContents />
</Sheet.Frame>
<Sheet.Overlay />
</Sheet>
</Adapt>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
## components/select/1.40.0
---
title: Select
description: Show a menu of items that users can select from.
name: select
component: Select
package: select
demoName: Select
---
<HeroContainer>
<SelectDemo />
</HeroContainer>
```tsx hero template=Select
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible, keyboard navigable, full-featured.`,
]}
/>
## Installation
Select is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/select
```
## Anatomy
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
<Notice>
Note that Select only works on Native using the Adapt component to adapt it to a Sheet.
See below for docs.
</Notice>
## API Reference
### Select
Contains every component for the select:
<PropsTable
data={[
{
name: 'id',
type: 'string',
description: `Optional for usage with Label`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Set the size of itself and pass to all inner elements`,
},
{
name: 'children',
type: 'React.ReactNode',
description: `Select children API components`,
},
{
name: 'value',
type: 'string',
description: `Controlled value`,
},
{
name: 'defaultValue',
type: 'string',
description: `Default value`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `Callback on value change`,
},
{
name: 'open',
type: 'boolean',
description: `Controlled open value`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Default open value`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Callback on open change`,
},
{
name: 'dir',
type: 'Direction',
description: `Direction of text display`,
},
{
name: 'name',
type: 'string',
description: `For use in forms`,
},
{
name: 'native',
type: 'NativeValue',
description: `If passed, will render a native component instead of the custom one. Currently only \`web\` is supported.`,
},
]}
/>
### Select.Trigger
Extends [ListItem](/docs/components/list-item) to give sizing, icons, and more.
#### Select.Value
Extends [Paragraph](/docs/components/text), adding:
<PropsTable
data={[
{
name: 'placeholder',
type: 'string',
description: `Optional placeholder to show when no value selected`,
},
]}
/>
#### SelectContent
Main container for Select content, used to contain the up/down arrows, no API beyond children.
#### Select.ScrollUpButton
Inside Content first, displays when you can scroll up, stuck to the top.
Extends [YStack](/docs/components/stacks).
### Select.ScrollDownButton
Inside Content last, displays when you can scroll down, stuck to the bottom.
Extends [YStack](/docs/components/stacks).
### Select.Viewport
Extends [ThemeableStack](/docs/components/stacks#themeablestack). Contains scrollable content items as children.
<PropsTable
data={[
{
name: 'disableScroll',
type: 'boolean',
description: `Removes ability to scroll and all style and functionality related to scrolling`,
},
{
name: 'unstyled',
type: 'boolean',
description: `Removes all default styles`,
},
]}
/>
<Notice>
Make sure to not pass `height` prop as that is managed internally because of UX reasons
and having a fixed height will break that behavior
</Notice>
### Select.Group
Extends [YStack](/docs/components/stacks). Use only when grouping together items, alongside a Label as the first child.
### Select.Label
Extends [ListItem](/docs/components/list-item). Used to label Groups.
### Select.Item
Extends [ListItem](/docs/components/list-item). Used to add selectable values ot the list. Must provide an index as React Native doesn't give any escape hatch for us to configure that automatically.
<PropsTable
data={[
{
name: 'index',
type: 'number',
required: true,
description: `Incrementally starting from 0, matching its appearance in the list.`,
},
{
name: 'value',
type: 'string',
description: `Provide a value that will be passed on selection.`,
},
]}
/>
### Select.ItemText
Extends [Paragraph](/docs/components/text). Used inside Item to provide unselectable text that will show above once selected in the parent Select.
### Select.Sheet
When used alongside `<Adapt />`, Select will render as a sheet when that breakpoint is active.
This is the only way to render a Select on Native for now, as mobile apps tend to show Select very differently from web and Tamagui wants to present the right abstractions for each platform.
See [Sheet](/docs/components/sheet) for more props.
Must use `Select.Adapt.Contents` inside the `Select.Sheet.Frame` to insert the contents given to `Select.Content`
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Adapt when="maxMd" platform="touch">
{/* or <Select.Sheet> */}
<Sheet>
<Sheet.Frame>
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay />
</Sheet>
</Adapt>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
## components/select/2.0.0
---
title: Select
description: Show a menu of items that users can select from
name: select
component: Select
package: select
demoName: Select
---
<HeroContainer>
<SelectDemo />
</HeroContainer>
```tsx hero template=Select
```
<Highlights
features={[
`Comes with styling, yet completely customizable and themeable.`,
`Accepts animations, themes, size props and more.`,
`Accessible, keyboard navigable, full-featured.`,
]}
/>
Select provides a dropdown menu for choosing from a list of options. It's fully accessible with keyboard navigation, supports typeahead search, and [automatically stacks](/ui/z-index) above other content.
## Installation
Select is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/select
```
<Notice theme="blue">
For native apps, we recommend [setting up native
portals](/docs/components/portal#native-portal-setup-recommended) to preserve React
context inside Select content.
</Notice>
## Anatomy
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
{/* Optional: Control focus behavior */}
<Select.FocusScope loop trapped focusOnIdle={true}>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select.FocusScope>
</Select>
)
```
<Notice>
Note that Select only works on Native using the Adapt component to adapt it to a Sheet.
See below for docs.
</Notice>
## API Reference
### Select
Contains every component for the select:
<PropsTable
data={[
{
name: 'id',
type: 'string',
description: `Optional for usage with Label.`,
},
{
name: 'size',
type: 'SizeTokens',
description: `Set the size of itself and pass to all inner elements.`,
},
{
name: 'children',
type: 'React.ReactNode',
description: `Select children API components.`,
},
{
name: 'value',
type: 'string',
description: `Controlled value.`,
},
{
name: 'defaultValue',
type: 'string',
description: `Default value.`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `Callback on value change.`,
},
{
name: 'open',
type: 'boolean',
description: `Controlled open value.`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Default open value.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Callback on open change.`,
},
{
name: 'dir',
type: 'Direction',
description: `Direction of text display.`,
},
{
name: 'name',
type: 'string',
description: `For use in forms.`,
},
{
name: 'native',
type: 'NativeValue',
description: `If passed, will render a native component instead of the custom one. Currently only \`web\` is supported.`,
},
{
name: 'renderValue',
type: '(value: string) => ReactNode',
description: `Render function for the selected value. Useful for SSR when you need custom display text. By default, Select falls back to showing the raw value for SSR compatibility.`,
},
{
name: 'lazyMount',
type: 'boolean',
default: 'false',
description: `When true, defers mounting Select items until opened using React's startTransition. Significantly improves initial render performance for pages with many Selects. Should be combined with \`renderValue\` for best results.`,
},
{
name: 'zIndex',
type: 'number',
description: `z-index for the select portal. Use when select dropdowns need to appear above other portaled content like dialogs or fixed headers. Defaults to automatic stacking (~100000).`,
},
]}
/>
### Select.Trigger
Extends [ListItem](/docs/components/list-item) to give sizing, icons, and more.
### Select.Value
Extends [Paragraph](/docs/components/text), adding:
<PropsTable
data={[
{
name: 'placeholder',
type: 'string',
description: `Optional placeholder to show when no value selected.`,
},
]}
/>
### Select.Content
Main container for Select content, used to contain the up/down arrows, no API beyond children.
### Select.ScrollUpButton
Inside Content first, displays when you can scroll up, stuck to the top.
Extends [YStack](/docs/components/stacks).
### Select.ScrollDownButton
Inside Content last, displays when you can scroll down, stuck to the bottom.
Extends [YStack](/docs/components/stacks).
### Select.Viewport
Extends [ThemeableStack](/docs/components/stacks#themeablestack). Contains scrollable content items as children.
<PropsTable
data={[
{
name: 'disableScroll',
type: 'boolean',
description: `Removes ability to scroll and all style and functionality related to scrolling.`,
},
{
name: 'unstyled',
type: 'boolean',
description: `Removes all default styles.`,
},
]}
/>
<Notice>
Make sure to not pass `height` prop as that is managed internally because of UX reasons
and having a fixed height will break that behavior.
</Notice>
### Select.Group
Extends [YStack](/docs/components/stacks). Use only when grouping together items, alongside a Label as the first child.
### Select.Label
Extends [SizableText](/docs/components/text). Used to label Groups. Includes size-based padding and minHeight for consistent appearance with other Select items.
### Select.Item
Extends [ListItem](/docs/components/list-item). Used to add selectable values to the list. Must provide an index as React Native doesn't give any escape hatch for us to configure that automatically.
<PropsTable
data={[
{
name: 'index',
type: 'number',
required: true,
description: `Incrementally starting from 0, matching its appearance in the list.`,
},
{
name: 'value',
type: 'string',
description: `Provide a value that will be passed on selection.`,
},
]}
/>
### Select.ItemText
Extends [Paragraph](/docs/components/text). Used inside Item to provide unselectable text that will show above once selected in the parent Select.
### Select.Indicator
An animated indicator that highlights the currently focused item. Place it inside `Select.Viewport` to enable a smooth sliding highlight animation as users navigate through options.
```tsx
<Select.Viewport>
<Select.Indicator transition="quick" />
<Select.Group>{/* items */}</Select.Group>
</Select.Viewport>
```
Use the `transition` prop to control the animation speed. You can use any animation name from your config like `quick`, `quicker`, or `quickest`.
<Notice>
By default, Select uses the item's `hoverStyle` and `pressStyle` for hover feedback. Add
`Select.Indicator` for a smoother animated effect. If using the indicator, you may want
to set items to have transparent hover styles to avoid visual conflict.
</Notice>
### Select.FocusScope
Provides access to the underlying FocusScope component used by Select for focus management. Can be used to control focus behavior from a parent component.
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: `Whether focus management is enabled.`,
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description: `When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable.`,
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description: `When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus.`,
},
{
name: 'focusOnIdle',
type: 'boolean | number',
default: 'false',
description: `When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations.`,
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on mount. Can be prevented.`,
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: `Event handler called when auto-focusing on unmount. Can be prevented.`,
},
]}
/>
## Performance
For pages with many Select components, you can significantly improve initial render performance by using the `lazyMount` prop combined with `renderValue`:
```tsx
const labels = {
apple: 'Apple',
orange: 'Orange',
banana: 'Banana',
}
<Select
lazyMount
renderValue={(value) => labels[value]}
defaultValue="apple"
>
{/* Items are not mounted until the Select is opened */}
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Viewport>
{/* These items mount in a startTransition when first opened */}
<Select.Item index={0} value="apple">
<Select.ItemText>Apple</Select.ItemText>
</Select.Item>
{/* ... more items */}
</Select.Viewport>
</Select.Content>
</Select>
```
**How it works:**
- `lazyMount` defers mounting all Select items until the dropdown is first opened
- The mount happens inside React's `startTransition`, keeping the trigger responsive
- `renderValue` provides the display text synchronously, avoiding the need to mount items just to show the selected value
- Once mounted, items stay mounted for fast subsequent opens
This is especially useful when rendering many Selects on a single page, as each Select with 20+ items would otherwise mount all those items on initial page load.
### Select.Sheet
When used alongside `<Adapt />`, Select will render as a sheet when that breakpoint is active.
This is the only way to render a Select on Native for now, as mobile apps tend to show Select very differently from web and Tamagui wants to present the right abstractions for each platform.
See [Sheet](/docs/components/sheet) for more props.
Must use `Select.Adapt.Contents` inside the `Select.Sheet.Frame` to insert the contents given to `Select.Content`
```tsx
import { Select } from 'tamagui' // or '@tamagui/select'
export default () => (
<Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
<Adapt when="max-md" platform="touch">
{/* or <Select.Sheet> */}
<Sheet>
<Sheet.Frame>
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay />
</Sheet>
</Adapt>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select>
)
```
## components/separator/1.0.0
---
title: Separator
description: Create borders between components.
name: separator
component: Separator
package: separator
demoName: Separator
---
<HeroContainer>
<SeparatorDemo />
</HeroContainer>
```tsx hero template=Separator
```
<Highlights features={[`Supports horizontal and vertical orientation.`]} />
## Installation
Separator is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/separator
```
## Usage
Separator uses the `borderWidth` and `borderColor` attribute for its style, so be sure to override those when extending it.
```tsx
export default () => (
<XStack alignItems="center">
<Paragraph>Blog</Paragraph>
<Separator alignSelf="stretch" vertical />
<Paragraph>Docs</Paragraph>
<Separator alignSelf="stretch" vertical />
<Paragraph>Source</Paragraph>
</XStack>
)
```
## API Reference
### Separator props
Separators extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'vertical',
required: false,
type: 'boolean',
description: `Show vertical separator.`,
},
]}
/>
## components/separator/2.0.0
---
title: Separator
description: Create borders between components
name: separator
component: Separator
package: separator
demoName: Separator
---
<HeroContainer>
<SeparatorDemo />
</HeroContainer>
```tsx hero template=Separator
```
<Highlights features={[`Supports horizontal and vertical orientation.`]} />
## Installation
Separator is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/separator
```
## Usage
Separator uses the `borderWidth` and `borderColor` attribute for its style, so be sure to override those when extending it.
```tsx
export default () => (
<XStack alignItems="center">
<Paragraph>Blog</Paragraph>
<Separator alignSelf="stretch" vertical />
<Paragraph>Docs</Paragraph>
<Separator alignSelf="stretch" vertical />
<Paragraph>Source</Paragraph>
</XStack>
)
```
## API Reference
### Separator
Separator extends Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'vertical',
required: false,
type: 'boolean',
description: `Shows the separator vertically.`,
},
]}
/>
## components/shapes/1.0.0
---
title: Shapes
description: Easy to use Square and Circle.
name: shapes
component: Square
package: shapes
demoName: Shapes
---
<HeroContainer>
<ShapesDemo />
</HeroContainer>
```tsx hero template=Shapes
```
<Highlights
features={[
'Accepts size props as number or token value.',
'Sets min and max width and height.',
]}
/>
## Installation
Shapes is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/shapes
```
## Usage
Tamagui supports sizing shapes using your `size` tokens, or plain numbers.
```tsx
import { Circle, Square } from 'tamagui'
export default () => (
<>
<Square size="$4" />
<Square size={100} />
<Circle size="$4" />
<Circle size={100} />
</>
)
```
## API Reference
### Square
`Square` extends Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'string | tokens.size',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'circular',
required: false,
type: 'boolean',
description: `Rounds the border radius to be circular.`,
},
]}
/>
### Circle
`Circle` extends [Square](#square), setting `circular` to `true`.
## components/shapes/2.0.0
---
title: Shapes
description: Easy to use Square and Circle
name: shapes
component: Square
package: shapes
demoName: Shapes
---
<HeroContainer>
<ShapesDemo />
</HeroContainer>
```tsx hero template=Shapes
```
<Highlights
features={[
'Accepts size props as number or token value.',
'Sets min and max width and height.',
]}
/>
## Installation
Shapes is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/shapes
```
## Usage
Tamagui supports sizing shapes using your `size` tokens, or plain numbers.
```tsx
import { Circle, Square } from 'tamagui'
export default () => (
<>
<Square size="$4" />
<Square size={100} />
<Circle size="$4" />
<Circle size={100} />
</>
)
```
## API Reference
### Square
Square extends Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'string | tokens.size',
description: `Set a size, number or one of the size token values.`,
},
{
name: 'circular',
required: false,
type: 'boolean',
description: `Rounds the border radius to be circular.`,
},
]}
/>
### Circle
Circle extends [Square](#square), setting `circular` to `true`.
## components/sheet/1.0.0
---
title: Sheet
description: A simple sheet component
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
# Sheet
<Description>A bottom sheet that slides up.</Description>
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap point points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
### Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
### API
#### <Sheet />
Contains every component for the sheet.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: 'number[]',
default: `[80, 10]`,
description: `Array of numbers, 0-100 that corresponds to % of the screen it should take up. Should go from most visible to least visible in order. Use "open" prop for fully closed.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
description: `By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
]}
/>
#### <Overlay />
Displays behind Frame. Extends [YStack](/docs/components/stacks).
#### <Frame />
Contains the content. Extends [YStack](/docs/components/stacks).
#### <Handle />
Shows a handle above the frame by default, on tap it will cycle between `snapPoints` but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
#### <Scrollview />
Allows scrolling within Sheet. Extends [Scrollview](/docs/components/scroll-view).
### Notes
For Android you need to manually re-propagate any context when using `modal`. This is because React Native doesn't support portals yet.
## components/sheet/1.116.0
---
title: Sheet
description: A simple sheet component
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
# Sheet
<Description>A bottom sheet that slides up</Description>
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap point points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
## Installation
Sheet is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/sheet
```
### PortalProvider
When rendering into root of app instead of inline, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
## Snap points
By default, snap points are treated as percentages.
```tsx
<Sheet snapPoints={[85, 50]}> // 85% and 50%
```
The behavior of snap points can be changed by setting the `snapPointsMode` prop to any of these values:
- **percent** (default) - Snap points are percentages of the parent container or screen as numbers
- **constant** - Snap points are raw pixel values as numbers
- **fit** - The sheet is constrained to the content's natural height without the `snapPoints` prop
- **mixed** - Snap points can be either numbers (pixels), percentages as strings (ex: `"50%"`), or `"fit"` for fit behavior
Snap points should be ordered from largest to smallest (most visible to least visible). When using `mixed` mode with the `"fit"` as a snap point, it must be the first and largest snap point.
## Unstyled
Adding the `unstyled` prop to your Handle, Overlay or Frame will turn off the default styles allowing you to customize without having to override any of the built-in styling.
## Headless with `createSheet`
Using the `createSheet` export, you can create a fully custom sheet without using any of the default styles. This is similar to `unstyled`, but it lets you also control the `open` variant.
Here's an example:
```tsx
import { Stack, styled } from '@tamagui/core'
import { createSheet } from '@tamagui/sheet'
const Handle = styled(Stack, {
variants: {
open: {
true: {
opacity: 0.35,
},
false: {
opacity: 0.5,
},
},
} as const,
})
const Overlay = styled(Stack, {
variants: {
open: {
true: {
opacity: 1,
pointerEvents: 'auto',
},
false: {
opacity: 0,
pointerEvents: 'none',
},
},
} as const,
})
const Frame = styled(Stack, {
backgroundColor: '$background',
// can add open variant as well
})
export const Sheet = createSheet({
Frame,
Handle,
Overlay,
})
```
## Native support
Sheets now support rendering to a native iOS sheet, while still rendering any of your React Native content inside of them.
Because Metro doesn't support conditional imports and we don't want to make `tamagui` enforce installing native dependencies in order to get started, there's an install step.
As of the time of writing, we are using the new `3.0.x` branch which is in beta. Until ready, it does require a bit more setup.
```sh
yarn add react-native-ios-modal@3.0.0-5 react-native-ios-utilities@next @dominicstop/ts-event-emitter
```
Then, rebuild your native iOS app so it picks up the new native dependencies. This is done either through Expo or plain React Native.
Finally, set it up:
```tsx
import { Sheet, setupNativeSheet } from '@tamagui/sheet'
import * as NativeModal from 'react-native-ios-modal'
setupNativeSheet('ios', NativeModal)
// now you can use the `native` prop:
export default <Sheet native>{/* ... the rest of your sheet */}</Sheet>
```
## API Reference
### Sheet
Contains every component for the sheet.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: '(number | string)[] | undefined',
default: `[80]`,
description: `Array of values representing different sizes for the sheet to snap to. Not used in 'fit' mode. See docs above for usage information.`,
},
{
name: 'snapPointsMode',
type: '"percent" | "constant" | "fit" | "mixed"',
default: '"percent"',
description: `Alters the behavior of the 'snapPoints' prop. See docs above for usage information.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'native',
type: 'boolean | "ios"[]',
description: `(iOS only) Render to a native sheet, must install native dependency first.`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
description: `By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
{
name: 'moveOnKeyboardChange',
type: 'boolean',
default: 'false',
description:
'Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.',
},
{
name: 'unmountChildrenWhenHidden',
type: 'boolean',
default: 'false',
description:
'Flag to enable unmounting the children after the exit animation has completed.',
},
]}
/>
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/sheet/1.59.0#portalprovider) for more information.
</Notice>
### Sheet.Overlay
Displays behind Frame. Extends [YStack](/docs/components/stacks).
### Sheet.Frame
Contains the content. Extends [YStack](/docs/components/stacks).
<PropsTable
data={[
{
name: 'disableHideBottomOverflow',
type: 'boolean',
description: `Disables Sheet cloning the Frame and positioning it below the frame, which helps to hide content that may appear underneath when spring animations bounce past 100%.`,
},
]}
/>
### Sheet.Handle
Shows a handle above the frame by default, on tap it will cycle between `snapPoints` but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
### Sheet.ScrollView
Allows scrolling within Sheet. Extends [ScrollView](/docs/components/scroll-view).
#### useSheet
Use this to control the sheet programatically.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'setOpen',
type: 'Function',
description: `Control the open state of the sheet.`,
},
{
name: 'setPosition',
type: '(index: number) => void',
description: `Control the position of the sheet.`,
},
]}
/>
## Notes
For Android you need to manually re-propagate any context when using `modal`. This is because React Native doesn't support portals yet.
## components/sheet/1.123.18
---
title: Sheet
description: A bottom sheet that animates.
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap point points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
## Installation
Sheet is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/sheet
```
### Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
### API
#### <Sheet />
Contains every component for the sheet.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: 'number[]',
default: `[80, 10]`,
description: `Array of numbers, 0-100 that corresponds to % of the screen it should take up. Should go from most visible to least visible in order. Use "open" prop for fully closed.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'native',
type: 'boolean | "ios"[]',
description: `(iOS only) Render to a native sheet, must install native dependency first.`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
description: `By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
{
name: 'moveOnKeyboardChange',
type: 'boolean',
default: 'false',
description:
'Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.',
},
]}
/>
#### <Overlay />
Displays behind Frame. Extends [YStack](/docs/components/stacks).
#### <Frame />
Contains the content. Extends [YStack](/docs/components/stacks).
#### <Handle />
Shows a handle above the frame by default, on tap it will cycle between `snapPoints` but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
#### <Scrollview />
Allows scrolling within Sheet. Extends [Scrollview](/docs/components/scroll-view).
### Notes
For Android you need to manually re-propagate any context when using `modal`. This is because React Native doesn't support portals yet.
### Native support
We've deprecated the `native` prop in favor of using Adapt.
## components/sheet/1.130.0
---
title: Sheet
description: A bottom sheet that animates.
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
## Installation
Sheet is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/sheet
```
### Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
### API
#### <Sheet />
Contains every component for the sheet.
<PropsTable
data={[
{
name: 'open',
type: 'boolean',
description: `Set to use as controlled component.`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: 'number[]',
default: `[80, 10]`,
description: `Array of numbers, 0-100 that corresponds to % of the screen it should take up. Should go from most visible to least visible in order. Use "open" prop for fully closed.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'native',
type: 'boolean | "ios"[]',
description: `(iOS only) Render to a native sheet, must install native dependency first.`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
description: `By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
{
name: 'moveOnKeyboardChange',
type: 'boolean',
default: 'false',
description:
'Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.',
},
{
name: 'preferAdaptParentOpenState',
type: 'boolean',
default: 'false',
description: `By default Sheet will prefer the open prop over a parent component that is controlling it via Adapt. In general if you want to Adapt to a sheet, you'd leave the open prop undefined. If you'd like to have the parent override the prop you've set manually on Sheet, set this to true.`,
},
]}
/>
#### <Overlay />
Displays behind Frame. Extends [YStack](/docs/components/stacks).
#### <Frame />
Contains the content. Extends [YStack](/docs/components/stacks).
#### <Handle />
Shows a handle above the frame by default, on tap it will cycle between
`snapPoints` but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
#### <ScrollView />
Allows scrolling within Sheet. Extends
[Scrollview](/docs/components/scroll-view).
### Notes
For Android you need to manually re-propagate any context when using `modal`.
This is because React Native doesn't support portals yet.
### Native support
We've deprecated the `native` prop in favor of using Adapt.
## components/sheet/1.21.0
---
title: Sheet
description: A simple sheet component
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
# Sheet
<Description>A bottom sheet that slides up</Description>
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap point points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
### Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
### API
#### <Sheet />
Contains every component for the sheet.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: 'number[]',
default: `[80, 10]`,
description: `Array of numbers, 0-100 that corresponds to % of the screen it should take up. Should go from most visible to least visible in order. Use "open" prop for fully closed.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'native',
type: 'boolean | "ios"[]',
description: `(iOS only) Render to a native sheet, must install native dependency first.`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
description: `By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
{
name: 'moveOnKeyboardChange',
type: 'boolean',
default: 'false',
description:
'Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.',
},
]}
/>
#### <Overlay />
Displays behind Frame. Extends [YStack](/docs/components/stacks).
#### <Frame />
Contains the content. Extends [YStack](/docs/components/stacks).
#### <Handle />
Shows a handle above the frame by default, on tap it will cycle between `snapPoints` but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
#### <Scrollview />
Allows scrolling within Sheet. Extends [Scrollview](/docs/components/scroll-view).
### Notes
For Android you need to manually re-propagate any context when using `modal`. This is because React Native doesn't support portals yet.
## components/sheet/1.27.0
---
title: Sheet
description: A simple sheet component
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
# Sheet
<Description>A bottom sheet that slides up</Description>
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap point points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
## Installation
Sheet is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/sheet
```
## Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
## Unstyled
Adding the `unstyled` prop to your Handle, Overlay or Frame will turn off the default styles allowing you to customize without having to override any of the built-in styling.
## Headless with `createSheet`
Using the `createSheet` export, you can create a fully custom sheet without using any of the default styles. This is similar to `unstyled`, but it lets you also control the `open` variant.
Here's an example:
```tsx
import { Stack, styled } from '@tamagui/core'
import { createSheet } from '@tamagui/sheet'
const Handle = styled(Stack, {
variants: {
open: {
true: {
opacity: 0.35,
},
false: {
opacity: 0.5,
},
},
} as const,
})
const Overlay = styled(Stack, {
variants: {
open: {
true: {
opacity: 1,
pointerEvents: 'auto',
},
false: {
opacity: 0,
pointerEvents: 'none',
},
},
} as const,
})
const Frame = styled(Stack, {
backgroundColor: '$background',
// can add open variant as well
})
export const Sheet = createSheet({
Frame,
Handle,
Overlay,
})
```
## Native support
Sheets now support rendering to a native iOS sheet, while still rendering any of your React Native content inside of them.
Because Metro doesn't support conditional imports and we don't want to make `tamagui` enforce installing native dependencies in order to get started, there's an install step:
```sh
yarn add react-native-ios-modal
pod install
# rebuild your app (expo ios, or use react-native cli)
```
And set it up as follows:
```tsx
import { Sheet, setupNativeSheet } from '@tamagui/sheet'
import { ModalView } from 'react-native-ios-modal'
setupNativeSheet('ios', ModalView)
export default (
<Sheet native>
{/* The rest of your sheet views, see Anatomy, example and props API */}
</Sheet>
)
```
## API Reference
### Sheet
Contains every component for the sheet.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: 'number[]',
default: `[80, 10]`,
description: `Array of numbers, 0-100 that corresponds to % of the screen it should take up. Should go from most visible to least visible in order. Use "open" prop for fully closed.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'native',
type: 'boolean | "ios"[]',
description: `(iOS only) Render to a native sheet, must install native dependency first.`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
description: `By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
{
name: 'moveOnKeyboardChange',
type: 'boolean',
default: 'false',
description:
'Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.',
},
]}
/>
### Sheet.Overlay
Displays behind Frame. Extends [YStack](/docs/components/stacks).
### Sheet.Frame
Contains the content. Extends [YStack](/docs/components/stacks).
<PropsTable
data={[
{
name: 'disableHideBottomOverflow',
type: 'boolean',
description: `Disables Sheet cloning the Frame and positioning it below the frame, which helps to hide content that may appear underneath when spring animations bounce past 100%.`,
},
]}
/>
### Sheet.Handle
Shows a handle above the frame by default, on tap it will cycle between `snapPoints` but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
### Sheet.ScrollView
Allows scrolling within Sheet. Extends [Scrollview](/docs/components/scroll-view).
#### useSheet
Use this to control the sheet programatically.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'setOpen',
type: 'Function',
description: `Control the open state of the sheet.`,
},
{
name: 'setPosition',
type: '(index: number) => void',
description: `Control the position of the sheet.`,
},
]}
/>
## Notes
For Android you need to manually re-propagate any context when using `modal`. This is because React Native doesn't support portals yet.
## components/sheet/1.59.0
---
title: Sheet
description: A simple sheet component
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
# Sheet
<Description>A bottom sheet that slides up</Description>
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap point points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
## Installation
Sheet is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/sheet
```
### PortalProvider
When rendering into root of app instead of inline, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
## Snap points
By default, snap points are treated as percentages.
```tsx
<Sheet snapPoints={[85, 50]}> // 85% and 50%
```
The behavior of snap points can be changed by setting the `snapPointsMode` prop to any of these values:
- **percent** (default) - Snap points are percentages of the parent container or screen as numbers
- **constant** - Snap points are raw pixel values as numbers
- **fit** - The sheet is constrained to the content's natural height without the `snapPoints` prop
- **mixed** - Snap points can be either numbers (pixels), percentages as strings (ex: `"50%"`), or `"fit"` for fit behavior
Snap points should be ordered from largest to smallest (most visible to least visible). When using `mixed` mode with the `"fit"` as a snap point, it must be the first and largest snap point.
## Unstyled
Adding the `unstyled` prop to your Handle, Overlay or Frame will turn off the default styles allowing you to customize without having to override any of the built-in styling.
## Headless with `createSheet`
Using the `createSheet` export, you can create a fully custom sheet without using any of the default styles. This is similar to `unstyled`, but it lets you also control the `open` variant.
Here's an example:
```tsx
import { Stack, styled } from '@tamagui/core'
import { createSheet } from '@tamagui/sheet'
const Handle = styled(Stack, {
variants: {
open: {
true: {
opacity: 0.35,
},
false: {
opacity: 0.5,
},
},
} as const,
})
const Overlay = styled(Stack, {
variants: {
open: {
true: {
opacity: 1,
pointerEvents: 'auto',
},
false: {
opacity: 0,
pointerEvents: 'none',
},
},
} as const,
})
const Frame = styled(Stack, {
backgroundColor: '$background',
// can add open variant as well
})
export const Sheet = createSheet({
Frame,
Handle,
Overlay,
})
```
## Native support
Sheets now support rendering to a native iOS sheet, while still rendering any of your React Native content inside of them.
Because Metro doesn't support conditional imports and we don't want to make `tamagui` enforce installing native dependencies in order to get started, there's an install step:
```sh
yarn add react-native-ios-modal
pod install
# rebuild your app (expo ios, or use react-native cli)
```
And set it up as follows:
```tsx
import { Sheet, setupNativeSheet } from '@tamagui/sheet'
import { ModalView } from 'react-native-ios-modal'
setupNativeSheet('ios', ModalView)
export default (
<Sheet native>
{/* The rest of your sheet views, see Anatomy, example and props API */}
</Sheet>
)
```
## API Reference
### Sheet
Contains every component for the sheet.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: '(number | string)[] | undefined',
default: `[80]`,
description: `Array of values representing different sizes for the sheet to snap to. Not used in 'fit' mode. See docs above for usage information.`,
},
{
name: 'snapPointsMode',
type: '"percent" | "constant" | "fit" | "mixed"',
default: '"percent"',
description: `Alters the behavior of the 'snapPoints' prop. See docs above for usage information.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'native',
type: 'boolean | "ios"[]',
description: `(iOS only) Render to a native sheet, must install native dependency first.`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
description: `By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
{
name: 'moveOnKeyboardChange',
type: 'boolean',
default: 'false',
description:
'Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.',
},
{
name: 'unmountChildrenWhenHidden',
type: 'boolean',
default: 'false',
description:
'Flag to enable unmounting the children after the exit animation has completed.',
},
]}
/>
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/sheet/1.59.0#portalprovider) for more information.
</Notice>
### Sheet.Overlay
Displays behind Frame. Extends [YStack](/docs/components/stacks).
### Sheet.Frame
Contains the content. Extends [YStack](/docs/components/stacks).
<PropsTable
data={[
{
name: 'disableHideBottomOverflow',
type: 'boolean',
description: `Disables Sheet cloning the Frame and positioning it below the frame, which helps to hide content that may appear underneath when spring animations bounce past 100%.`,
},
]}
/>
### Sheet.Handle
Shows a handle above the frame by default, on tap it will cycle between `snapPoints` but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
### Sheet.ScrollView
Allows scrolling within Sheet. Extends [ScrollView](/docs/components/scroll-view).
#### useSheet
Use this to control the sheet programatically.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'setOpen',
type: 'Function',
description: `Control the open state of the sheet.`,
},
{
name: 'setPosition',
type: '(index: number) => void',
description: `Control the position of the sheet.`,
},
]}
/>
## Notes
For Android you need to manually re-propagate any context when using `modal`. This is because React Native doesn't support portals yet.
## components/sheet/1.9.18
---
title: Sheet
description: A simple sheet component
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
# Sheet
<Description>A bottom sheet that slides up.</Description>
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap point points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
### Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
### API
#### <Sheet />
Contains every component for the sheet.
<PropsTable
data={[
{ name: 'open', type: 'boolean', description: `Set to use as controlled component.` },
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: 'number[]',
default: `[80, 10]`,
description: `Array of numbers, 0-100 that corresponds to % of the screen it should take up. Should go from most visible to least visible in order. Use "open" prop for fully closed.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
description: `By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
{
name: 'moveOnKeyboardChange',
type: 'boolean',
default: 'false',
description:
'Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.',
},
]}
/>
#### <Overlay />
Displays behind Frame. Extends [YStack](/docs/components/stacks).
#### <Frame />
Contains the content. Extends [YStack](/docs/components/stacks).
#### <Handle />
Shows a handle above the frame by default, on tap it will cycle between `snapPoints` but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
#### <Scrollview />
Allows scrolling within Sheet. Extends [Scrollview](/docs/components/scroll-view).
### Notes
For Android you need to manually re-propagate any context when using `modal`. This is because React Native doesn't support portals yet.
## components/sheet/2.0.0
---
title: Sheet
description: A bottom sheet that animates
name: sheet
component: Sheet
package: sheet
demoName: Sheet
---
<HeroContainer showAnimationDriverControl>
<SheetDemo />
</HeroContainer>
```tsx hero template=Sheet
```
<Highlights
features={[
`Lightweight implementation with dragging support.`,
`Multiple snap points and a handle.`,
`Automatically adjusts to screen size.`,
`Accepts animations, themes, size props and more.`,
]}
/>
Sheet is a bottom panel that slides up from the bottom of the screen, commonly used for mobile-friendly dialogs and action menus. It supports drag-to-dismiss, multiple snap points, and [automatically stacks](/ui/z-index) above other content.
## Installation
Sheet is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/sheet
```
<Notice theme="blue">
For native apps, we recommend [setting up native
portals](/docs/components/portal#native-portal-setup-recommended) to preserve React
context inside Sheet content.
</Notice>
## Anatomy
```tsx
import { Sheet } from 'tamagui' // or '@tamagui/sheet'
export default () => (
<Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Frame>{/* ...inner contents */}</Sheet.Frame>
</Sheet>
)
```
## API Reference
### Sheet
Contains every component for the sheet.
<PropsTable
data={[
{
name: 'open',
type: 'boolean',
description: `Set to use as controlled component.`,
},
{
name: 'defaultOpen',
type: 'boolean',
description: `Uncontrolled open state on mount.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
description: `Called on change open, controlled or uncontrolled.`,
},
{
name: 'position',
type: 'number',
description: `Controlled position, set to an index of snapPoints.`,
},
{
name: 'defaultPosition',
type: 'number',
description: `Uncontrolled default position on mount.`,
},
{
name: 'snapPoints',
type: 'number[]',
default: `[80, 10]`,
description: `Array of numbers, 0-100 that corresponds to % of the screen it should take up. Should go from most visible to least visible in order. Use "open" prop for fully closed.`,
},
{
name: 'onPositionChange',
type: '(position: number) => void',
description: `Called on change position, controlled or uncontrolled.`,
},
{
name: 'dismissOnOverlayPress',
type: 'boolean',
default: 'true',
description: `Controls tapping on the overlay to close, defaults to true.`,
},
{
name: 'animationConfig',
type: 'Animated.SpringAnimationConfig',
default: 'true',
description: `Customize the spring used, passed to react-native Animated.spring().`,
},
{
name: 'native',
type: 'boolean | "ios"[]',
description: `(iOS only) Render to a native sheet, must install native dependency first.`,
},
{
name: 'disableDrag',
type: 'boolean',
description: `Disables all touch events to drag the sheet.`,
},
{
name: 'modal',
type: 'boolean',
description: `Renders sheet into the root of your app instead of inline.`,
},
{
name: 'dismissOnSnapToBottom',
type: 'boolean',
description: `Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).`,
},
{
name: 'disableRemoveScroll',
type: 'boolean',
default: 'false',
description: `Disables the RemoveScroll behavior that prevents body scrolling while sheet is open. By default, RemoveScroll is enabled when the sheet is open and modal.`,
},
{
name: 'forceRemoveScrollEnabled',
type: 'boolean',
default: 'false',
deprecated: true,
description: `@deprecated Use \`disableRemoveScroll\` instead. By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).`,
},
{
name: 'portalProps',
type: 'Object',
description: `YStack props that can be passed to the Portal that sheet uses when in modal mode.`,
},
{
name: 'moveOnKeyboardChange',
type: 'boolean',
default: 'false',
description:
'Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.',
},
{
name: 'preferAdaptParentOpenState',
type: 'boolean',
default: 'false',
description: `By default Sheet will prefer the open prop over a parent component that is controlling it via Adapt. In general if you want to Adapt to a sheet, you'd leave the open prop undefined. If you'd like to have the parent override the prop you've set manually on Sheet, set this to true.`,
},
]}
/>
### Sheet.Overlay
Displays behind Frame. Extends [YStack](/docs/components/stacks).
### Sheet.Frame
Contains the content. Extends [YStack](/docs/components/stacks).
<PropsTable
data={[
{
name: 'disableHideBottomOverflow',
type: 'boolean',
description: `Disables the default cloned frame cover below the sheet. Leave it omitted or false when a spring can overshoot on open; the cover prevents page content from showing through below the sheet. If your frame uses decorative absolute layers, put the fallback background on Sheet.Frame so the cloned cover can occlude the bottom too.`,
},
]}
/>
### Sheet.Handle
Shows a handle above the frame by default. On tap, it will cycle between
`snapPoints`, but this can be overridden with `onPress`.
Extends [XStack](/docs/components/stacks).
### Sheet.ScrollView
Allows scrolling within Sheet. Extends
[ScrollView](/docs/components/scroll-view).
## Native Gesture Handler Integration
For the best gesture experience on iOS and Android, Sheet supports optional integration with `react-native-gesture-handler`. This provides:
- **Smooth scroll-to-drag handoffs** - Seamlessly transition between scrolling content and dragging the sheet
- **No gesture conflicts** - Sheet and ScrollView gestures coordinate properly
- **Native-quality feel** - Matches the behavior of system sheets
#### Setup
1. Install `react-native-gesture-handler`:
```bash
npm install react-native-gesture-handler
```
2. Add the setup import to your app entry point (before any Tamagui imports):
```tsx
// App.tsx or index.js
import '@tamagui/native/setup-gesture-handler'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>{/* Your app */}</GestureHandlerRootView>
)
}
```
That's it! Sheet will automatically detect and use the native gesture handler when available.
#### Using Sheet.ScrollView
When using scrollable content inside a Sheet, use `Sheet.ScrollView` for proper gesture coordination:
```tsx
<Sheet>
<Sheet.Handle />
<Sheet.Frame>
<Sheet.ScrollView>{/* Scrollable content */}</Sheet.ScrollView>
</Sheet.Frame>
</Sheet>
```
This ensures:
- Scrolling up at the top of content works naturally
- Dragging down when scroll is at top drags the sheet
- Direction changes mid-gesture work smoothly
#### Without Gesture Handler
If you don't set up `react-native-gesture-handler`, Sheet falls back to React Native's built-in `PanResponder`. This works well for basic use cases but has some limitations on iOS where scroll and pan gestures can occasionally conflict.
## Notes
For Android you need to manually re-propagate any context when using `modal`.
This is because React Native doesn't support portals yet.
## Native Support
We've deprecated the `native` prop in favor of using Adapt.
## components/slider/1.0.0
---
title: Slider
description: A simple slider component
name: slider
component: Slider
package: slider
demoName: Slider
---
# Slider
<Description>Drag to set values, vertically or horizontally</Description>
<HeroContainer>
<SliderDemo />
</HeroContainer>
```tsx hero template=Slider
```
<Highlights
features={[
`Sizable, themed, works controlled or uncontrolled.`,
`Multiple thumbs support.`,
`Control steps and control with your keyboard.`,
`Accessible, easy to compose and customize.`,
]}
/>
## Installation
Slider is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/slider
```
## Usage
Slider comes as multiple components that ship with default styles and are sizable. The `size` prop on `<Slider />` will automatically pass size down to all the sub-components.
```tsx
import { Slider } from 'tamagui'
export default () => (
<Slider size="$4" width={200} defaultValue={[50]} max={100} step={1}>
<Slider.Track>
<Slider.TrackActive />
</Slider.Track>
<Slider.Thumb circular index={0} />
</Slider>
)
```
You can also optionally style any component, either using inline style props or by wrapping with `styled`:
```tsx
import { Slider, styled } from 'tamagui'
const CustomSliderTrack = styled(Slider.Track, {
backgroundColor: 'red',
})
export default () => (
<Slider size="$4" width={200} defaultValue={[50]} max={100} step={1}>
<CustomSliderTrack>
<Slider.TrackActive />
</CustomSliderTrack>
<Slider.Thumb circular index={0} />
</Slider>
)
```
## API Reference
### Slider
Contains every component for the slider.
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'SizeTokens',
description: `Control size of every component.`,
},
{
name: 'name',
required: false,
type: 'string',
description: `For usage with forms.`,
},
{
name: 'value',
required: false,
type: 'number[]',
description: `Controlled value.`,
},
{
name: 'defaultValue',
required: false,
type: 'number[]',
description: `Uncontrolled starting value.`,
},
{
name: 'onValueChange',
required: false,
type: '(value: number[]): void',
description: ``,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Disable interaction.`,
},
{
name: 'orientation',
required: false,
type: `"horizontal" | "vertical"`,
default: 'horizontal',
description: `Direction of the slider.`,
},
{
name: 'dir',
required: false,
type: '"ltr" | "rtl"',
description: `Controls the side the active track appears on.`,
},
{
name: 'min',
required: false,
type: 'number',
description: `Minimum value.`,
},
{
name: 'max',
required: false,
type: 'number',
description: `Maximum value.`,
},
{
name: 'step',
required: false,
type: 'number',
description: `Minimum thumb move distance.`,
},
{
name: 'minStepsBetweenThumbs',
required: false,
type: 'number',
description: `Minimum steps between thumbs.`,
},
]}
/>
### Slider.Track
`Slider.Track` Inherits `SizableStack`, extending all the default [props](/docs/intro/props).
### Slider.TrackActive
`Slider.Track` Inherits `Stack`, extending all the default [props](/docs/intro/props).
### Slider.Thumb
`Slider.Track` Inherits `SizableStack`, extending all the default [props](/docs/intro/props), adding:
<PropsTable
data={[
{
name: 'index',
required: true,
type: 'number',
description: `Corresponds to the index of \`value\` or \`defaultValue\`. Use to correlate thumbs to each value in the array.`,
},
]}
/>
## components/slider/1.45.0
---
title: Slider
description: Drag to set values, vertically or horizontally.
name: slider
component: Slider
package: slider
demoName: Slider
---
<HeroContainer>
<SliderDemo />
</HeroContainer>
```tsx hero template=Slider
```
<Highlights
features={[
`Sizable, themed, works controlled or uncontrolled.`,
`Multiple thumbs support.`,
`Control steps and control with your keyboard.`,
`Accessible, easy to compose and customize.`,
]}
/>
## Installation
Slider is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/slider
```
## Usage
Slider comes as multiple components that ship with default styles and are sizable. The `size` prop on `<Slider />` will automatically pass size down to all the sub-components.
```tsx
import { Slider } from 'tamagui'
export default () => (
<Slider size="$4" width={200} defaultValue={[50]} max={100} step={1}>
<Slider.Track>
<Slider.TrackActive />
</Slider.Track>
<Slider.Thumb circular index={0} />
</Slider>
)
```
You can also optionally style any component, either using inline style props or by wrapping with `styled`:
```tsx
import { Slider, styled } from 'tamagui'
const CustomSliderTrack = styled(Slider.Track, {
backgroundColor: 'red',
})
export default () => (
<Slider size="$4" width={200} defaultValue={[50]} max={100} step={1}>
<CustomSliderTrack>
<Slider.TrackActive />
</CustomSliderTrack>
<Slider.Thumb circular index={0} />
</Slider>
)
```
## API Reference
### Slider
Contains every component for the slider.
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'SizeTokens',
description: `Control size of every component.`,
},
{
name: 'name',
required: false,
type: 'string',
description: `For usage with forms.`,
},
{
name: 'value',
required: false,
type: 'number[]',
description: `Controlled value.`,
},
{
name: 'defaultValue',
required: false,
type: 'number[]',
description: `Uncontrolled starting value.`,
},
{
name: 'onValueChange',
required: false,
type: '(value: number[]): void',
description: ``,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Disable interaction.`,
},
{
name: 'orientation',
required: false,
type: `"horizontal" | "vertical"`,
default: 'horizontal',
description: `Direction of the slider.`,
},
{
name: 'dir',
required: false,
type: '"ltr" | "rtl"',
description: `Controls the side the active track appears on.`,
},
{
name: 'min',
required: false,
type: 'number',
description: `Minimum value.`,
},
{
name: 'max',
required: false,
type: 'number',
description: `Maximum value.`,
},
{
name: 'step',
required: false,
type: 'number',
description: `Minimum thumb move distance.`,
},
{
name: 'minStepsBetweenThumbs',
required: false,
type: 'number',
description: `Minimum steps between thumbs.`,
},
{
name: 'onSlideStart',
required: false,
type: `(event: GestureReponderEvent, value: number, target: 'thumb' | 'track') => void`,
description: `Called on slide start.`,
},
{
name: 'onSlideMove',
required: false,
type: `(event: GestureReponderEvent, value: number) => void`,
description: `Called on slide move.`,
},
{
name: 'onSlideEnd',
required: false,
type: `(event: GestureReponderEvent, value: number) => void`,
description: `Called on slide end.`,
},
]}
/>
### Slider.Track
`Slider.Track` Inherits `SizableStack`, extending all the default [props](/docs/intro/props).
### Slider.TrackActive
`Slider.TrackActive` Inherits `Stack`, extending all the default [props](/docs/intro/props).
### Slider.Thumb
`Slider.Thumb` Inherits `SizableStack`, extending all the default [props](/docs/intro/props), adding:
<PropsTable
data={[
{
name: 'index',
required: true,
type: 'number',
description: `Corresponds to the index of \`value\` or \`defaultValue\`. Use to correlate thumbs to each value in the array.`,
},
]}
/>
## components/slider/2.0.0
---
title: Slider
description: Drag to set values, vertically or horizontally
name: slider
component: Slider
package: slider
demoName: Slider
---
<HeroContainer>
<SliderDemo />
</HeroContainer>
```tsx hero template=Slider
```
<Highlights
features={[
`Sizable, themed, works controlled or uncontrolled.`,
`Multiple thumbs support.`,
`Control steps and control with your keyboard.`,
`Accessible, easy to compose and customize.`,
]}
/>
## Installation
Slider is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/slider
```
## Usage
Slider comes as multiple components that ship with default styles and are sizable. The `size` prop on `<Slider />` will automatically pass size down to all the sub-components.
```tsx
import { Slider } from 'tamagui'
export default () => (
<Slider size="$4" width={200} defaultValue={[50]} max={100} step={1}>
<Slider.Track>
<Slider.TrackActive />
</Slider.Track>
<Slider.Thumb circular index={0} />
</Slider>
)
```
You can also optionally style any component, either using inline style props or by wrapping with `styled`:
```tsx
import { Slider, styled } from 'tamagui'
const CustomSliderTrack = styled(Slider.Track, {
backgroundColor: 'red',
})
export default () => (
<Slider size="$4" width={200} defaultValue={[50]} max={100} step={1}>
<CustomSliderTrack>
<Slider.TrackActive />
</CustomSliderTrack>
<Slider.Thumb circular index={0} />
</Slider>
)
```
## Vertical Slider on iOS
When using a vertical slider on iOS, you need to pass safe area insets to `TamaguiProvider` for proper pointer position calculation. Without this, the slider thumb may not track your finger correctly due to iOS safe area offsets.
```tsx
import { TamaguiProvider } from 'tamagui'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { config } from './tamagui.config'
function App() {
const insets = useSafeAreaInsets()
return (
<TamaguiProvider config={config} insets={insets}>
{/* Your app content */}
</TamaguiProvider>
)
}
```
Then you can use vertical sliders normally:
```tsx
import { Slider, XStack } from 'tamagui'
export default () => (
<XStack height={200} alignItems="center" gap="$8">
<Slider height={200} orientation="vertical" defaultValue={[50]} max={100} step={1}>
<Slider.Track>
<Slider.TrackActive />
</Slider.Track>
<Slider.Thumb size="$2" index={0} circular />
</Slider>
</XStack>
)
```
<Notice theme="blue">
This is only required on iOS. On Android and web, the slider works correctly without
passing insets.
</Notice>
## API Reference
### Slider
Contains every component for the slider.
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'SizeTokens',
description: `Controls the size of every component.`,
},
{
name: 'name',
required: false,
type: 'string',
description: `For usage with forms.`,
},
{
name: 'value',
required: false,
type: 'number[]',
description: `Controlled value.`,
},
{
name: 'defaultValue',
required: false,
type: 'number[]',
description: `Uncontrolled starting value.`,
},
{
name: 'onValueChange',
required: false,
type: '(value: number[]): void',
description: `Callback called when the value changes.`,
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: `Disables interaction.`,
},
{
name: 'orientation',
required: false,
type: `"horizontal" | "vertical"`,
default: 'horizontal',
description: `Direction of the slider.`,
},
{
name: 'dir',
required: false,
type: '"ltr" | "rtl"',
description: `Controls the side the active track appears on.`,
},
{
name: 'min',
required: false,
type: 'number',
description: `Minimum value.`,
},
{
name: 'max',
required: false,
type: 'number',
description: `Maximum value.`,
},
{
name: 'step',
required: false,
type: 'number',
description: `Minimum thumb move distance.`,
},
{
name: 'minStepsBetweenThumbs',
required: false,
type: 'number',
description: `Minimum steps between thumbs.`,
},
{
name: 'onSlideStart',
required: false,
type: `(event: GestureReponderEvent, value: number, target: 'thumb' | 'track') => void`,
description: `Called on slide start.`,
},
{
name: 'onSlideMove',
required: false,
type: `(event: GestureReponderEvent, value: number) => void`,
description: `Called on slide move.`,
},
{
name: 'onSlideEnd',
required: false,
type: `(event: GestureReponderEvent, value: number) => void`,
description: `Called on slide end.`,
},
]}
/>
### Slider.Track
Slider.Track inherits `SizableStack`, extending all the default [props](/docs/intro/props).
### Slider.TrackActive
Slider.TrackActive inherits `Stack`, extending all the default [props](/docs/intro/props).
### Slider.Thumb
Slider.Thumb inherits `SizableStack`, extending all the default [props](/docs/intro/props), adding:
<PropsTable
data={[
{
name: 'index',
required: true,
type: 'number',
description: `Corresponds to the index of \`value\` or \`defaultValue\`. Use to correlate thumbs to each value in the array.`,
},
]}
/>
## components/spinner/1.0.0
---
title: Spinner
description: Render a loading indicator.
name: spinner
component: Spinner
demoName: Spinner
---
<HeroContainer>
<SpinnerDemo />
</HeroContainer>
```tsx hero template=Spinner
```
<Highlights
features={[
'Custom size "small" or "large".',
'Accepts all theme colors.',
'Accepts all YStack props.',
]}
/>
## Installation
Spinner is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/spinner
```
Note that due to the fact that Spinner is an extension of React Native [ActivityIndicator](https://reactnative.dev/docs/activityindicator), and that only accepts size `small` or `large`, we are currently limited to just these sizes.
```tsx
import { Button, Spinner } from 'tamagui'
export default () => <Spinner size="large" color="$green10" />
```
## API Reference
### Spinner
Spinner extends [YStack](/docs/components/stacks), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: '"small" | "large"',
},
{
name: 'color',
required: false,
type: 'string | ColorTokens',
description: `Give the spinner a color.`,
},
]}
/>
## components/spinner/2.0.0
---
title: Spinner
description: Render a loading indicator
name: spinner
component: Spinner
package: spinner
demoName: Spinner
---
<HeroContainer>
<SpinnerDemo />
</HeroContainer>
```tsx hero template=Spinner
```
<Highlights
features={[
'Custom size "small" or "large".',
'Accepts all theme colors.',
'Accepts all YStack props.',
]}
/>
## Installation
Spinner is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/spinner
```
Note that due to the fact that Spinner is an extension of React Native [ActivityIndicator](https://reactnative.dev/docs/activityindicator), and that only accepts size `small` or `large`, we are currently limited to just these sizes.
## Usage
```tsx
import { Button, Spinner } from 'tamagui'
export default () => <Spinner size="large" color="$green10" />
```
## API Reference
### Spinner
Spinner extends [YStack](/docs/components/stacks), getting [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'size',
required: false,
type: '"small" | "large"',
description: `Size of the spinner.`,
},
{
name: 'color',
required: false,
type: 'string | ColorTokens',
description: `Gives the spinner a color.`,
},
]}
/>
## components/stacks/1.0.0
---
title: Stacks
description: An optional base for creating flex-based layouts.
name: stacks
component: Stacks
package: stacks
demoName: Stacks
---
<HeroContainer>
<StacksDemo />
</HeroContainer>
```tsx hero template=Stacks
```
<Highlights
features={[
'X, Y, and Z stacks for easy flex layouts.',
'Gap property to add space between elements.',
'Handle press, focus, and layout events easily.',
]}
/>
Tamagui UI includes optional stack views - XStack, YStack and ZStack. They extend directly off the [View](/docs/core/view-and-text) from `@tamagui/core`.
Stack props accept [every prop from react-native-web](https://necolas.github.io/react-native-web/docs/view/) View, as well as all [the style properties Tamagui supports](/docs/intro/props).
In this example you'd show three `YStack` elements spaced out.
## Installation
Stacks is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/stacks
```
```tsx
import { XStack, YStack } from 'tamagui'
export default () => (
<XStack gap="$2">
<YStack />
<YStack />
<YStack />
</XStack>
)
```
To see all the style properties supported, see the [Props](/docs/intro/props) documentation.
### Fuller example
An example using a wide variety of style properties:
```tsx
import { Text, XStack, YStack } from 'tamagui'
export default () => (
<XStack
flex={1}
flexWrap="wrap"
backgroundColor="#fff"
hoverStyle={{
backgroundColor: 'red',
}}
// media query
$gtSm={{
flexDirection: 'column',
flexWrap: 'nowrap',
}}
>
<YStack gap="$3">
<Text>Hello</Text>
<Text>World</Text>
</YStack>
</XStack>
)
```
## API Reference
### XStack, YStack, ZStack
Beyond the [Tamagui Props](/docs/intro/props), the stacks add just two variants:
<PropsTable
data={[
{
name: 'fullscreen',
required: false,
type: 'boolean',
description: (
<span>Sets position: absolute, top: 0, left: 0, right: 0, bottom: 0.</span>
),
},
{
name: 'elevation',
required: false,
type: 'number | tokens.size',
description: (
<span>
Sets a natural looking shadow that expands out and away as the size gets bigger.
</span>
),
},
]}
/>
## components/stacks/2.0.0
---
title: Stacks
description: An optional base for creating flex-based layouts
name: stacks
component: Stacks
package: stacks
demoName: Stacks
---
<HeroContainer>
<StacksDemo />
</HeroContainer>
```tsx hero template=Stacks
```
<Highlights
features={[
'X, Y, and Z stacks for easy flex layouts.',
'Gap property to add space between elements.',
'Handle press, focus, and layout events easily.',
]}
/>
Tamagui UI includes optional stack views - XStack, YStack and ZStack. They
extend directly off the [View](/docs/core/view-and-text) from `@tamagui/core`,
and so accept all [style properties](/docs/intro/props).
In this example you'd show three `YStack` elements spaced out.
## Installation
Stacks is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/stacks
```
## Usage
```tsx
import { XStack, YStack } from 'tamagui'
export default () => (
<XStack gap="$2">
<YStack />
<YStack />
<YStack />
</XStack>
)
```
To see all the style properties supported, see the [Props](/docs/intro/props)
documentation.
### Fuller Example
An example using a wide variety of style properties:
```tsx
import { Text, XStack, YStack } from 'tamagui'
export default () => (
<XStack
flex={1}
flexWrap="wrap"
backgroundColor="#fff"
hoverStyle={{
backgroundColor: 'red',
}}
// media query
$gtSm={{
flexDirection: 'column',
flexWrap: 'nowrap',
}}
>
<YStack gap="$3">
<Text>Hello</Text>
<Text>World</Text>
</YStack>
</XStack>
)
```
## API Reference
### XStack, YStack, ZStack
Beyond the [Tamagui Props](/docs/intro/props), the stacks add just two variants:
<PropsTable
data={[
{
name: 'fullscreen',
required: false,
type: 'boolean',
deprecated: true,
description: (
<span>
Deprecated: use <Code>inset: 0, position: 'absolute'</Code> instead.
</span>
),
},
{
name: 'elevation',
required: false,
type: 'number | tokens.size',
description: (
<span>
Sets a natural looking shadow that expands out and away as the size gets bigger.
</span>
),
},
]}
/>
## components/switch/1.0.0
---
title: Switch
description: A simple switch component
name: switch
component: Switch
package: switch
demoName: Switch
---
# Switch
<Description>Use in forms to toggle between two states.</Description>
<HeroContainer showAnimationDriverControl>
<SwitchDemo />
</HeroContainer>
```tsx hero template=Switch
```
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Style and animate both frame and thumb.`,
`Sizable & works controlled or uncontrolled.`,
]}
/>
### Usage
```tsx
import { Switch } from 'tamagui'
export default () => (
<Switch size="$4">
<Switch.Thumb animation="bouncy" />
</Switch>
)
```
### Switch props
Switchs extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'onCheckedChange',
type: '(checked: boolean) => void',
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
## components/switch/1.28.0
---
title: Switch
description: A simple switch component
name: switch
component: Switch
package: switch
demoName: Switch
---
# Switch
<Description>Use in forms to toggle between two states.</Description>
<HeroContainer showAnimationDriverControl>
<SwitchDemo />
</HeroContainer>
```tsx hero template=Switch
```
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Style and animate both frame and thumb.`,
`Sizable & works controlled or uncontrolled.`,
`Native prop that renders native Switch on mobile`,
]}
/>
## Installation
Switch is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/switch
```
## Usage
```tsx
import { Switch } from 'tamagui' // or '@tamagui/switch'
export default () => (
<Switch size="$4">
<Switch.Thumb animation="bouncy" />
</Switch>
)
```
## API Reference
### Switch
Switchs extend Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'onCheckedChange',
type: '(checked: boolean) => void',
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
{
name: 'native',
type: 'NativeValue<"mobile" | "ios" | "android">',
description: `Render to a native switch. (Not supported on web)`,
},
{
name: 'nativeProps',
type: 'SwitchProps (from `react-native`)',
description: `Props to pass to the native Switch;`,
},
]}
/>
### Switch.Thumb
`Switch.Thumb` extends Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
## components/switch/1.58.0
---
title: Switch
description: A simple switch component
name: switch
component: Switch
package: switch
demoName: Switch
---
# Switch
<Description>Use in forms to toggle between two states.</Description>
<HeroContainer showAnimationDriverControl>
<SwitchDemo />
</HeroContainer>
```tsx hero template=Switch
```
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Style and animate both frame and thumb.`,
`Sizable & works controlled or uncontrolled.`,
`Native prop that renders native Switch on mobile`,
]}
/>
## Installation
Switch is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/switch
```
## Usage
```tsx
import { Switch } from 'tamagui' // or '@tamagui/switch'
export default () => (
<Switch size="$4">
<Switch.Thumb animation="bouncy" />
</Switch>
)
```
## Headless with `createSwitch`
Using the `createSwitch` export, you can create a fully custom switch without using any of the default styles. This is similar to `unstyled`, but it doesn't assume the props `size` or `unstyled` exist, and it won't automatically apply the `active` theme.
You must pass `SwitchContext` as the `context` option to your Frame and Thumb styled components.
If you define a `checked` variant, it will apply those styles.
Here's an example:
```tsx
import { Stack, styled } from '@tamagui/core'
import { createSwitch } from '@tamagui/switch'
const Frame = styled(Stack, {
variants: {
checked: {
true: {
backgroundColor: 'yellow',
},
false: {
backgroundColor: 'green',
},
},
} as const,
})
const Thumb = styled(Stack, {
variants: {
checked: {
true: {
opacity: 1,
},
false: {
opacity: 0.5,
},
},
} as const,
})
export const Switch = createSwitch({
Frame,
Thumb,
})
```
## API Reference
### Switch
`Switch` extends Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'onCheckedChange',
type: '(checked: boolean) => void',
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
{
name: 'native',
type: 'NativeValue<"mobile" | "ios" | "android">',
description: `Render to a native switch. (Not supported on web)`,
},
{
name: 'nativeProps',
type: 'SwitchProps (from `react-native`)',
description: `Props to pass to the native Switch;`,
},
]}
/>
### Switch.Thumb
`Switch.Thumb` extends Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
## components/switch/1.89.0
---
title: Switch
description: A toggle between two states.
name: switch
component: Switch
package: switch
demoName: Switch
---
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Tamagui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer showAnimationDriverControl>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<SwitchDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<SwitchUnstyledDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<SwitchHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=Switch
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=SwitchUnstyled
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=SwitchHeadless
````
</Tabs.Content>
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Style and animate both frame and thumb.`,
`Sizable & works controlled or uncontrolled.`,
`Native prop that renders native Switch on mobile`,
]}
/>
## Installation
<Tabs.Content value="styled">
Switch is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/switch
````
</Tabs.Content>
<Tabs.Content value="unstyled">
Switch is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/switch
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless switch, you want to import it from the
`@tamagui/switch-headless` package. This package has no dependency on
`@tamagui/core`, but still works off the react-native APIs.
This means you can bring your own style library.
```bash
npm install @tamagui/switch-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { Switch } from 'tamagui' // or '@tamagui/switch'
export default () => (
<Switch size="$4">
<Switch.Thumb animation="bouncy" />
</Switch>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Using the `createSwitch` export, you can create an unstyled switch without using
any of the default styles. This is similar to the `unstyled` prop, but it
doesn't assume the props `size` or `unstyled` exist, and it won't automatically
apply the `active` theme.
You must pass `SwitchContext` as the `context` option to your Frame and Thumb
styled components.
If you define a `checked` variant, it will apply those styles.
```tsx template=SwitchUnstyled
```
</Tabs.Content>
<Tabs.Content value="headless">
Using the `useSwitch` API, you can make your own Switch from scratch.
```tsx template=SwitchHeadless
```
</Tabs.Content>
## API Reference
### Switch
`Switch` extends Stack views inheriting all the
[Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'onCheckedChange',
type: '(checked: boolean) => void',
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
{
name: 'native',
type: 'NativeValue<"mobile" | "ios" | "android">',
description: `Render to a native switch. (Not supported on web)`,
},
{
name: 'nativeProps',
type: 'SwitchProps (from `react-native`)',
description: `Props to pass to the native Switch;`,
},
]}
/>
### Switch.Thumb
`Switch.Thumb` extends Stack views inheriting all the
[Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
</Tabs>
{/* TODO: document createSwitch and useSwitch's API */}
## components/switch/2.0.0
---
title: Switch
description: A toggle between two states
name: switch
component: Switch
package: switch
demoName: Switch
---
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Tamagui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer showAnimationDriverControl>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<SwitchDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<SwitchUnstyledDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<SwitchHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=Switch
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=SwitchUnstyled
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=SwitchHeadless
````
</Tabs.Content>
<Highlights
features={[
`Accessible, easy to compose and customize.`,
`Style and animate both frame and thumb.`,
`Sizable & works controlled or uncontrolled.`,
`Native prop that renders native Switch on mobile`,
]}
/>
## Installation
<Tabs.Content value="styled">
Switch is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/switch
````
</Tabs.Content>
<Tabs.Content value="unstyled">
Switch is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/switch
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless switch, you want to import it from the
`@tamagui/switch-headless` package. This package has no dependency on
`@tamagui/core`, but still works off the react-native APIs.
This means you can bring your own style library.
```bash
npm install @tamagui/switch-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { Switch } from 'tamagui' // or '@tamagui/switch'
export default () => (
<Switch size="$4">
<Switch.Thumb transition="bouncy" />
</Switch>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Using the `createSwitch` export, you can create an unstyled switch without using
any of the default styles. This is similar to the `unstyled` prop, but it
doesn't assume the props `size` or `unstyled` exist, and it won't automatically
apply the `active` theme.
You must pass `SwitchContext` as the `context` option to your Frame and Thumb
styled components.
If you define a `checked` variant, it will apply those styles.
```tsx template=SwitchUnstyled
```
</Tabs.Content>
<Tabs.Content value="headless">
The `useSwitch` hook provides all the state and accessibility props needed to build a custom switch with any styling solution.
```tsx template=SwitchHeadless
```
### Basic Usage
```tsx
import { useSwitch } from '@tamagui/switch-headless'
import { useState } from 'react'
import { Pressable, View } from 'react-native'
function MySwitch({ defaultChecked, onCheckedChange, ...props }) {
const [checked, setChecked] = useState(defaultChecked || false)
const { switchProps, switchRef, bubbleInput } = useSwitch(
props,
[checked, setChecked],
null
)
return (
<>
<Pressable
ref={switchRef}
{...switchProps}
style={{
width: 50,
height: 28,
borderRadius: 14,
backgroundColor: checked ? '#22c55e' : '#d1d5db',
padding: 2,
}}
>
<View
style={{
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: 'white',
transform: [{ translateX: checked ? 22 : 0 }],
}}
/>
</Pressable>
{bubbleInput}
</>
)
}
```
</Tabs.Content>
## API Reference
### Switch
`Switch` extends Stack views inheriting all the
[Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labeled-by.`,
},
{
name: 'name',
type: 'string',
description: `Equivalent to input name.`,
},
{
name: 'value',
type: 'string',
description: `Give it a value (for use in HTML forms).`,
},
{
name: 'checked',
type: 'boolean',
description: `Control the input.`,
},
{
name: 'defaultChecked',
type: 'boolean',
description: `Uncontrolled default value.`,
},
{
name: 'required',
type: 'boolean',
description: `Sets aria-required.`,
},
{
name: 'onCheckedChange',
type: '(checked: boolean) => void',
description: `Callback called when checked state changes.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, removes all default Tamagui styling.`,
},
{
name: 'native',
type: 'NativeValue<"mobile" | "ios" | "android">',
description: `Render to a native switch. (Not supported on web)`,
},
{
name: 'nativeProps',
type: 'SwitchProps (from `react-native`)',
description: `Props to pass to the native Switch.`,
},
{
name: 'activeStyle',
type: 'ViewStyle',
description: `Styles to apply when the switch is checked/active.`,
},
{
name: 'activeTheme',
type: 'string | null',
description: `Theme to apply when the switch is checked/active.`,
},
]}
/>
### Switch.Thumb
`Switch.Thumb` extends Stack views inheriting all the
[Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, removes all default Tamagui styling.`,
},
{
name: 'activeStyle',
type: 'ViewStyle',
description: `Styles to apply to the thumb when the switch is checked/active.`,
},
]}
/>
<Tabs.Content value="headless">
### useSwitch
The `useSwitch` hook accepts three arguments:
```tsx
const { switchProps, switchRef, bubbleInput } = useSwitch(
props, // SwitchProps
state, // [checked: boolean, setChecked: (checked: boolean) => void]
ref // React.Ref
)
```
#### Props (first argument)
<PropsTable
data={[
{
name: 'labeledBy',
type: 'string',
description: `Set aria-labelledby for accessibility.`,
},
{
name: 'disabled',
type: 'boolean',
description: `Whether the switch is disabled.`,
},
{
name: 'name',
type: 'string',
description: `Form input name for the hidden input.`,
},
{
name: 'value',
type: 'string',
description: `Form input value.`,
},
{
name: 'required',
type: 'boolean',
description: `Whether the switch is required in a form.`,
},
{
name: 'onPress',
type: '(event) => void',
description: `Called when switch is pressed (composed with internal handler).`,
},
]}
/>
#### State (second argument)
A tuple of `[checked, setChecked]` where:
- `checked`: Current boolean state
- `setChecked`: React state setter function
#### Return Value
| Property | Type | Description |
| ------------- | ------------------- | -------------------------------------------------------------------------- |
| `switchProps` | `object` | Props to spread on your switch element (role, aria-checked, onPress, etc.) |
| `switchRef` | `Ref` | Composed ref to attach to your switch element |
| `bubbleInput` | `ReactNode \| null` | Hidden input for form compatibility (render as sibling, web only) |
</Tabs.Content>
</Tabs>
## components/tabs/1.125.35
---
title: Tabs
description: Use in pages to manage sub-pages.
name: tabs
component: Tabs
package: tabs
demoName: Tabs
---
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
</Tabs>
<Tabs.Content value="styled">
<HeroContainer>
<TabsDemo />
</HeroContainer>
```tsx hero template=Tabs
```
<Highlights
features={[
`Accessible, easy to compose, customize and animate`,
`Sizable & works controlled or uncontrolled`,
`Supports automatic and manual activation modes for web`,
`Full keyboard navigation`,
]}
/>
Note: Tabs have landed on v1.7 and not fully ready for runtime. Send us your feedback and we'll address it. We're marking it Beta a such as there may be hopefully minimal breaking changes as we get feedback on the API.
## Installation
Tabs is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/tabs
```
## Usage
```tsx
import { SizableText, Tabs } from 'tamagui'
export default () => (
<Tabs defaultValue="tab1" width={400}>
<Tabs.List space>
<Tabs.Tab value="tab1">
<SizableText>Tab 1</SizableText>
</Tabs.Tab>
<Tabs.Tab value="tab2">
<SizableText>Tab 2</SizableText>
</Tabs.Tab>
</Tabs.List>
<Tabs.Content value="tab1">
<H5>Tab 1</H5>
</Tabs.Content>
<Tabs.Content value="tab2">
<H5>Tab 2</H5>
</Tabs.Content>
</Tabs>
)
```
## API Reference
### Tabs
Root tabs component. Extends [Stack](/docs/components/stacks). Passing the `size` prop to this component will have effect on the descendants.
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `The value for the selected tab, if controlled`,
},
{
name: 'defaultValue',
type: 'string',
description: `The value of the tab to select by default, if uncontrolled`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `A function called when a new tab is selected`,
},
{
name: 'orientation',
type: '"horizontal" | "vertical"',
default: 'horizontal',
description: `The orientation the tabs are laid out.`,
},
{
name: 'dir',
type: '"ltr" | "rtl"',
description: `The direction of navigation between toolbar items`,
},
{
name: 'activationMode',
type: '"manual" | "automatic"',
default: 'automatic',
description: `Whether or not a tab is activated automatically or manually. Not applicable on mobile`,
},
]}
/>
### Tabs.List
Container for the trigger buttons. Supports scrolling by extending [Group](/docs/components/group). You can disable passing border radius to children by passing `disablePassBorderRadius`.
<PropsTable
data={[
{
name: 'loop',
type: 'boolean',
default: 'true',
description: `Whether or not to loop over after reaching the end or start of the items. Used mainly for managing keyboard navigation`,
},
]}
/>
### Tabs.Tab
The clickable tab button that activates its corresponding content. Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `The value for the tabs state to be changed to after activation of the tab`,
},
{
name: 'onInteraction',
type: `(type: InteractionType, layout: TabLayout | null) => void`,
description: `Used for making custom indicators when tab is interacted with. InteractionType is 'select' | 'focus' | 'hover'`,
},
{
name: 'disabled',
type: `boolean`,
description: `When true, prevents user interaction with the tab`,
},
{
name: 'disableActiveTheme',
type: `boolean`,
description: `When true, disables applying the 'active' theme when the tab is selected`,
},
{
name: 'unstyled',
type: `boolean`,
description: `When true, remove all default tamagui styling`,
},
]}
/>
### Tabs.Trigger
<Aside type="deprecated">
Use `Tabs.Tab` instead. `Tabs.Trigger` is an alias kept for backwards compatibility.
</Aside>
Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `The value for the tabs state to be changed to after activation of the trigger`,
},
{
name: 'onInteraction',
type: `(type: InteractionType, layout: TabLayout | null) => void`,
description: `Used for making custom indicators when trigger interacted with`,
},
{
name: 'unstyled',
type: `boolean`,
description: `When true, remove all default tamagui styling`,
},
]}
/>
### Tabs.Content
Where each tab's content will be shown. Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `Will show the content when the value matches the state of Tabs root`,
},
{
name: 'forceMount',
type: 'boolean',
default: 'false',
description: `Used to force mounting when more control is needed. Useful when controlling animation with Tamagui animations`,
},
]}
/>
## Examples
### Animations
Here is a demo with more advanced animations using [AnimatePresence](/docs/core/animations#animatepresence-and-exitstyle) and [Trigger](#trigger)'s `onInteraction` prop.
<HeroContainer noPad showAnimationDriverControl>
<TabsAdvancedDemo />
</HeroContainer>
```tsx hero template=TabsAdvanced
```
</Tabs.Content>
<Tabs.Content value="headless">
<HeroContainer>
<TabsHeadlessDemo />
</HeroContainer>
```tsx hero template=TabsHeadless
```
### Customization Options
When using `createTabs`, you need to provide three styled components:
- `TabsFrame`: The root container component
- `TabFrame`: The tab trigger component
- `ContentFrame`: The content container component
The created tabs component will maintain all the accessibility features and keyboard navigation while allowing you to have complete control over the visual presentation.
The headless API gives you the foundation to build completely custom tab interfaces while maintaining accessibility and interaction patterns. You can integrate it with your design system and add any custom behaviors you need.
</Tabs.Content>
## components/tabs/1.7.0
---
title: Tabs
description: Use in pages to manage sub-pages.
name: tabs
component: Tabs
package: tabs
demoName: Tabs
---
<HeroContainer>
<TabsDemo />
</HeroContainer>
```tsx hero template=Tabs
```
<Highlights
features={[
`Accessible, easy to compose, customize and animate`,
`Sizable & works controlled or uncontrolled`,
`Supports automatic and manual activation modes for web`,
`Full keyboard navigation`,
]}
/>
Note: Tabs have landed on v1.7 and not fully ready for runtime. Send us your feedback and we'll address it. We're marking it Beta a such as there may be hopefully minimal breaking changes as we get feedback on the API.
## Usage
```tsx
import { SizableText, Tabs } from 'tamagui'
export default () => (
<Tabs defaultValue="tab1" width={400}>
<Tabs.List gap="$4">
<Tabs.Tab value="tab1">
<SizableText>Tab 1</SizableText>
</Tabs.Tab>
<Tabs.Tab value="tab2">
<SizableText>Tab 2</SizableText>
</Tabs.Tab>
</Tabs.List>
<Tabs.Content value="tab1">
<H5>Tab 1</H5>
</Tabs.Content>
<Tabs.Content value="tab2">
<H5>Tab 2</H5>
</Tabs.Content>
</Tabs>
)
```
## API Reference
### Tabs
Root tabs component. Extends [Stack](/docs/components/stack). Passing the `size` prop to this component will have effect on the descendants.
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `The value for the selected tab, if controlled`,
},
{
name: 'defaultValue',
type: 'string',
description: `The value of the tab to select by default, if uncontrolled`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `A function called when a new tab is selected`,
},
{
name: 'orientation',
type: '"horizontal" | "vertical"',
default: 'horizontal',
description: `The orientation the tabs are layed out`,
},
{
name: 'dir',
type: '"ltr" | "rtl"',
description: `The direction of navigation between toolbar items`,
},
{
name: 'activationMode',
type: '"manual" | "automatic"',
default: 'automatic',
description: `Whether or not a tab is activated automatically or manually. Not applicable on mobile`,
},
]}
/>
### Tabs.List
Container for the trigger buttons. Supports scrolling by extending [Group](/docs/components/group). You can disable passing border radius to children by passing `disablePassBorderRadius`.
<PropsTable
data={[
{
name: 'loop',
type: 'boolean',
default: 'true',
description: `Whether or not to loop over after reaching the end or start of the items. Used mainly for managing keyboard navigation`,
},
]}
/>
### Tabs.Trigger
Extends [Button](/docs/components/button), adding:
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `The value for the tabs state to be changed to after activation of the trigger`,
},
{
name: 'onInteraction',
type: `(type: InteractionType, layout: TabTriggerLayout | null) => void`,
description: `Used for making custom indicators when trigger interacted with`,
},
{
name: 'unstyled',
type: `boolean`,
description: `When true, remove all default tamagui styling`,
},
]}
/>
### Tabs.Content
Where each tab's content will be shown. Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `Will show the content when the value matches the state of Tabs root`,
},
{
name: 'forceMount',
type: 'boolean',
default: 'false',
description: `Used to force mounting when more control is needed. Useful when controlling animation with Tamagui animations`,
},
]}
/>
## Examples
### Animations
Here is a demo with more advanced animations using [AnimatePresence](/docs/core/animations#animatepresence-and-exitstyle) and [Trigger](#trigger)'s `onInteraction` prop.
<HeroContainer noPad showAnimationDriverControl>
<TabsAdvancedDemo />
</HeroContainer>
```tsx hero template=TabsAdvanced
```
## components/tabs/2.0.0
---
title: Tabs
description: Use in pages to manage sub-pages
name: tabs
component: Tabs
package: tabs
demoName: Tabs
---
<YStack className="is-sticky" />
<Tabs id="type" defaultValue="styled">
<Tabs.List>
<TooltipSimple label="With Tamagui's default styles">
<Tabs.Tab value="styled">Styled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No default styles, easy to customize">
<Tabs.Tab value="unstyled">Unstyled</Tabs.Tab>
</TooltipSimple>
<TooltipSimple label="No dependency on Tamagui's core">
<Tabs.Tab value="headless" label="No styles and no dependency on Tamagui's styling">Headless</Tabs.Tab>
</TooltipSimple>
</Tabs.List>
<HeroContainer>
<Tabs.Content value="styled" justifyContent="center" alignItems="center" width="100%">
<TabsDemo />
</Tabs.Content>
<Tabs.Content value="unstyled" justifyContent="center" alignItems="center" width="100%">
<TabsHeadlessDemo />
</Tabs.Content>
<Tabs.Content value="headless" justifyContent="center" alignItems="center" width="100%">
<TabsHeadlessDemo />
</Tabs.Content>
</HeroContainer>
<Tabs.Content value="styled">
```tsx hero template=Tabs
````
</Tabs.Content>
<Tabs.Content value="unstyled">
```tsx hero template=TabsHeadless
````
</Tabs.Content>
<Tabs.Content value="headless">
```tsx hero template=TabsHeadless
````
</Tabs.Content>
<Highlights
features={[
`Accessible, easy to compose, customize and animate`,
`Sizable & works controlled or uncontrolled`,
`Supports automatic and manual activation modes`,
`Full keyboard navigation`,
]}
/>
## Installation
<Tabs.Content value="styled">
Tabs is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/tabs
````
</Tabs.Content>
<Tabs.Content value="unstyled">
Tabs is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/tabs
```
</Tabs.Content>
<Tabs.Content value="headless">
To use the headless tabs, import from the `@tamagui/tabs-headless` package. This package has no dependency on `@tamagui/core` and provides hooks for building custom tab interfaces with any styling solution.
```bash
npm install @tamagui/tabs-headless
```
</Tabs.Content>
## Usage
<Tabs.Content value="styled">
```tsx
import { SizableText, Tabs } from 'tamagui'
export default () => (
<Tabs defaultValue="tab1" width={400}>
<Tabs.List>
<Tabs.Tab value="tab1">
<SizableText>Tab 1</SizableText>
</Tabs.Tab>
<Tabs.Tab value="tab2">
<SizableText>Tab 2</SizableText>
</Tabs.Tab>
</Tabs.List>
<Tabs.Content value="tab1">
<H5>Tab 1</H5>
</Tabs.Content>
<Tabs.Content value="tab2">
<H5>Tab 2</H5>
</Tabs.Content>
</Tabs>
)
```
</Tabs.Content>
<Tabs.Content value="unstyled">
Use the `createTabs` export to create fully custom tabs that still use the Tamagui styling system. You provide your own styled frame components and get back a fully functional tabs component.
```tsx template=TabsHeadless
```
</Tabs.Content>
<Tabs.Content value="headless">
The `useTabs` hook provides all the state and accessibility props needed to build custom tabs with any styling solution.
```tsx
import { useTabs } from '@tamagui/tabs-headless'
function MyTabs() {
const { tabsProps, listProps, getTabProps, getContentProps, value } = useTabs({
defaultValue: 'tab1',
orientation: 'horizontal',
})
return (
<div {...tabsProps}>
<div {...listProps}>
<button {...getTabProps('tab1')}>Tab 1</button>
<button {...getTabProps('tab2')}>Tab 2</button>
</div>
<div {...getContentProps('tab1')}>{value === 'tab1' && <p>Content 1</p>}</div>
<div {...getContentProps('tab2')}>{value === 'tab2' && <p>Content 2</p>}</div>
</div>
)
}
```
### Component-Based API
For more complex use cases, you can use the context-based hooks:
```tsx
import { useTabs, TabsProvider, useTab, useTabContent } from '@tamagui/tabs-headless'
function Tabs({ children, ...props }) {
const { contextValue, tabsProps } = useTabs(props)
return (
<TabsProvider value={contextValue}>
<div {...tabsProps}>{children}</div>
</TabsProvider>
)
}
function Tab({ value, children }) {
const { isSelected, tabProps } = useTab({ value })
return (
<button {...tabProps} style={{ fontWeight: isSelected ? 'bold' : 'normal' }}>
{children}
</button>
)
}
function TabContent({ value, children }) {
const { shouldMount, contentProps } = useTabContent({ value })
if (!shouldMount) return null
return <div {...contentProps}>{children}</div>
}
```
</Tabs.Content>
## API Reference
### Tabs
<Tabs.Content value="styled">
Root tabs component. Extends [Stack](/docs/components/stacks). Passing the `size` prop to this component will affect the descendants.
</Tabs.Content>
<Tabs.Content value="unstyled">
When using `createTabs`, you provide three styled components:
- `TabsFrame`: The root container component
- `TabFrame`: The tab trigger component
- `ContentFrame`: The content container component
</Tabs.Content>
<Tabs.Content value="headless">
The `useTabs` hook accepts these options and returns props/helpers:
</Tabs.Content>
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `The value for the selected tab, if controlled.`,
},
{
name: 'defaultValue',
type: 'string',
description: `The value of the tab to select by default, if uncontrolled.`,
},
{
name: 'onValueChange',
type: '(value: string) => void',
description: `A function called when a new tab is selected.`,
},
{
name: 'orientation',
type: '"horizontal" | "vertical"',
default: 'horizontal',
description: `The orientation the tabs are laid out in.`,
},
{
name: 'dir',
type: '"ltr" | "rtl"',
description: `The direction of navigation between toolbar items.`,
},
{
name: 'activationMode',
type: '"manual" | "automatic"',
default: 'manual',
description: `Whether a tab is activated automatically (on focus) or manually (on click/enter).`,
},
{
name: 'size',
type: 'SizeTokens',
default: "'$true'",
description: `Sets the size of the tab triggers. Passed down via context to all Tabs.Tab children.`,
},
{
name: 'loop',
type: 'boolean',
default: 'true',
description: `Whether keyboard navigation should loop from last to first and vice versa.`,
},
]}
/>
<Tabs.Content value="headless">
### useTabs Return Value
| Property | Type | Description |
| ----------------- | ----------------------------------------------- | ------------------------------------- |
| `value` | `string` | The currently selected tab value |
| `setValue` | `(value: string) => void` | Function to change the selected tab |
| `direction` | `'ltr' \| 'rtl'` | The resolved text direction |
| `tabsProps` | `object` | Props to spread on the tabs container |
| `listProps` | `object` | Props to spread on the tab list |
| `getTabProps` | `(value: string, disabled?: boolean) => object` | Get props for a tab trigger |
| `getContentProps` | `(value: string) => object` | Get props for a tab content panel |
| `contextValue` | `TabsContextValue` | Context value for component-based API |
### useTab
Hook for individual tab triggers when using the component-based API.
```tsx
const { isSelected, tabProps } = useTab({
value: 'tab1',
disabled: false,
onPress: () => {},
onKeyDown: () => {},
onFocus: () => {},
})
```
### useTabContent
Hook for tab content panels when using the component-based API.
```tsx
const { isSelected, shouldMount, contentProps } = useTabContent({
value: 'tab1',
forceMount: false,
})
```
</Tabs.Content>
<Tabs.Content value="styled">
### Tabs.List
Container for the trigger buttons. Supports scrolling by extending [Group](/docs/components/group). You can disable passing border radius to children by passing `disablePassBorderRadius`.
<PropsTable
data={[
{
name: 'loop',
type: 'boolean',
default: 'true',
description: `Whether or not to loop over after reaching the end or start of the items. Used mainly for managing keyboard navigation.`,
},
]}
/>
<Notice>
Since Tabs.List extends Group, the same limitation applies: automatic border radius
detection only works when `Tabs.Tab` is a **direct child** of `Tabs.List`. If you wrap
tabs in custom components, see the [Group docs on nested
items](/docs/components/group#custom-components--nested-items) for workarounds.
</Notice>
### Tabs.Tab
Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `The value for the tabs state to be changed to after activation of the trigger.`,
},
{
name: 'onInteraction',
type: `(type: InteractionType, layout: TabLayout | null) => void`,
description: `Used for making custom indicators when trigger is interacted with.`,
},
{
name: 'disabled',
type: `boolean`,
description: `Whether the tab is disabled.`,
},
{
name: 'unstyled',
type: `boolean`,
description: `When true, removes all default Tamagui styling.`,
},
{
name: 'activeStyle',
type: `StyleProp`,
description: `Styles to apply when the tab is selected.`,
},
{
name: 'activeTheme',
type: `string | null`,
description: `Theme to apply when the tab is selected. Set to null for no theme change.`,
},
]}
/>
### Tabs.Content
Where each tab's content will be shown. Extends [ThemeableStack](/docs/components/stacks#themeablestack), adding:
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `Will show the content when the value matches the state of Tabs root.`,
},
{
name: 'forceMount',
type: 'boolean',
default: 'false',
description: `Used to force mounting when more control is needed. Useful when controlling animation with Tamagui animations.`,
},
]}
/>
## Examples
### Animations
Here is a demo with more advanced animations using [AnimatePresence](/docs/core/animations#animatepresence-and-exitstyle) and [Tab](#tabstab)'s `onInteraction` prop.
<HeroContainer noPad showAnimationDriverControl>
<TabsAdvancedDemo />
</HeroContainer>
```tsx hero template=TabsAdvanced
```
</Tabs.Content>
</Tabs>
## components/tamagui-image/1.0.0
---
title: Image
description: Web compatible and super light image component with Tamagui style props.
name: html
component: Image
package: image-next
demoName: WebNativeImageDemo
---
<HeroContainer noPad>
<WebNativeImageDemo />
</HeroContainer>
```tsx hero template=WebNativeImage
```
<Highlights
features={[
'Web compatible.',
'Supports SSR.',
'Works on native and web.',
'No react-native-web dependency',
'Super light',
]}
/>
## Installation
Image is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/image
```
## Usage
```tsx
export default () => <Image src="https://..." width={300} height={400} />
```
## API Reference
### Image
[Tamagui props](/docs/intro/props) +
[Web img props](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) +
[React Native Image props](https://necolas.github.io/react-native-web/docs/image/).
<Notice>
All web [img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) props are
supported on web, and all native
[Image](https://necolas.github.io/react-native-web/docs/image/) props are supported on
native. on native we are still using web img APIs, but not all web img props are
supported. like `decoding`
</Notice>
#### Some common props
<PropsTable
data={[
{
name: 'src',
required: true,
type: 'string',
description: `The image URL.`,
},
{
name: 'alt',
required: false,
type: 'string',
description: `mandatory and incredibly useful for accessibility`,
},
{
name: 'objectFit',
required: false,
type: 'CSS.ObjectFit',
description: `sets how the content of image is resized to fit its container. it's alternative to resizeMode prop`,
},
{
name: 'unstyled',
required: false,
type: 'boolean',
description: `When true will remove all default styles`,
},
{
name: 'onLoad',
required: false,
type: 'function',
description: `Callback when image is loaded`,
},
{
name: 'onError',
required: false,
type: 'function',
description: `Callback when image fails to load`,
},
]}
/>
## components/tamagui-image/2.0.0
---
title: Image
description: Web compatible and super light image component with Tamagui style props
name: tamagui-image
component: Image
package: image
demoName: WebNativeImageDemo
---
<HeroContainer noPad>
<WebNativeImageDemo />
</HeroContainer>
```tsx hero template=WebNativeImage
```
<Highlights
features={[
'Web compatible.',
'Supports SSR.',
'Works on native and web.',
'No react-native-web dependency.',
'Super light.',
]}
/>
## Installation
Image is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/image
```
## Usage
```tsx
export default () => <Image src="https://..." width={300} height={400} />
```
## API Reference
### Image
[Tamagui props](/docs/intro/props) +
[Web img props](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) +
[React Native Image props](https://necolas.github.io/react-native-web/docs/image/).
<Notice>
All web [img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) props are
supported on web, and all native
[Image](https://necolas.github.io/react-native-web/docs/image/) props are supported on
native. On native we are still using web img APIs, but not all web img props are
supported, like `decoding`.
</Notice>
#### Some common props
<PropsTable
data={[
{
name: 'src',
required: true,
type: 'string',
description: `The image URL.`,
},
{
name: 'alt',
required: false,
type: 'string',
description: `Mandatory and incredibly useful for accessibility.`,
},
{
name: 'objectFit',
required: false,
type: 'CSS.ObjectFit',
description: `Sets how the content of the image is resized to fit its container. An alternative to the \`resizeMode\` prop.`,
},
{
name: 'unstyled',
required: false,
type: 'boolean',
description: `When true, removes all default styles.`,
},
{
name: 'onLoad',
required: false,
type: 'function',
description: `Callback when the image is loaded.`,
},
{
name: 'onError',
required: false,
type: 'function',
description: `Callback when the image fails to load.`,
},
]}
/>
## components/text/1.0.0-alpha
---
title: Text
description: Text, Sized Text and Paragraph show one way to build a design system.
name: text
component: Paragraph
demoName: Text
---
# Text
<Description>
Text, Sized Text and Paragraph show one way to build a design system.
</Description>
<HeroContainer>
<TextDemo />
</HeroContainer>
```tsx hero template=Text
```
<Highlights
features={[
'Supports all react-native-web props and Tamagui styling props.',
'Media query styles, hoverStyle, pressStyle.',
'Paragraph uses themes and spread variants for a nicer default.',
]}
/>
### Usage
Text in Tamagui matches to Text in react-native-web, just with the added [Tamagui Props](/docs/intro/props).
It explicitly doesn't inherit your theme color or other font properties, as it's meant to be plain and used for extension. Below, we'll show `SizableText` which extends Text, and `Paragraph` which extends SizableText. Generally, Paragraph is the useful view as it will use theme values, while you can extend Text if you'd like to derive your own design system.
```tsx
import { Text, XStack, YStack } from 'tamagui'
export default () => (
<>
<Text
// can add theme values
color="$white"
fontFamily="$body"
// or just use direct values
fontSize={20}
hoverStyle={{
color: '$color2',
}}
>
Lorem ipsum
</Text>
</>
)
```
<Notice>
SizableText and Paragraph default to the "body" fontFamily defined in your config.
Headings all default to "heading".
</Notice>
## SizableText
[Seeing how SizableText is defined](https://github.com/tamagui/tamagui/blob/v2/code/ui/text/src/SizableText.tsx) is helpful for understanding Tamagui. They serve as a good example of how you can extend and compose components.
SizableText simply adds a single `size` property to maniplulate all of:
- fontSize
- lineHeight
- fontWeight
- letterSpacing
Based on the values set in your tokens. It uses [spread variants](/docs/core/styled#spread-variants) feature. Then Paragraph extends that and ensures it defaults to values from your theme - fontSize, lineHeight, color and fontFamily.
## Paragraph
Finally Paragraph extends `SizableText` and simply sets some default values from your theme:
```tsx
export const Paragraph = styled(SizableText, {
fontFamily: '$body',
color: '$color',
// note tamagui uses a generic "true" token that your sizes should set to be the same as the default on your scale
size: '$true',
})
```
## components/text/1.0.0
---
title: Text
description: Text primitives with themes custom to each font.
name: text
component: Paragraph
package: text
demoName: Text
---
<HeroContainer demoMultiple>
<TextDemo />
</HeroContainer>
```tsx hero template=Text
```
<Highlights
features={[
'Themes that give you control over spacing, weights, and sizes custom to each font.',
'Size prop that automatically matches all theme values.',
'Media query styles, hoverStyle, pressStyle, focusStyle.',
]}
/>
## Installation
Text is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/text
```
## Usage
```tsx
export default () => (
<>
<Text>Text</Text>
<SizableText>Sizable Text</SizableText>
<Paragraph>Paragraph</Paragraph>
</>
)
```
## Text
Text in Tamagui matches to Text in react-native-web, just with the added [Tamagui Props](/docs/intro/props).
It explicitly doesn't inherit your theme color or other font properties, as it's meant to be plain and used for extension. Below, we'll show `SizableText` which extends Text, and `Paragraph` which extends SizableText. Generally, Paragraph is the useful view as it will use theme values, while you can extend Text if you'd like to derive your own design system.
```tsx
import { Text, XStack, YStack } from 'tamagui'
export default () => (
<>
<Text
// can add theme values
color="$white"
fontFamily="$body"
// or just use direct values
fontSize={20}
hoverStyle={{
color: '$colorHover',
}}
>
Lorem ipsum
</Text>
</>
)
```
## SizableText
Tamagui lets you define font sizing, spacing, line height, letter spacing and other properties with `createFont`, of which you can have many different configurations. We've found a nice pattern is to "align" all your keys across these sub-objects.
SizableText adds a `size` property thats defined using a [spread variant](/docs/core/styled#spread-variants) which looks for a matching key on each of these properties (using `@tamagui/get-font-sized`):
- color
- fontStyle
- textTransform
- fontFamily
- fontWeight
- letterSpacing
- fontSize
- lineHeight
So, if you've defined `small`, `medium` and `large` keys on each createFont category, you can use it like so:
```tsx
<SizableText size="$small" />
```
[Source code for SizableText](https://github.com/tamagui/tamagui/blob/main/code/ui/text/src/SizableText.tsx).
## Paragraph
Paragraph extends SizableText and is defined as:
```tsx
export const Paragraph = styled(SizableText, {
name: 'Paragraph',
tag: 'p',
userSelect: 'auto',
color: '$color',
size: '$true',
whiteSpace: 'normal',
})
```
<Notice>
Note: Paragraph renders to a `p` tag on web, which can cause issues when you nest them
during SSR. If you don't mind rendering to a span, use `SizableText`, otherwise, be
careful when nesting items inside a Paragraph.
</Notice>
## components/text/2.0.0
---
title: Text
description: Text primitives with themes custom to each font
name: text
component: Paragraph
package: text
demoName: Text
---
<HeroContainer demoMultiple>
<TextDemo />
</HeroContainer>
```tsx hero template=Text
```
<Highlights
features={[
'Themes that give you control over spacing, weights, and sizes custom to each font.',
'Size prop that automatically matches all theme values.',
'Media query styles, hoverStyle, pressStyle, focusStyle.',
]}
/>
<Notice theme="green">
The base `Text` component is already included in `@tamagui/core` (and `tamagui`). This
package is optional and adds `SizableText` and `Paragraph`, which extend `Text` with the
`size` prop and theme-aware defaults.
</Notice>
## Installation
If you're using `tamagui`, `SizableText` and `Paragraph` are already included. Otherwise, install independently:
```bash
npm install @tamagui/text
```
## Usage
```tsx
export default () => (
<>
<Text>Text</Text>
<SizableText>Sizable Text</SizableText>
<Paragraph>Paragraph</Paragraph>
</>
)
```
## Text
Text in Tamagui matches Text in react-native-web, with the added [Tamagui props](/docs/intro/props).
It explicitly does not inherit your theme color or other font properties, as it is meant to be plain and used for extension. Below, we show `SizableText` which extends Text, and `Paragraph` which extends SizableText. Generally, Paragraph is the useful component as it uses theme values, while you can extend Text if you want to derive your own design system.
```tsx
import { Text, XStack, YStack } from 'tamagui'
export default () => (
<>
<Text
// can add theme values
color="$white"
fontFamily="$body"
// or just use direct values
fontSize={20}
hoverStyle={{
color: '$colorHover',
}}
>
Lorem ipsum
</Text>
</>
)
```
## SizableText
Tamagui lets you define font sizing, spacing, line height, letter spacing and other properties with `createFont`, of which you can have many different configurations. We've found a nice pattern is to "align" all your keys across these sub-objects.
SizableText adds a `size` property that is defined using a [spread variant](/docs/core/styled#spread-variants) which looks for a matching key on each of these properties (using `@tamagui/get-font-sized`):
- color
- fontStyle
- textTransform
- fontFamily
- fontWeight
- letterSpacing
- fontSize
- lineHeight
So, if you've defined `small`, `medium` and `large` keys on each createFont category, you can use it like so:
```tsx
<SizableText size="$small" />
```
[Source code for SizableText](https://github.com/tamagui/tamagui/blob/main/code/ui/text/src/SizableText.tsx).
## Paragraph
Paragraph extends SizableText and is defined as:
```tsx
export const Paragraph = styled(SizableText, {
name: 'Paragraph',
tag: 'p',
userSelect: 'auto',
color: '$color',
size: '$true',
whiteSpace: 'normal',
})
```
<Notice>
Note: Paragraph renders to a `p` tag on web, which can cause issues when you nest them
during SSR. If you don't mind rendering to a span, use `SizableText`, otherwise, be
careful when nesting items inside a Paragraph.
</Notice>
## Inline Text Elements
For semantic inline text styling, `@tamagui/text` exports `Strong`, `Em`, and `Span`:
```tsx
import { Strong, Em, Span, Paragraph } from '@tamagui/text'
export default () => (
<Paragraph>
This is <Strong>bold text</Strong>, this is <Em>italic text</Em>, and this is a{' '}
<Span color="$blue10">colored span</Span>.
</Paragraph>
)
```
These render to their corresponding HTML elements (`<strong>`, `<em>`, `<span>`) on web while remaining compatible with React Native.
## Text Truncation
Tamagui provides two props for truncating text with ellipsis: `ellipsis` for single-line truncation and `numberOfLines` for multi-line clamping.
### Single-line Ellipsis
Use the `ellipsis` prop to truncate text to a single line with an ellipsis:
```tsx
import { Text, SizableText, Paragraph } from 'tamagui'
export default () => (
<Paragraph ellipsis maxWidth={200}>
This is a very long text that will be truncated with an ellipsis when it overflows.
</Paragraph>
)
```
On web, this applies `text-overflow: ellipsis`, `white-space: nowrap`, and `overflow: hidden`. On native, it sets `numberOfLines={1}`.
### Multi-line Clamping
Use the `numberOfLines` prop to clamp text to a specific number of lines:
```tsx
import { Paragraph } from 'tamagui'
export default () => (
<>
<Paragraph numberOfLines={1} maxWidth={200}>
Single line truncation, same as ellipsis.
</Paragraph>
<Paragraph numberOfLines={3} maxWidth={200}>
This text will be clamped to three lines. Any overflow beyond the third
line will be hidden. This is useful for card descriptions or preview text
where you want to show a limited amount of content.
</Paragraph>
</>
)
```
On web, multi-line clamping uses `-webkit-line-clamp` with `-webkit-box` display. On native, it passes `numberOfLines` directly to the React Native Text component.
<Notice>
The `numberOfLines` prop is supported on the base `Text` component from `@tamagui/core`
as well as `SizableText` and `Paragraph`. The `ellipsis` prop is a shorthand for
`numberOfLines={1}`.
</Notice>
## API Reference
### Text
The base text component from `@tamagui/core`. Accepts all [Tamagui style props](/docs/intro/props) for text elements.
<PropsTable
data={[
{
name: 'ellipsis',
required: false,
type: 'boolean',
description: `When true, truncates text to a single line with an ellipsis. Equivalent to numberOfLines={1}.`,
},
{
name: 'numberOfLines',
required: false,
type: 'number',
description: `Clamps text to the specified number of lines. Overflow is hidden. On web uses -webkit-line-clamp; on native uses the React Native numberOfLines prop.`,
},
]}
/>
### SizableText
Extends Text with the `size` prop for theme-aware font sizing.
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'SizeTokens',
description: `Sets font size, line height, letter spacing, and font weight from your theme's font configuration. Accepts any size token (e.g., "$4", "$true").`,
},
{
name: 'ellipsis',
required: false,
type: 'boolean',
description: `When true, truncates text to a single line with an ellipsis.`,
},
{
name: 'numberOfLines',
required: false,
type: 'number',
description: `Clamps text to the specified number of lines.`,
},
]}
/>
### Paragraph
Extends SizableText with semantic defaults. Renders as `<p>` on web, uses theme color, and sets `userSelect: 'auto'` and `whiteSpace: 'normal'`.
<PropsTable
data={[
{
name: 'size',
required: false,
type: 'SizeTokens',
default: "'$true'",
description: `Sets font size from your theme. Defaults to the theme's base size.`,
},
{
name: 'ellipsis',
required: false,
type: 'boolean',
description: `When true, truncates text to a single line with an ellipsis.`,
},
{
name: 'numberOfLines',
required: false,
type: 'number',
description: `Clamps text to the specified number of lines.`,
},
]}
/>
## components/toast/1.11.3
---
title: Toast
description: A toast component with native features
name: toast
component: Toast
package: toast
demoName: Toast
---
# Toast <Beta />
<Description>Use to show feedback to user interactions</Description>
<HeroContainer showAnimationDriverControl>
<ToastDemo />
</HeroContainer>
```tsx hero template=Toast
```
<Highlights
features={[
`Automatically closes`,
`Pause closing on hover, focus, window blur and mobile touch`,
`Supports closing via swipe gesture`,
`Easily animatable with Tamagui's animation drivers`,
`Native toasts included for Android, iOS and web (notification API)`,
]}
/>
## Installation
Run the following command:
```
yarn add @tamagui/toast burnt
```
then, rebuild your React Native app. React Native requires sub-dependencies with native dependencies always be hoisted to your apps package.json and Toast relies on the amazing [Burnt](https://github.com/nandorojo/burnt) library by Fernando Rojo to provide its native functionality.
## Usage
To display the toast natively, you should either pass an array of native platforms (`native: ["ios", "web"]`), a single platform or `true` for all platforms.
```tsx
import { Button } from 'tamagui' // or '@tamagui/button'
import { Toast, ToastProvider, useToast } from '@tamagui/toast'
export default () => (
<ToastProvider native={['mobile']}>
<CurrentToast />
<MyPage />
<ToastViewport />
</ToastProvider>
)
const CurrentToast = () => {
const { currentToast } = useToast()
// only show the component if it's present and not handled by native toast
if (!currentToast || currentToast.isHandledNatively) return null
return (
<Toast key={currentToast.id}>
<Toast.Title>{currentToast.title}</Toast.Title>
<Toast.Description>{currentToast.message}</Toast.Description>
</Toast>
)
}
const MyPage = () => {
const { show } = useToast()
return (
<Button onPress={() => show('Done!', { message: 'Form submitted successfully.' })}>
Show Toast
</Button>
)
}
```
## API
### ToastProvider
Your toasts should be wrapped within a `ToastProvider`. This is usually done at the root of your application.
<PropsTable
data={[
{
name: 'label',
required: false,
type: 'string',
description: `An author-localized label for each toast. Used to help screen reader users associate the interruption with a toast.`,
default: 'Notification',
},
{
name: 'duration',
required: false,
type: 'number',
description: `Time in milliseconds that each toast should remain visible for. This could be overwritten at the toast level as well.`,
default: 5000,
},
{
name: 'swipeDirection',
required: false,
type: 'SwipeDirection',
description: `Direction of pointer swipe that should close the toast.`,
default: 'right',
},
{
name: 'swipeThreshold',
required: false,
type: 'number',
description: `Distance in pixels that the swipe must pass before a close is triggered.`,
default: 50,
},
{
name: 'id',
required: false,
type: 'string',
default: 'A unique generated ID',
},
{
name: 'native',
required: false,
type: 'boolean | ToastNativePlatform | ToastNativePlatform[]',
description: `Will show a native toast if is true or is set to the current platform. On iOS, it wraps \`SPIndicator\` and \`SPAlert\`. On Android, it wraps \`ToastAndroid\`. On web, it wraps Notification API. Mobile's native features are handled by \`burnt\`.`,
},
{
name: `burntOptions`,
required: false,
type: `Omit<BurntToastOptions, 'title' | 'message' | 'duration'>`,
description: `Options for the burnt package if you're using native toasts on mobile`,
},
{
name: `notificationOptions`,
required: false,
type: `NotificationOptions`,
description: `Options for the notification API if you're using native toasts on web`,
},
]}
/>
### ToastViewport
The portal for toasts to be directed to. Should be used inside [ToastProvider](#toastprovider). Beyond [Stack Props](/docs/components/stacks/1.0.0), adds:
<PropsTable
data={[
{
name: 'hotkey',
type: 'string[]',
default: "['F8']",
required: false,
description: `The keys to use as the keyboard shortcut that will move focus to the toast viewport.`,
},
{
name: 'label',
type: 'string',
default: 'Notifications ({hotkey})',
required: false,
description: `An author-localized label for the toast viewport to provide context for screen reader users when navigating page landmarks. The available \`{hotkey}\` placeholder will be replaced for you.`,
},
{
name: 'name',
type: 'string',
required: false,
description: `Used to reference the viewport if you want to have multiple viewports in the same provider.`,
},
{
name: 'multipleToasts',
type: 'boolean',
required: false,
description: `Pass this when you want to have multiple/duplicated toasts.`,
},
]}
/>
### Toast
Contains the Title, Description, Action and Close component. Should be used inside [ToastProvider](#toastprovider). Extends [Stack](/docs/components/stack#api) and adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'type',
type: "'foreground' | 'background'",
required: false,
description: `Control the sensitivity of the toast for accessibility purposes. For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.`,
},
{
name: 'duration',
type: 'number',
required: false,
description: `Time in milliseconds that toast should remain visible for. Overrides value given to \`ToastProvider\`.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `The controlled open state of the dialog. Must be used in conjunction with \`onOpenChange\`.`,
},
{
name: 'onOpenChange',
type: '(open: boolean): void',
required: false,
description: `Event handler called when the open state of the dialog changes.`,
},
{
name: 'onEscapeKeyDown',
type: "(): DismissableProps['onEscapeKeyDown']",
required: false,
description: `Event handler called when the escape key is down. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onPause',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is paused. On web, this occurs when the pointer is moved over the viewport, the viewport is focused or when the window is blurred. On mobile, this occurs when the toast is touched.`,
},
{
name: 'onResume',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is resumed. On web, this occurs when the pointer is moved away from the viewport, the viewport is blurred or when the window is focused. On mobile, this occurs when the toast is released.`,
},
{
name: 'onSwipeStart',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called when starting a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeMove',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called during a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeCancel',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the cancellation of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeEnd',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the end of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'viewportName',
type: 'string',
required: false,
description: `The viewport's name to send the toast to. Used when using multiple viewports and want to forward toasts to different ones.`,
default: 'default',
},
]}
/>
#### Toast.Title
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext).
#### Toast.Description
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext).
#### Toast.Close
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
#### Toast.Action
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
### useToast
Should be used inside [ToastProvider](#toastprovider).
<PropsTable
title="Returns"
data={[
{
name: 'currentToast',
type: 'ToastData | null',
description: `The information about the current toast to show such as title, message, duration, etc.`,
},
{
name: `show`,
type: '(title: string, showOptions?: ShowToastOptions): void',
description: `Call it to show a new toast. If you're using native toasts, you can pass native options using \`burntOptions\` or \`notificationOptions\` depending on the native platform (mobile/web).`,
},
{
name: `hide`,
type: '(): void',
description: `Call it to hide the currently displayed toast.`,
},
]}
/>
## FAQ
#### How to change the placement of toasts?
Native toasts:
- iOS (burnt): Supports top or bottom placements. Adjustable by passing `from` to `burntOptions`:
```tsx
<ToastProvider burntOptions={{ from: 'bottom' }}>
```
- Android (burnt): Not supported.
- Web (Notification API): Not supported.
Non-native toasts:
You should change the positioning of your [`<ToastViewport>`](#toastviewport). For instance, if you want them to appear from top right:
```tsx
<ToastViewport flexDirection="column-reverse" top={0} right={0} />
```
Or for bottom center:
```tsx
<ToastViewport flexDirection="column" bottom={0} left={0} right={0} />
```
<Notice theme="blue">
When using multiple toasts, you can change the order of toasts by setting
`flexDirection` to `column` or `column-reverse`. Or even have them stack horizontally
using `row` or `row-reverse`.
</Notice>
#### How to show non-native toasts within safe area on mobile?
Install `react-native-safe-area-context` if you haven't, wrap your app inside `<SafeAreaProvider>`, and use the safe area insets to position the viewport inside the safe area.
```tsx
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const SafeToastViewport = () => {
const { left, top, right } = useSafeAreaInsets()
return (
<ToastViewport flexDirection="column-reverse" top={top} left={left} right={right} />
)
}
```
#### Can I send toasts to different toast viewports?
Yes, but you will have to name them and then reference the viewport name on the `<Toast>` component. for instance:
```tsx
const App = () => {
return (
<ToastProvider>
<ToastViewport /> {/* name will be "default" */}
<ToastViewport name="viewport-custom" />
</ToastProvider>
)
}
const MyComponent = () => {
return <Toast>{/* goes to default viewport */}</Toast>
}
const MyComponent2 = () => {
return <Toast viewportName="viewport-custom" />
}
```
#### How can I have more control over toasts?
You will have to opt-out of the native toasts and only use custom ones. Here are some examples:
##### Single Toast
```tsx
export default () => {
const [open, setOpen] = React.useState(false)
const timerRef = React.useRef(0)
React.useEffect(() => {
return () => clearTimeout(timerRef.current)
}, [])
return (
<YStack ai="center">
<Button
onPress={() => {
setOpen(false)
window.clearTimeout(timerRef.current)
timerRef.current = window.setTimeout(() => {
setOpen(true)
}, 150)
}}
>
Single Toast
</Button>
<Toast
onOpenChange={setOpen}
open={open}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
</YStack>
)
}
```
##### Multiple Toasts
<Notice>
To use multiple toasts, you should pass `multipleToasts` to your `ToastViewport`.
Otherwise there'll be issues when swipe-dismissing or animating toasts.
</Notice>
```tsx
export default () => {
const [savedCount, setSavedCount] = React.useState(0)
return (
<YStack ai="center">
<Button
onPress={() => {
setSavedCount((old) => old + 1)
}}
>
Show toast
</Button>
{[...Array(savedCount)].map((_, index) => (
<Toast
key={index}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
))}
</YStack>
)
}
```
## components/toast/1.13.0
---
title: Toast
description: A toast component with native features
name: toast
component: Toast
package: toast
demoName: Toast
---
# Toast <Beta />
<Description>Use to show feedback to user interactions</Description>
<HeroContainer showAnimationDriverControl>
<ToastDemo />
</HeroContainer>
```tsx hero template=Toast
```
<Highlights
features={[
`Automatically closes`,
`Pause closing on hover, focus, window blur and mobile touch`,
`Supports closing via swipe gesture`,
`Easily animatable with Tamagui's animation drivers`,
`Native toasts included for Android, iOS and web (notification API)`,
]}
/>
## Installation
Run the following command:
```
yarn add @tamagui/toast burnt
```
then, if targetin native, rebuild your React Native app. React Native requires sub-dependencies with native dependencies always be hoisted to your apps package.json and Toast relies on the amazing [Burnt](https://github.com/nandorojo/burnt) library by Fernando Rojo to provide its native functionality.
## Usage
To display the toast natively, you should either pass an array of native platforms (`native: ["ios", "web"]`), a single platform or `true` for all platforms.
```tsx
import { Toast, ToastProvider, useToastController, useToastState } from '@tamagui/toast'
import { Button } from 'tamagui' // or '@tamagui/button'
export default () => (
<ToastProvider native={['mobile']}>
<CurrentToast />
<MyPage />
<ToastViewport />
</ToastProvider>
)
const CurrentToast = () => {
const toast = useToastState()
// only show the component if it's present and not handled by native toast
if (!toast || toast.isHandledNatively) {
return null
}
return (
<Toast key={toast.id}>
<Toast.Title>{toast.title}</Toast.Title>
<Toast.Description>{toast.message}</Toast.Description>
</Toast>
)
}
const MyPage = () => {
const { show } = useToastController()
return (
<Button onPress={() => show('Done!', { message: 'Form submitted successfully.' })}>
Show Toast
</Button>
)
}
```
## API
### ToastProvider
Your toasts should be wrapped within a `ToastProvider`. This is usually done at the root of your application.
<PropsTable
data={[
{
name: 'label',
required: false,
type: 'string',
description: `An author-localized label for each toast. Used to help screen reader users associate the interruption with a toast.`,
default: 'Notification',
},
{
name: 'duration',
required: false,
type: 'number',
description: `Time in milliseconds that each toast should remain visible for. This could be overwritten at the toast level as well.`,
default: 5000,
},
{
name: 'swipeDirection',
required: false,
type: 'SwipeDirection',
description: `Direction of pointer swipe that should close the toast.`,
default: 'right',
},
{
name: 'swipeThreshold',
required: false,
type: 'number',
description: `Distance in pixels that the swipe must pass before a close is triggered.`,
default: 50,
},
{
name: 'id',
required: false,
type: 'string',
default: 'A unique generated ID',
},
{
name: 'native',
required: false,
type: 'boolean | ToastNativePlatform | ToastNativePlatform[]',
description: `Will show a native toast if is true or is set to the current platform. On iOS, it wraps \`SPIndicator\` and \`SPAlert\`. On Android, it wraps \`ToastAndroid\`. On web, it wraps Notification API. Mobile's native features are handled by \`burnt\`.`,
},
{
name: `burntOptions`,
required: false,
type: `Omit<BurntToastOptions, 'title' | 'message' | 'duration'>`,
description: `Options for the burnt package if you're using native toasts on mobile`,
},
{
name: `notificationOptions`,
required: false,
type: `NotificationOptions`,
description: `Options for the notification API if you're using native toasts on web`,
},
]}
/>
### ToastViewport
The portal for toasts to be directed to. Should be used inside [ToastProvider](#toastprovider). Beyond [Stack Props](/docs/components/stacks/1.0.0), adds:
<PropsTable
data={[
{
name: 'hotkey',
type: 'string[]',
default: "['F8']",
required: false,
description: `The keys to use as the keyboard shortcut that will move focus to the toast viewport.`,
},
{
name: 'label',
type: 'string',
default: 'Notifications ({hotkey})',
required: false,
description: `An author-localized label for the toast viewport to provide context for screen reader users when navigating page landmarks. The available \`{hotkey}\` placeholder will be replaced for you.`,
},
{
name: 'name',
type: 'string',
required: false,
description: `Used to reference the viewport if you want to have multiple viewports in the same provider.`,
},
{
name: 'multipleToasts',
type: 'boolean',
required: false,
description: `Pass this when you want to have multiple/duplicated toasts.`,
},
]}
/>
### Toast
Contains the Title, Description, Action and Close component. Should be used inside [ToastProvider](#toastprovider). Extends [Stack](/docs/components/stack#api) and adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'type',
type: "'foreground' | 'background'",
required: false,
description: `Control the sensitivity of the toast for accessibility purposes. For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.`,
},
{
name: 'duration',
type: 'number',
required: false,
description: `Time in milliseconds that toast should remain visible for. Overrides value given to \`ToastProvider\`.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `The controlled open state of the dialog. Must be used in conjunction with \`onOpenChange\`.`,
},
{
name: 'onOpenChange',
type: '(open: boolean): void',
required: false,
description: `Event handler called when the open state of the dialog changes.`,
},
{
name: 'onEscapeKeyDown',
type: "(): DismissableProps['onEscapeKeyDown']",
required: false,
description: `Event handler called when the escape key is down. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onPause',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is paused. On web, this occurs when the pointer is moved over the viewport, the viewport is focused or when the window is blurred. On mobile, this occurs when the toast is touched.`,
},
{
name: 'onResume',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is resumed. On web, this occurs when the pointer is moved away from the viewport, the viewport is blurred or when the window is focused. On mobile, this occurs when the toast is released.`,
},
{
name: 'onSwipeStart',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called when starting a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeMove',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called during a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeCancel',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the cancellation of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeEnd',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the end of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'viewportName',
type: 'string',
required: false,
description: `The viewport's name to send the toast to. Used when using multiple viewports and want to forward toasts to different ones.`,
default: 'default',
},
]}
/>
#### Toast.Title
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext).
#### Toast.Description
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext).
#### Toast.Close
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
#### Toast.Action
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
### useToastController
Used to control the display of toasts. Should be used inside [ToastProvider](#toastprovider).
<PropsTable
title="Returns"
data={[
{
name: `show`,
type: '(title: string, showOptions?: ShowToastOptions): void',
description: `Call it to show a new toast. If you're using native toasts, you can pass native options using \`burntOptions\` or \`notificationOptions\` depending on the native platform (mobile/web).`,
},
{
name: `hide`,
type: '(): void',
description: `Call it to hide the currently displayed toast.`,
},
]}
/>
### useToastState
Used to render out your toast contents. Should be used inside [ToastProvider](#toastprovider).
<PropsTable
title="Hook for rendering toast"
data={[
{
name: `title`,
type: 'string',
},
{
name: `id`,
type: 'string',
},
{
name: `message`,
type: 'string',
},
{
name: `duration`,
type: 'number',
},
]}
/>
## FAQ
#### How to change the placement of toasts?
Native toasts:
- iOS (burnt): Supports top or bottom placements. Adjustable by passing `from` to `burntOptions`:
```tsx
<ToastProvider burntOptions={{ from: 'bottom' }}>
```
- Android (burnt): Not supported.
- Web (Notification API): Not supported.
Non-native toasts:
You should change the positioning of your [`<ToastViewport>`](#toastviewport). For instance, if you want them to appear from top right:
```tsx
<ToastViewport flexDirection="column-reverse" top={0} right={0} />
```
Or for bottom center:
```tsx
<ToastViewport flexDirection="column" bottom={0} left={0} right={0} />
```
<Notice theme="blue">
When using multiple toasts, you can change the order of toasts by setting
`flexDirection` to `column` or `column-reverse`. Or even have them stack horizontally
using `row` or `row-reverse`.
</Notice>
#### How to show non-native toasts within safe area on mobile?
Install `react-native-safe-area-context` if you haven't, wrap your app inside `<SafeAreaProvider>`, and use the safe area insets to position the viewport inside the safe area.
```tsx
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const SafeToastViewport = () => {
const { left, top, right } = useSafeAreaInsets()
return (
<ToastViewport flexDirection="column-reverse" top={top} left={left} right={right} />
)
}
```
#### Can I send toasts to different toast viewports?
Yes, but you will have to name them and then reference the viewport name on the `<Toast>` component. for instance:
```tsx
const App = () => {
return (
<ToastProvider>
<ToastViewport /> {/* name will be "default" */}
<ToastViewport name="viewport-custom" />
</ToastProvider>
)
}
const MyComponent = () => {
return <Toast>{/* goes to default viewport */}</Toast>
}
const MyComponent2 = () => {
return <Toast viewportName="viewport-custom" />
}
```
#### How can I have more control over toasts?
You will have to opt-out of the native toasts and only use custom ones. Here are some examples:
##### Single Toast
```tsx
export default () => {
const [open, setOpen] = React.useState(false)
const timerRef = React.useRef(0)
React.useEffect(() => {
return () => clearTimeout(timerRef.current)
}, [])
return (
<YStack ai="center">
<Button
onPress={() => {
setOpen(false)
window.clearTimeout(timerRef.current)
timerRef.current = window.setTimeout(() => {
setOpen(true)
}, 150)
}}
>
Single Toast
</Button>
<Toast
onOpenChange={setOpen}
open={open}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
</YStack>
)
}
```
##### Multiple Toasts
<Notice>
To use multiple toasts, you should pass `multipleToasts` to your `ToastViewport`.
Otherwise there'll be issues when swipe-dismissing or animating toasts.
</Notice>
```tsx
export default () => {
const [savedCount, setSavedCount] = React.useState(0)
return (
<YStack ai="center">
<Button
onPress={() => {
setSavedCount((old) => old + 1)
}}
>
Show toast
</Button>
{[...Array(savedCount)].map((_, index) => (
<Toast
key={index}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
))}
</YStack>
)
}
```
## components/toast/1.15.15
---
title: Toast
description: A toast component with native features
name: toast
component: Toast
package: toast
demoName: Toast
---
# Toast <Beta />
<Description>Use to show feedback to user interactions</Description>
<HeroContainer showAnimationDriverControl>
<ToastDemo />
</HeroContainer>
```tsx hero template=Toast
```
<Highlights
features={[
`Automatically closes`,
`Pause closing on hover, focus, window blur and mobile touch`,
`Supports closing via swipe gesture`,
`Easily animatable with Tamagui's animation drivers`,
`Native toasts included for Android, iOS and web (notification API)`,
]}
/>
## Installation
Run the following command:
```bash
yarn add @tamagui/toast burnt
```
Then, if targeting native, rebuild your React Native app. React Native requires sub-dependencies with native dependencies always be hoisted to your apps package.json and Toast relies on the amazing [Burnt](https://github.com/nandorojo/burnt) library by Fernando Rojo to provide its native functionality.
## Anatomy
```tsx
<ToastProvider>
<Toast>
<Toast.Title />
<Toast.Description />
<Toast.Action />
<Toast.Close />
</Toast>
<ToastViewport />
</ToastProvider>
```
## API Reference
### ToastProvider
Your toasts should be wrapped within a `ToastProvider`. This is usually done at the root of your application.
<PropsTable
data={[
{
name: 'label',
required: false,
type: 'string',
description: `An author-localized label for each toast. Used to help screen reader users associate the interruption with a toast.`,
default: 'Notification',
},
{
name: 'duration',
required: false,
type: 'number',
description: `Time in milliseconds that each toast should remain visible for. This could be overwritten at the toast level as well.`,
default: 5000,
},
{
name: 'swipeDirection',
required: false,
type: 'SwipeDirection',
description: `Direction of pointer swipe that should close the toast.`,
default: 'right',
},
{
name: 'swipeThreshold',
required: false,
type: 'number',
description: `Distance in pixels that the swipe must pass before a close is triggered.`,
default: 50,
},
{
name: 'id',
required: false,
type: 'string',
default: 'A unique generated ID',
},
{
name: 'native',
required: false,
type: 'boolean | ToastNativePlatform | ToastNativePlatform[]',
description: `Will show a native toast if is true or is set to the current platform. On iOS, it wraps \`SPIndicator\` and \`SPAlert\`. On Android, it wraps \`ToastAndroid\`. On web, it wraps Notification API. Mobile's native features are handled by \`burnt\`.`,
},
{
name: `burntOptions`,
required: false,
type: `Omit<BurntToastOptions, 'title' | 'message' | 'duration'>`,
description: `Options for the burnt package if you're using native toasts on mobile`,
},
{
name: `notificationOptions`,
required: false,
type: `NotificationOptions`,
description: `Options for the notification API if you're using native toasts on web`,
},
]}
/>
### ToastViewport
The portal for toasts to be directed to. Should be used inside [ToastProvider](#toastprovider). Beyond [Stack Props](/docs/components/stacks/1.0.0), adds:
<PropsTable
data={[
{
name: 'hotkey',
type: 'string[]',
default: "['F8']",
required: false,
description: `The keys to use as the keyboard shortcut that will move focus to the toast viewport.`,
},
{
name: 'label',
type: 'string',
default: 'Notifications ({hotkey})',
required: false,
description: `An author-localized label for the toast viewport to provide context for screen reader users when navigating page landmarks. The available \`{hotkey}\` placeholder will be replaced for you.`,
},
{
name: 'name',
type: 'string',
required: false,
description: `Used to reference the viewport if you want to have multiple viewports in the same provider.`,
},
{
name: 'multipleToasts',
type: 'boolean',
required: false,
description: `Pass this when you want to have multiple/duplicated toasts.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast
Contains the Title, Description, Action and Close component. Should be used inside [ToastProvider](#toastprovider). Extends [Stack](/docs/components/stack#api) and adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'type',
type: "'foreground' | 'background'",
required: false,
description: `Control the sensitivity of the toast for accessibility purposes. For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.`,
},
{
name: 'duration',
type: 'number',
required: false,
description: `Time in milliseconds that toast should remain visible for. Overrides value given to \`ToastProvider\`.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `The controlled open state of the dialog. Must be used in conjunction with \`onOpenChange\`.`,
},
{
name: 'onOpenChange',
type: '(open: boolean): void',
required: false,
description: `Event handler called when the open state of the dialog changes.`,
},
{
name: 'onEscapeKeyDown',
type: "(): DismissableProps['onEscapeKeyDown']",
required: false,
description: `Event handler called when the escape key is down. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onPause',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is paused. On web, this occurs when the pointer is moved over the viewport, the viewport is focused or when the window is blurred. On mobile, this occurs when the toast is touched.`,
},
{
name: 'onResume',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is resumed. On web, this occurs when the pointer is moved away from the viewport, the viewport is blurred or when the window is focused. On mobile, this occurs when the toast is released.`,
},
{
name: 'onSwipeStart',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called when starting a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeMove',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called during a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeCancel',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the cancellation of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeEnd',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the end of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'viewportName',
type: 'string',
required: false,
description: `The viewport's name to send the toast to. Used when using multiple viewports and want to forward toasts to different ones.`,
default: 'default',
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Title
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext), adding:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Description
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext), adding:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Close
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
### Toast.Action
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
### useToastController
Used to control the display of toasts. Should be used inside [ToastProvider](#toastprovider).
<PropsTable
title="Returns"
data={[
{
name: `show`,
type: '(title: string, showOptions?: ShowToastOptions): void',
description: `Call it to show a new toast. If you're using native toasts, you can pass native options using \`burntOptions\` or \`notificationOptions\` depending on the native platform (mobile/web).`,
},
{
name: `hide`,
type: '(): void',
description: `Call it to hide the currently displayed toast.`,
},
{
name: `options`,
type: `ToastOptions`,
description: `You can use if need to access the toast options.`,
},
]}
/>
### useToastState
Used to render out your toast contents. Should be used inside [ToastProvider](#toastprovider). Doesn't take in anything and returns `ToastData`.
```tsx
const CurrentToast = () => {
const toast = useToastState()
// don't show any toast if no toast is present or it's handled natively
if (!toast || toast.isHandledNatively) {
return null
}
return (
<Toast key={toast.id} duration={toast.duration} viewport={toast.viewport}>
<Toast.Title>{toast.title}</Toast.Title>
<Toast.Description>{toast.message}</Toast.Description>
</Toast>
)
}
```
## Examples
### Position the viewport
To position the viewport on native toasts:
- iOS (burnt): Supports top or bottom placements. Adjustable by passing `from` to `burntOptions`:
```tsx
<ToastProvider burntOptions={{ from: 'bottom' }}>
```
- Android (burnt): Not supported.
- Web (Notification API): Not supported.
To position the viewport on custom toasts:
You should change the positioning of your [`<ToastViewport>`](#toastviewport). For instance, if you want them to appear from top right:
```tsx
<ToastViewport flexDirection="column-reverse" top={0} right={0} />
```
Or for bottom center:
```tsx
<ToastViewport flexDirection="column" bottom={0} left={0} right={0} />
```
<Notice theme="blue">
When using multiple toasts, you can change the order of toasts by setting
`flexDirection` to `column` or `column-reverse`. Or even have them stack horizontally
using `row` or `row-reverse`.
</Notice>
### Mobile safe area
To show toasts inside device's safe area, install `react-native-safe-area-context` if you haven't, wrap your app inside `<SafeAreaProvider>`, and then use the safe area insets to position the viewport inside the safe area.
```tsx
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const SafeToastViewport = () => {
const { left, top, right } = useSafeAreaInsets()
return (
<ToastViewport flexDirection="column-reverse" top={top} left={left} right={right} />
)
}
```
### Different viewports
To send toasts to different viewports, you can set up different viewports:
```tsx
const App = () => {
return (
<ToastProvider>
<ToastViewport /> {/* default viewport */}
<ToastViewport name="viewport-custom" />
</ToastProvider>
)
}
```
And then, use the viewport's name on the toasts.
```tsx
const MyComponent = () => {
return <Toast /> // default viewport
}
const MyComponent2 = () => {
return <Toast viewportName="viewport-custom" />
}
```
### Custom data
Just pass your custom data to the second parameter of the `show()` method.
```ts
const toastController = useToastController()
toastController.show('Title', { myPreset: 'error' }) // or toastController.show("Title", { customData: { myPreset: 'error' } })
```
then, when showing the toast, you can retrieve them like so:
```ts
const toastState = useToastState()
toastState.myPreset // or toastState.customData.myPreset
```
To add TypeScript auto-completion for your custom fields, you can use TS module augmentation:
```ts
declare module '@tamagui/toast' {
interface CustomData {
myPreset: 'error' | 'success' | 'warning'
}
}
```
### Without hooks
You can also use toasts without the hooks.
<Notice>You can't use native toasts this way.</Notice>
#### Single Toast
```tsx
export default () => {
const [open, setOpen] = React.useState(false)
const timerRef = React.useRef(0)
React.useEffect(() => {
return () => clearTimeout(timerRef.current)
}, [])
return (
<YStack ai="center">
<Button
onPress={() => {
setOpen(false)
window.clearTimeout(timerRef.current)
timerRef.current = window.setTimeout(() => {
setOpen(true)
}, 150)
}}
>
Single Toast
</Button>
<Toast
onOpenChange={setOpen}
open={open}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
</YStack>
)
}
```
#### Multiple Toasts
<Notice>
To use multiple toasts, you should pass `multipleToasts` to your `ToastViewport`.
Otherwise there'll be issues when swipe-dismissing or animating toasts.
</Notice>
```tsx
export default () => {
const [savedCount, setSavedCount] = React.useState(0)
return (
<YStack ai="center">
<Button
onPress={() => {
setSavedCount((old) => old + 1)
}}
>
Show toast
</Button>
{[...Array(savedCount)].map((_, index) => (
<Toast
key={index}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
))}
</YStack>
)
}
```
## components/toast/1.8.0
---
title: Toast
description: A toast component with native features
name: toast
component: Toast
package: toast
demoName: Toast
---
# Toast <Beta />
<Description>Use to show feedback to user interactions</Description>
<HeroContainer showAnimationDriverControl>
<ToastDemo />
</HeroContainer>
```tsx hero template=Toast
```
<Highlights
features={[
`Automatically closes`,
`Pause closing on hover, focus, window blur and mobile touch`,
`Supports closing via swipe gesture`,
`Easily animatable with Tamagui's animation drivers`,
`Native toasts included for Android, iOS and web`,
]}
/>
## Install
For native support, run `yarn add burnt` to add `burnt`, then rebuild your React Native app. React Native requires sub-dependencies with native dependencies always be hoisted to your apps package.json and Toast relies on the amazing [Burnt](https://github.com/nandorojo/burnt) library by Fernando Rojo to provide its native functionality.
## Anatomy
```tsx
import { Toast, ToastProvider, ToastViewport } from 'tamagui' // or '@tamagui/toast'
export default () => (
<ToastProvider>
<Toast>
<Toast.Title />
<Toast.Description />
<Toast.Action />
<Toast.Close />
</Toast>
<ToastViewport />
</ToastProvider>
)
```
## Usage
### Single Toast
```tsx
export default () => {
const [open, setOpen] = React.useState(false)
const timerRef = React.useRef(0)
React.useEffect(() => {
return () => clearTimeout(timerRef.current)
}, [])
return (
<YStack ai="center">
<Button
onPress={() => {
setOpen(false)
window.clearTimeout(timerRef.current)
timerRef.current = window.setTimeout(() => {
setOpen(true)
}, 150)
}}
>
Single Toast
</Button>
<Toast
onOpenChange={setOpen}
open={open}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
</YStack>
)
}
```
### Duplicate Toasts
```tsx
export default () => {
const [savedCount, setSavedCount] = React.useState(0)
return (
<YStack ai="center">
<Button
onPress={() => {
setSavedCount((old) => old + 1)
}}
>
Show toast
</Button>
{[...Array(savedCount)].map((_, index) => (
<Toast
key={index}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
))}
</YStack>
)
}
```
### Using `createToast`
```tsx
import { Button } from 'tamagui' // or '@tamagui/button'
import { Toast, ToastProvider, createToast } from 'tamagui' // or '@tamagui/toast'
export const { ImperativeToastProvider, useToast } = createToast()
export default () => (
<ToastProvider>
<ImperativeToastProvider>
<CurrentToast />
<MyPage />
</ImperativeToastProvider>
<ToastViewport />
</ToastProvider>
)
const CurrentToast = () => {
const { currentToast } = useToast()
if (!currentToast) return
return (
<Toast key={currentToast.id}>
<Toast.Title>{currentToast.title}</Toast.Title>
<Toast.Description>{currentToast.message}</Toast.Description>
</Toast>
)
}
const MyPage = () => {
const { show } = useToast()
return (
<Button onPress={() => show('Done!', { message: 'Form submitted successfully.' })}>
Show Toast
</Button>
)
}
```
## API
### ToastProvider
Your toasts should be wrapped within a `ToastProvider`. This is usually done at the root of your application.
<PropsTable
data={[
{
name: 'label',
required: false,
type: 'string',
description: `An author-localized label for each toast. Used to help screen reader users associate the interruption with a toast.`,
default: 'Notification',
},
{
name: 'duration',
required: false,
type: 'number',
description: `Time in milliseconds that each toast should remain visible for. This could be overwritten at the toast level as well.`,
default: 5000,
},
{
name: 'swipeDirection',
required: false,
type: 'SwipeDirection',
description: `Direction of pointer swipe that should close the toast.`,
default: 'right',
},
{
name: 'swipeThreshold',
required: false,
type: 'number',
description: `Distance in pixels that the swipe must pass before a close is triggered.`,
default: 50,
},
{
name: 'id',
required: false,
type: 'string',
default: 'A unique generated ID',
},
]}
/>
### ToastViewport
The portal for toasts to be directed to. Should be inside [ToastProvider](#toastprovider). Beyond [Stack Props](/docs/components/stacks/1.0.0), adds:
<PropsTable
data={[
{
name: 'hotkey',
type: 'string[]',
default: "['F8']",
required: false,
description: `The keys to use as the keyboard shortcut that will move focus to the toast viewport.`,
},
{
name: 'label',
type: 'string',
default: 'Notifications ({hotkey})',
required: false,
description: `An author-localized label for the toast viewport to provide context for screen reader users when navigating page landmarks. The available \`{hotkey}\` placeholder will be replaced for you.`,
},
{
name: 'name',
type: 'string',
required: false,
description: `Used to reference the viewport if you want to have multiple viewports in the same provider.`,
},
]}
/>
### Toast
Contains the Title, Description, Action and Close component. Should be inside [ToastProvider](#toastprovider). Extends [Stack](/docs/components/stack#api) and adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'type',
type: "'foreground' | 'background'",
required: false,
description: `Control the sensitivity of the toast for accessibility purposes. For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.`,
},
{
name: 'duration',
type: 'number',
required: false,
description: `Time in milliseconds that toast should remain visible for. Overrides value given to \`ToastProvider\`.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `The controlled open state of the dialog. Must be used in conjunction with \`onOpenChange\`.`,
},
{
name: 'onOpenChange',
type: '(open: boolean): void',
required: false,
description: `Event handler called when the open state of the dialog changes.`,
},
{
name: 'onEscapeKeyDown',
type: "(): DismissableProps['onEscapeKeyDown']",
required: false,
description: `Event handler called when the escape key is down. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onPause',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is paused. On web, this occurs when the pointer is moved over the viewport, the viewport is focused or when the window is blurred. On mobile, this occurs when the toast is touched.`,
},
{
name: 'onResume',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is resumed. On web, this occurs when the pointer is moved away from the viewport, the viewport is blurred or when the window is focused. On mobile, this occurs when the toast is released.`,
},
{
name: 'onSwipeStart',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called when starting a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeMove',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called during a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeCancel',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the cancellation of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeEnd',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the end of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'viewportName',
type: 'string',
required: false,
description: `The viewport's name to send the toast to. Used when using multiple viewports and want to forward toasts to different ones.`,
default: 'default',
},
]}
/>
#### Toast.Title
Should be inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext).
#### Toast.Description
Should be inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext).
#### Toast.Close
Should be inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
#### Toast.Action
Should be inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
### createToast
An alternative way to work with toasts.
<PropsTable
title="Arguments"
data={[
{
name: 'native',
type: "boolean | 'web' | 'mobile'",
description: `Will show a native toast if is true or is set to the current platform. On iOS, it wraps \`SPIndicator\` and \`SPAlert\`. On Android, it wraps \`ToastAndroid\`. On web, it wraps Notification API. Mobile's native features are handled by \`burnt\`.`,
default: 'false',
},
]}
/>
This function, then returns the following:
#### ImperativeToastProvider
Wrap children within this provider so that they can use `useToast()`. Takes no params.
#### useToast
You may export this hook to use it throughout your app.
<PropsTable
title="Returns"
data={[
{
name: 'currentToast',
type: 'ToastData | null',
description: `The information about the current toast to showm such as title, message, duration, etc.`,
},
{
name: `show`,
type: '(title: string, showOptions?: ToastOptions): void',
description: 'Call it to show a new toast.',
},
]}
/>
## components/toast/1.83.0
---
title: Toast
description: Use to show feedback to user interactions.
name: toast
component: Toast
package: toast
demoName: Toast
---
<HeroContainer showAnimationDriverControl>
<ToastDemo />
</HeroContainer>
```tsx hero template=Toast
```
<Highlights
features={[
`Automatically closes`,
`Pause closing on hover, focus, window blur and mobile touch`,
`Supports closing via swipe gesture`,
`Easily animatable with Tamagui's animation drivers`,
`Native toasts included for Android, iOS and web (notification API)`,
]}
/>
## Installation
Note that `@tamagui/toast` is the only UI package not included by default in `tamagui`. The reason for this is that Metro would force you to install the native `burnt` dependency, and that would mean `tamagui` wouldn't work with Expo Go and would require a native install step no matter if you used Toast or not. This is due to a limitation in Metro.
To install toast:
```bash
yarn add @tamagui/toast burnt
```
Then, if targeting native, rebuild your React Native app. React Native requires sub-dependencies with native dependencies always be hoisted to your apps package.json and Toast relies on the amazing [Burnt](https://github.com/nandorojo/burnt) library by Fernando Rojo to provide its native functionality.
We're open to a refactor that moves the burnt dependency into a sub-path like `@tamagui/toast/burnt` and forces setup through there, that likely fixes the Metro issue and allow us to include it by default.
## Anatomy
```tsx
<ToastProvider>
<Toast>
<Toast.Title />
<Toast.Description />
<Toast.Action />
<Toast.Close />
</Toast>
<ToastViewport />
</ToastProvider>
```
## API Reference
### ToastProvider
Your toasts should be wrapped within a `ToastProvider`. This is usually done at the root of your application.
<PropsTable
data={[
{
name: 'label',
required: false,
type: 'string',
description: `An author-localized label for each toast. Used to help screen reader users associate the interruption with a toast.`,
default: 'Notification',
},
{
name: 'duration',
required: false,
type: 'number',
description: `Time in milliseconds that each toast should remain visible for. This could be overwritten at the toast level as well.`,
default: 5000,
},
{
name: 'swipeDirection',
required: false,
type: 'SwipeDirection',
description: `Direction of pointer swipe that should close the toast.`,
default: 'right',
},
{
name: 'swipeThreshold',
required: false,
type: 'number',
description: `Distance in pixels that the swipe must pass before a close is triggered.`,
default: 50,
},
{
name: 'id',
required: false,
type: 'string',
default: 'A unique generated ID',
},
{
name: 'native',
required: false,
type: 'boolean | ToastNativePlatform | ToastNativePlatform[]',
description: `Will show a native toast if is true or is set to the current platform. On iOS, it wraps \`SPIndicator\` and \`SPAlert\`. On Android, it wraps \`ToastAndroid\`. On web, it wraps Notification API. Mobile's native features are handled by \`burnt\`.`,
},
{
name: `burntOptions`,
required: false,
type: `Omit<BurntToastOptions, 'title' | 'message' | 'duration'>`,
description: `Options for the burnt package if you're using native toasts on mobile`,
},
{
name: `notificationOptions`,
required: false,
type: `NotificationOptions`,
description: `Options for the notification API if you're using native toasts on web`,
},
]}
/>
### ToastViewport
The portal for toasts to be directed to. Should be used inside [ToastProvider](#toastprovider). Beyond [Stack Props](/docs/components/stacks), adds:
<PropsTable
data={[
{
name: 'hotkey',
type: 'string[]',
default: "['F8']",
required: false,
description: `The keys to use as the keyboard shortcut that will move focus to the toast viewport.`,
},
{
name: 'label',
type: 'string',
default: 'Notifications ({hotkey})',
required: false,
description: `An author-localized label for the toast viewport to provide context for screen reader users when navigating page landmarks. The available \`{hotkey}\` placeholder will be replaced for you.`,
},
{
name: 'name',
type: 'string',
required: false,
description: `Used to reference the viewport if you want to have multiple viewports in the same provider.`,
},
{
name: 'multipleToasts',
type: 'boolean',
required: false,
description: `Pass this when you want to have multiple/duplicated toasts.`,
},
{
name: 'portalToRoot',
type: 'boolean | number',
default: 'false',
description: `When true, uses a portal to render at the very top of the root TamaguiProvider.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast
Contains the Title, Description, Action and Close component. Should be used inside [ToastProvider](#toastprovider). Extends [Stack](/docs/components/stacks) and adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'type',
type: "'foreground' | 'background'",
required: false,
description: `Control the sensitivity of the toast for accessibility purposes. For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.`,
},
{
name: 'duration',
type: 'number',
required: false,
description: `Time in milliseconds that toast should remain visible for. Overrides value given to \`ToastProvider\`.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `The controlled open state of the dialog. Must be used in conjunction with \`onOpenChange\`.`,
},
{
name: 'onOpenChange',
type: '(open: boolean): void',
required: false,
description: `Event handler called when the open state of the dialog changes.`,
},
{
name: 'onEscapeKeyDown',
type: "(): DismissableProps['onEscapeKeyDown']",
required: false,
description: `Event handler called when the escape key is down. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onPause',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is paused. On web, this occurs when the pointer is moved over the viewport, the viewport is focused or when the window is blurred. On mobile, this occurs when the toast is touched.`,
},
{
name: 'onResume',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is resumed. On web, this occurs when the pointer is moved away from the viewport, the viewport is blurred or when the window is focused. On mobile, this occurs when the toast is released.`,
},
{
name: 'onSwipeStart',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called when starting a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeMove',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called during a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeCancel',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the cancellation of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeEnd',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the end of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'viewportName',
type: 'string',
required: false,
description: `The viewport's name to send the toast to. Used when using multiple viewports and want to forward toasts to different ones.`,
default: 'default',
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Title
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text#sizabletext), adding:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Description
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text#sizabletext), adding:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Close
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks). You can pass `asChild` to this component and use a custom `<Button>` inside.
### Toast.Action
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks). You can pass `asChild` to this component and use a custom `<Button>` inside.
### useToastController
Used to control the display of toasts. Should be used inside [ToastProvider](#toastprovider).
<PropsTable
title="Returns"
data={[
{
name: `show`,
type: '(title: string, showOptions?: ShowToastOptions): void',
description: `Call it to show a new toast. If you're using native toasts, you can pass native options using \`burntOptions\` or \`notificationOptions\` depending on the native platform (mobile/web).`,
},
{
name: `hide`,
type: '(): void',
description: `Call it to hide the currently displayed toast.`,
},
{
name: `options`,
type: `ToastOptions`,
description: `You can use if need to access the toast options.`,
},
]}
/>
### useToastState
Used to render out your toast contents. Should be used inside [ToastProvider](#toastprovider). Doesn't take in anything and returns `ToastData`.
```tsx
const CurrentToast = () => {
const toast = useToastState()
// don't show any toast if no toast is present or it's handled natively
if (!toast || toast.isHandledNatively) {
return null
}
return (
<Toast key={toast.id} duration={toast.duration} viewport={toast.viewport}>
<Toast.Title>{toast.title}</Toast.Title>
<Toast.Description>{toast.message}</Toast.Description>
</Toast>
)
}
```
## Examples
### Position the viewport
To position the viewport on native toasts:
- iOS (burnt): Supports top or bottom placements. Adjustable by passing `from` to `burntOptions`:
```tsx
<ToastProvider burntOptions={{ from: 'bottom' }}>
```
- Android (burnt): Not supported.
- Web (Notification API): Not supported.
To position the viewport on custom toasts:
You should change the positioning of your [`<ToastViewport>`](#toastviewport). For instance, if you want them to appear from top right:
```tsx
<ToastViewport flexDirection="column-reverse" top={0} right={0} />
```
Or for bottom center:
```tsx
<ToastViewport flexDirection="column" bottom={0} left={0} right={0} />
```
<Notice theme="blue">
When using multiple toasts, you can change the order of toasts by setting
`flexDirection` to `column` or `column-reverse`. Or even have them stack horizontally
using `row` or `row-reverse`.
</Notice>
### Mobile safe area
To show toasts inside device's safe area, install `react-native-safe-area-context` if you haven't, wrap your app inside `<SafeAreaProvider>`, and then use the safe area insets to position the viewport inside the safe area.
```tsx
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const SafeToastViewport = () => {
const { left, top, right } = useSafeAreaInsets()
return (
<ToastViewport flexDirection="column-reverse" top={top} left={left} right={right} />
)
}
```
### Different viewports
To send toasts to different viewports, you can set up different viewports:
```tsx
const App = () => {
return (
<ToastProvider>
<ToastViewport /> {/* default viewport */}
<ToastViewport name="viewport-custom" />
</ToastProvider>
)
}
```
And then, use the viewport's name on the toasts.
```tsx
const MyComponent = () => {
return <Toast /> // default viewport
}
const MyComponent2 = () => {
return <Toast viewportName="viewport-custom" />
}
```
### Custom data
Just pass your custom data to the second parameter of the `show()` method.
```ts
const toastController = useToastController()
toastController.show('Title', { myPreset: 'error' }) // or toastController.show("Title", { customData: { myPreset: 'error' } })
```
then, when showing the toast, you can retrieve them like so:
```ts
const toastState = useToastState()
toastState.myPreset // or toastState.customData.myPreset
```
To add TypeScript auto-completion for your custom fields, you can use TS module augmentation:
```ts
declare module '@tamagui/toast' {
interface CustomData {
myPreset: 'error' | 'success' | 'warning'
}
}
```
### Without hooks
You can also use toasts without the hooks.
<Notice>You can't use native toasts this way.</Notice>
#### Single Toast
```tsx
export default () => {
const [open, setOpen] = React.useState(false)
const timerRef = React.useRef(0)
React.useEffect(() => {
return () => clearTimeout(timerRef.current)
}, [])
return (
<YStack ai="center">
<Button
onPress={() => {
setOpen(false)
window.clearTimeout(timerRef.current)
timerRef.current = window.setTimeout(() => {
setOpen(true)
}, 150)
}}
>
Single Toast
</Button>
<Toast
onOpenChange={setOpen}
open={open}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
</YStack>
)
}
```
#### Multiple Toasts
<Notice>
To use multiple toasts, you should pass `multipleToasts` to your `ToastViewport`.
Otherwise there'll be issues when swipe-dismissing or animating toasts.
</Notice>
```tsx
export default () => {
const [savedCount, setSavedCount] = React.useState(0)
return (
<YStack ai="center">
<Button
onPress={() => {
setSavedCount((old) => old + 1)
}}
>
Show toast
</Button>
{[...Array(savedCount)].map((_, index) => (
<Toast
key={index}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
))}
</YStack>
)
}
```
## components/toast/1.9.1
---
title: Toast
description: A toast component with native features
name: toast
component: Toast
package: toast
demoName: Toast
---
# Toast <Beta />
<Description>Use to show feedback to user interactions</Description>
<HeroContainer showAnimationDriverControl>
<ToastDemo />
</HeroContainer>
```tsx hero template=Toast
```
<Highlights
features={[
`Automatically closes`,
`Pause closing on hover, focus, window blur and mobile touch`,
`Supports closing via swipe gesture`,
`Easily animatable with Tamagui's animation drivers`,
`Native toasts included for Android, iOS and web (notification API)`,
]}
/>
## Usage
### Imperative API
This API offers a nice imperative interface, support for native toasts with a lot of control.
#### Installation
For native support on mobile, run `yarn add burnt` to add `burnt`, then rebuild your React Native app. React Native requires sub-dependencies with native dependencies always be hoisted to your apps package.json and Toast relies on the amazing [Burnt](https://github.com/nandorojo/burnt) library by Fernando Rojo to provide its native functionality.
Note: `Burnt` [will not work](https://github.com/nandorojo/burnt?tab=readme-ov-file#expo) with Expo Go.
#### Usage
To display the toast natively, you should either pass an array of native platforms (`native: ["ios", "web"]`), a single platform or `true` for all platforms.
```tsx
import { Button } from 'tamagui' // or '@tamagui/button'
import { Toast, ToastImperativeProvider, ToastProvider, useToast } from 'tamagui' // or '@tamagui/toast'
const options = { native: 'mobile' }
export default () => (
<ToastProvider>
<ToastImperativeProvider options={options}>
<CurrentToast />
<MyPage />
</ToastImperativeProvider>
<ToastViewport />
</ToastProvider>
)
const CurrentToast = () => {
const { currentToast } = useToast()
// only show the component if it's present and not handled by native toast
if (!currentToast || currentToast.isHandledNatively) return null
return (
<Toast key={currentToast.id}>
<Toast.Title>{currentToast.title}</Toast.Title>
<Toast.Description>{currentToast.message}</Toast.Description>
</Toast>
)
}
const MyPage = () => {
const { show } = useToast()
return (
<Button onPress={() => show('Done!', { message: 'Form submitted successfully.' })}>
Show Toast
</Button>
)
}
```
### Barebone API
<Notice>
This API does not support native toasts. For native toasts, use the [Imperative
API](#imperative-api).
</Notice>
#### Single Toast
```tsx
export default () => {
const [open, setOpen] = React.useState(false)
const timerRef = React.useRef(0)
React.useEffect(() => {
return () => clearTimeout(timerRef.current)
}, [])
return (
<YStack ai="center">
<Button
onPress={() => {
setOpen(false)
window.clearTimeout(timerRef.current)
timerRef.current = window.setTimeout(() => {
setOpen(true)
}, 150)
}}
>
Single Toast
</Button>
<Toast
onOpenChange={setOpen}
open={open}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
</YStack>
)
}
```
#### Multiple Toast
<Notice>
To use multiple toasts, you should pass `multipleToasts` to your `ToastViewport`.
Otherwise there'll be issues when swipe-dismissing or animating toasts.
</Notice>
```tsx
export default () => {
const [savedCount, setSavedCount] = React.useState(0)
return (
<YStack ai="center">
<Button
onPress={() => {
setSavedCount((old) => old + 1)
}}
>
Show toast
</Button>
{[...Array(savedCount)].map((_, index) => (
<Toast
key={index}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
))}
</YStack>
)
}
```
## API
### ToastProvider
Your toasts should be wrapped within a `ToastProvider`. This is usually done at the root of your application.
<PropsTable
data={[
{
name: 'label',
required: false,
type: 'string',
description: `An author-localized label for each toast. Used to help screen reader users associate the interruption with a toast.`,
default: 'Notification',
},
{
name: 'duration',
required: false,
type: 'number',
description: `Time in milliseconds that each toast should remain visible for. This could be overwritten at the toast level as well.`,
default: 5000,
},
{
name: 'swipeDirection',
required: false,
type: 'SwipeDirection',
description: `Direction of pointer swipe that should close the toast.`,
default: 'right',
},
{
name: 'swipeThreshold',
required: false,
type: 'number',
description: `Distance in pixels that the swipe must pass before a close is triggered.`,
default: 50,
},
{
name: 'id',
required: false,
type: 'string',
default: 'A unique generated ID',
},
]}
/>
### ToastViewport
The portal for toasts to be directed to. Should be inside [ToastProvider](#toastprovider). Beyond [Stack Props](/docs/components/stacks/1.0.0), adds:
<PropsTable
data={[
{
name: 'hotkey',
type: 'string[]',
default: "['F8']",
required: false,
description: `The keys to use as the keyboard shortcut that will move focus to the toast viewport.`,
},
{
name: 'label',
type: 'string',
default: 'Notifications ({hotkey})',
required: false,
description: `An author-localized label for the toast viewport to provide context for screen reader users when navigating page landmarks. The available \`{hotkey}\` placeholder will be replaced for you.`,
},
{
name: 'name',
type: 'string',
required: false,
description: `Used to reference the viewport if you want to have multiple viewports in the same provider.`,
},
{
name: 'multipleToasts',
type: 'boolean',
required: false,
description: `Pass this when you want to have multiple/duplicated toasts.`,
},
]}
/>
### Toast
Contains the Title, Description, Action and Close component. Should be inside [ToastProvider](#toastprovider). Extends [Stack](/docs/components/stack#api) and adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'type',
type: "'foreground' | 'background'",
required: false,
description: `Control the sensitivity of the toast for accessibility purposes. For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.`,
},
{
name: 'duration',
type: 'number',
required: false,
description: `Time in milliseconds that toast should remain visible for. Overrides value given to \`ToastProvider\`.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `The controlled open state of the dialog. Must be used in conjunction with \`onOpenChange\`.`,
},
{
name: 'onOpenChange',
type: '(open: boolean): void',
required: false,
description: `Event handler called when the open state of the dialog changes.`,
},
{
name: 'onEscapeKeyDown',
type: "(): DismissableProps['onEscapeKeyDown']",
required: false,
description: `Event handler called when the escape key is down. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onPause',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is paused. On web, this occurs when the pointer is moved over the viewport, the viewport is focused or when the window is blurred. On mobile, this occurs when the toast is touched.`,
},
{
name: 'onResume',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is resumed. On web, this occurs when the pointer is moved away from the viewport, the viewport is blurred or when the window is focused. On mobile, this occurs when the toast is released.`,
},
{
name: 'onSwipeStart',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called when starting a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeMove',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called during a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeCancel',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the cancellation of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeEnd',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the end of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'viewportName',
type: 'string',
required: false,
description: `The viewport's name to send the toast to. Used when using multiple viewports and want to forward toasts to different ones.`,
default: 'default',
},
]}
/>
#### Toast.Title
Should be inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext).
#### Toast.Description
Should be inside [Toast](#toast). Extends [SizableText](/docs/components/text/1.0.0#sizabletext).
#### Toast.Close
Should be inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
#### Toast.Action
Should be inside [Toast](#toast). Extends [Stack](/docs/components/stacks/1.0.0). You can pass `asChild` to this component and use a custom `<Button>` inside.
### ToastImperativeProvider
Wrap components within this provider to use `useToast()` inside them.
<PropsTable
data={[
{
name: 'options',
type: 'ToastImperativeOptions',
required: false,
description: `Used to provide defaults to imperative API. Options can be overwritten when calling \`show()\`.`,
},
]}
/>
### useToast
Used when using the imperative API.
<PropsTable
title="Returns"
data={[
{
name: 'currentToast',
type: 'ToastData | null',
description: `The information about the current toast to show such as title, message, duration, etc.`,
},
{
name: `show`,
type: '(title: string, showOptions?: ShowToastOptions): void',
description: `Call it to show a new toast. If you're using native toasts, you can pass native options using \`burntOptions\` or \`notificationOptions\` depending on the native platform (mobile/web).`,
},
{
name: `hide`,
type: '(): void',
description: `Call it to hide the currently displayed toast.`,
},
]}
/>
## FAQ
#### How to change the placement of toasts?
##### Native toasts
- iOS (burnt): Supports top or bottom placements. Adjustable by passing `from` to `burntOptions`:
```tsx
<ToastImperativeProvider options={{ burntOptions: { from: 'bottom' } }}>
```
- Android (burnt): Not supported.
- Web (Notification API): Not supported.
##### Non-native toasts
You should change the positioning of your [`<ToastViewport>`](#toastviewport). For instance, if you want them to appear from top right:
```tsx
<ToastViewport flexDirection="column-reverse" top={0} right={0} />
```
Or for bottom center:
```tsx
<ToastViewport flexDirection="column" bottom={0} left={0} right={0} />
```
<Notice theme="blue">
When using multiple toasts, you can change the order of toasts by setting
`flexDirection` to `column` or `column-reverse`. Or even have them stack horizontally
using `row` or `row-reverse`.
</Notice>
#### How to show non-native toasts within safe area on mobile?
Install `react-native-safe-area-context` if you haven't, wrap your app inside `<SafeAreaProvider>`, and use the safe area insets to position the viewport inside the safe area.
```tsx
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const SafeToastViewport = () => {
const { left, top, right } = useSafeAreaInsets()
return (
<ToastViewport flexDirection="column-reverse" top={top} left={left} right={right} />
)
}
```
#### Can I have multiple viewports?
Yes, but you will have to name them and then reference the viewport name on the `<Toast>` component. for instance:
```tsx
const App = () => {
return (
<ToastProvider>
<ToastViewport /> // name will be "default"
<ToastViewport name="viewport-custom" />
</ToastProvider>
)
}
const MyComponent = () => {
return <Toast> // goes to default viewport // ...</Toast>
}
const MyComponent2 = () => {
return <Toast viewportName="viewport-custom">// ...</Toast>
}
```
## components/toast/2.0.0
---
title: Toast
description: Use to show feedback to user interactions.
name: toast
component: Toast
package: toast
demoName: ToastV1
---
<HeroContainer showAnimationDriverControl>
<ToastV1Demo />
</HeroContainer>
```tsx hero template=Toast
```
<Highlights
features={[
`Automatically closes`,
`Pause closing on hover, focus, window blur and mobile touch`,
`Supports closing via swipe gesture`,
`Easily animatable with Tamagui's animation drivers`,
`Native toasts included for Android, iOS and web (notification API)`,
]}
/>
## Installation
Toast is included by default in `tamagui`. If you're using it standalone:
```bash
yarn add @tamagui/toast
```
### Native Toast Setup
For native toasts on iOS/Android (using [Burnt](https://github.com/nandorojo/burnt) by Fernando Rojo), install the native dependency and import the setup module:
```bash
yarn add burnt
```
```tsx
// App.tsx - before any Tamagui imports
import '@tamagui/native/setup-burnt'
```
Then rebuild your React Native app. Without this setup, toasts will still work but the `native` prop will have no effect on mobile.
## Anatomy
```tsx
<ToastProvider>
<Toast>
<Toast.Title />
<Toast.Description />
<Toast.Action />
<Toast.Close />
</Toast>
<ToastViewport />
</ToastProvider>
```
## API Reference
### ToastProvider
Your toasts should be wrapped within a `ToastProvider`. This is usually done at the root of your application.
<PropsTable
data={[
{
name: 'label',
required: false,
type: 'string',
description: `An author-localized label for each toast. Used to help screen reader users associate the interruption with a toast.`,
default: 'Notification',
},
{
name: 'duration',
required: false,
type: 'number',
description: `Time in milliseconds that each toast should remain visible for. This could be overwritten at the toast level as well.`,
default: 5000,
},
{
name: 'swipeDirection',
required: false,
type: 'SwipeDirection',
description: `Direction of pointer swipe that should close the toast.`,
default: 'right',
},
{
name: 'swipeThreshold',
required: false,
type: 'number',
description: `Distance in pixels that the swipe must pass before a close is triggered.`,
default: 50,
},
{
name: 'id',
required: false,
type: 'string',
default: 'A unique generated ID',
},
{
name: 'native',
required: false,
type: 'boolean | ToastNativePlatform | ToastNativePlatform[]',
description: `Will show a native toast if is true or is set to the current platform. On iOS, it wraps \`SPIndicator\` and \`SPAlert\`. On Android, it wraps \`ToastAndroid\`. On web, it wraps Notification API. Mobile's native features are handled by \`burnt\`.`,
},
{
name: `burntOptions`,
required: false,
type: `Omit<BurntToastOptions, 'title' | 'message' | 'duration'>`,
description: `Options for the burnt package if you're using native toasts on mobile`,
},
{
name: `notificationOptions`,
required: false,
type: `NotificationOptions`,
description: `Options for the notification API if you're using native toasts on web`,
},
]}
/>
### ToastViewport
The portal for toasts to be directed to. Should be used inside [ToastProvider](#toastprovider). Beyond [Stack Props](/docs/components/stacks), adds:
<PropsTable
data={[
{
name: 'hotkey',
type: 'string[]',
default: "['F8']",
required: false,
description: `The keys to use as the keyboard shortcut that will move focus to the toast viewport.`,
},
{
name: 'label',
type: 'string',
default: 'Notifications ({hotkey})',
required: false,
description: `An author-localized label for the toast viewport to provide context for screen reader users when navigating page landmarks. The available \`{hotkey}\` placeholder will be replaced for you.`,
},
{
name: 'name',
type: 'string',
required: false,
description: `Used to reference the viewport if you want to have multiple viewports in the same provider.`,
},
{
name: 'multipleToasts',
type: 'boolean',
required: false,
description: `Pass this when you want to have multiple/duplicated toasts.`,
},
{
name: 'portalToRoot',
type: 'boolean | number',
default: 'false',
description: `When true, uses a portal to render at the very top of the root TamaguiProvider.`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast
Contains the Title, Description, Action and Close component. Should be used inside [ToastProvider](#toastprovider). Extends [Stack](/docs/components/stacks) and adds:
<PropsTable
data={[
{
name: 'forceMount',
type: 'boolean',
required: false,
description: `Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries.`,
},
{
name: 'type',
type: "'foreground' | 'background'",
required: false,
description: `Control the sensitivity of the toast for accessibility purposes. For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.`,
},
{
name: 'duration',
type: 'number',
required: false,
description: `Time in milliseconds that toast should remain visible for. Overrides value given to \`ToastProvider\`.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `The controlled open state of the dialog. Must be used in conjunction with \`onOpenChange\`.`,
},
{
name: 'onOpenChange',
type: '(open: boolean): void',
required: false,
description: `Event handler called when the open state of the dialog changes.`,
},
{
name: 'onEscapeKeyDown',
type: "(): DismissableProps['onEscapeKeyDown']",
required: false,
description: `Event handler called when the escape key is down. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onPause',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is paused. On web, this occurs when the pointer is moved over the viewport, the viewport is focused or when the window is blurred. On mobile, this occurs when the toast is touched.`,
},
{
name: 'onResume',
type: '(): void',
required: false,
description: `Event handler called when the dismiss timer is resumed. On web, this occurs when the pointer is moved away from the viewport, the viewport is blurred or when the window is focused. On mobile, this occurs when the toast is released.`,
},
{
name: 'onSwipeStart',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called when starting a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeMove',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called during a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeCancel',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the cancellation of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'onSwipeEnd',
type: '(event: SwipeEvent): void',
required: false,
description: `Event handler called at the end of a swipe interaction. It can be prevented by calling \`event.preventDefault\`.`,
},
{
name: 'viewportName',
type: 'string',
required: false,
description: `The viewport's name to send the toast to. Used when using multiple viewports and want to forward toasts to different ones.`,
default: 'default',
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Title
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text#sizabletext), adding:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Description
Should be used inside [Toast](#toast). Extends [SizableText](/docs/components/text#sizabletext), adding:
<PropsTable
data={[
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
### Toast.Close
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks). You can pass `asChild` to this component and use a custom `<Button>` inside.
### Toast.Action
Should be used inside [Toast](#toast). Extends [Stack](/docs/components/stacks). You can pass `asChild` to this component and use a custom `<Button>` inside.
### useToastController
Used to control the display of toasts. Should be used inside [ToastProvider](#toastprovider).
<PropsTable
title="Returns"
data={[
{
name: `show`,
type: '(title: string, showOptions?: ShowToastOptions): void',
description: `Call it to show a new toast. If you're using native toasts, you can pass native options using \`burntOptions\` or \`notificationOptions\` depending on the native platform (mobile/web).`,
},
{
name: `hide`,
type: '(): void',
description: `Call it to hide the currently displayed toast.`,
},
{
name: `options`,
type: `ToastOptions`,
description: `You can use if need to access the toast options.`,
},
]}
/>
### useToastState
Used to render out your toast contents. Should be used inside [ToastProvider](#toastprovider). Doesn't take in anything and returns `ToastData`.
```tsx
const CurrentToast = () => {
const toast = useToastState()
// don't show any toast if no toast is present or it's handled natively
if (!toast || toast.isHandledNatively) {
return null
}
return (
<Toast key={toast.id} duration={toast.duration} viewport={toast.viewport}>
<Toast.Title>{toast.title}</Toast.Title>
<Toast.Description>{toast.message}</Toast.Description>
</Toast>
)
}
```
## Examples
### Position the viewport
To position the viewport on native toasts:
- iOS (burnt): Supports top or bottom placements. Adjustable by passing `from` to `burntOptions`:
```tsx
<ToastProvider burntOptions={{ from: 'bottom' }}>
```
- Android (burnt): Not supported.
- Web (Notification API): Not supported.
To position the viewport on custom toasts:
You should change the positioning of your [`<ToastViewport>`](#toastviewport). For instance, if you want them to appear from top right:
```tsx
<ToastViewport flexDirection="column-reverse" top={0} right={0} />
```
Or for bottom center:
```tsx
<ToastViewport flexDirection="column" bottom={0} left={0} right={0} />
```
<Notice theme="blue">
When using multiple toasts, you can change the order of toasts by setting
`flexDirection` to `column` or `column-reverse`. Or even have them stack horizontally
using `row` or `row-reverse`.
</Notice>
### Mobile safe area
To show toasts inside device's safe area, install `react-native-safe-area-context` if you haven't, wrap your app inside `<SafeAreaProvider>`, and then use the safe area insets to position the viewport inside the safe area.
```tsx
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const SafeToastViewport = () => {
const { left, top, right } = useSafeAreaInsets()
return (
<ToastViewport flexDirection="column-reverse" top={top} left={left} right={right} />
)
}
```
### Different viewports
To send toasts to different viewports, you can set up different viewports:
```tsx
const App = () => {
return (
<ToastProvider>
<ToastViewport /> {/* default viewport */}
<ToastViewport name="viewport-custom" />
</ToastProvider>
)
}
```
And then, use the viewport's name on the toasts.
```tsx
const MyComponent = () => {
return <Toast /> // default viewport
}
const MyComponent2 = () => {
return <Toast viewportName="viewport-custom" />
}
```
### Custom data
Just pass your custom data to the second parameter of the `show()` method.
```ts
const toastController = useToastController()
toastController.show('Title', { myPreset: 'error' }) // or toastController.show("Title", { customData: { myPreset: 'error' } })
```
then, when showing the toast, you can retrieve them like so:
```ts
const toastState = useToastState()
toastState.myPreset // or toastState.customData.myPreset
```
To add TypeScript auto-completion for your custom fields, you can use TS module augmentation:
```ts
declare module '@tamagui/toast' {
interface CustomData {
myPreset: 'error' | 'success' | 'warning'
}
}
```
### Without hooks
You can also use toasts without the hooks.
<Notice>You can't use native toasts this way.</Notice>
#### Single Toast
```tsx
export default () => {
const [open, setOpen] = React.useState(false)
const timerRef = React.useRef(0)
React.useEffect(() => {
return () => clearTimeout(timerRef.current)
}, [])
return (
<YStack ai="center">
<Button
onPress={() => {
setOpen(false)
window.clearTimeout(timerRef.current)
timerRef.current = window.setTimeout(() => {
setOpen(true)
}, 150)
}}
>
Single Toast
</Button>
<Toast
onOpenChange={setOpen}
open={open}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
</YStack>
)
}
```
#### Multiple Toasts
<Notice>
To use multiple toasts, you should pass `multipleToasts` to your `ToastViewport`.
Otherwise there'll be issues when swipe-dismissing or animating toasts.
</Notice>
```tsx
export default () => {
const [savedCount, setSavedCount] = React.useState(0)
return (
<YStack ai="center">
<Button
onPress={() => {
setSavedCount((old) => old + 1)
}}
>
Show toast
</Button>
{[...Array(savedCount)].map((_, index) => (
<Toast
key={index}
animation="100ms"
enterStyle={{ x: -20, opacity: 0 }}
exitStyle={{ x: -20, opacity: 0 }}
opacity={1}
x={0}
>
<Toast.Title>Subscribed!</Toast.Title>
<Toast.Description>We'll be in touch.</Toast.Description>
</Toast>
))}
</YStack>
)
}
```
## components/toast-2/2.0.0
---
title: Toast v2
description: Beautiful, accessible toasts with smooth animations and gesture support.
name: toast-2
component: Toast
package: toast
demoName: Toast
---
<HeroContainer showAnimationDriverControl>
<ToastDemo />
</HeroContainer>
```tsx hero template=Toast
```
<Highlights
features={[
`Composable component API`,
`Smooth stacking with hover-to-expand`,
`Swipe to dismiss with spring physics`,
`Promise toasts for async operations`,
`Works with all Tamagui animation drivers`,
]}
/>
## Installation
```bash
yarn add @tamagui/toast
```
Optionally install [Burnt](https://github.com/nandorojo/burnt) for native OS-level toasts on iOS/Android. Without it, toasts render as in-app UI on all platforms.
## Quick Start
```tsx
import { Toast, toast } from '@tamagui/toast/v2'
function App() {
return (
<Toast position="bottom-right">
<Toast.Viewport>
<Toast.List
renderItem={({ toast: t, index }) => (
<Toast.Item key={t.id} toast={t} index={index}>
<Toast.Title>{t.title}</Toast.Title>
<Toast.Description>{t.description}</Toast.Description>
<Toast.Close />
</Toast.Item>
)}
/>
</Toast.Viewport>
<Button onPress={() => toast('Hello!')}>Show Toast</Button>
</Toast>
)
}
```
## Anatomy
```tsx
<Toast> {/* Root - context and position */}
<Toast.Viewport> {/* Container with portal */}
<Toast.Item> {/* Individual toast with stacking/drag */}
<Toast.Title />
<Toast.Description />
<Toast.Close />
<Toast.Action />
</Toast.Item>
</Toast.Viewport>
</Toast>
```
## API
### toast()
The imperative toast function:
```tsx
// Basic
toast('Hello world')
// With types
toast.success('Saved!')
toast.error('Failed')
toast.warning('Careful')
toast.info('FYI')
toast.loading('Processing...')
// With options
toast('Message', {
description: 'More details here',
duration: 5000,
})
// Dismiss
const id = toast('Hello')
toast.dismiss(id) // dismiss specific
toast.dismiss() // dismiss all
```
### toast.promise()
For async operations:
```tsx
toast.promise(saveData(), {
loading: 'Saving...',
success: 'Saved!',
error: 'Failed to save',
})
```
### Toast (Root)
<PropsTable
data={[
{
name: 'position',
type: "'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'",
default: "'bottom-right'",
description: 'Position of the toast viewport.',
},
{
name: 'duration',
type: 'number',
default: '4000',
description: 'Default toast duration in milliseconds.',
},
{
name: 'gap',
type: 'number',
default: '14',
description: 'Gap between toasts when expanded.',
},
{
name: 'visibleToasts',
type: 'number',
default: '4',
description: 'Maximum number of visible toasts.',
},
{
name: 'swipeDirection',
type: "'auto' | 'left' | 'right' | 'up' | 'down'",
default: "'auto'",
description: "Swipe direction to dismiss. 'auto' detects based on position.",
},
{
name: 'closeButton',
type: 'boolean',
default: 'false',
description: 'Show close button on toasts.',
},
]}
/>
### Toast.Viewport
The portal container for toasts. Extends Stack.
<PropsTable
data={[
{
name: 'offset',
type: 'number | { top?: number, right?: number, bottom?: number, left?: number }',
default: '24',
description: 'Offset from screen edge.',
},
{
name: 'hotkey',
type: 'string[]',
default: "['altKey', 'KeyT']",
description: 'Hotkey to focus viewport.',
},
{
name: 'label',
type: 'string',
default: "'Notifications'",
description: 'Aria label for the viewport.',
},
]}
/>
### Toast.Item
Individual toast wrapper with stacking and drag support. Extends Stack.
<PropsTable
data={[
{
name: 'toast',
type: 'ToastT',
required: true,
description: 'The toast data object.',
},
{
name: 'index',
type: 'number',
required: true,
description: 'Index in the toast list (for stacking).',
},
]}
/>
Supports render prop for custom content:
```tsx
<Toast.Item toast={t} index={i}>
{({ toast, handleClose }) => (
<YStack>
<Toast.Title>{toast.title}</Toast.Title>
<Button onPress={handleClose}>Close</Button>
</YStack>
)}
</Toast.Item>
```
### Toast.Title
Styled text for the toast title. Extends SizableText.
### Toast.Description
Styled text for the toast description. Extends SizableText.
### Toast.Close
Close button. Extends Stack.
### Toast.Action
Action button. Extends Stack.
### useToasts
Hook to access toast state from a descendant of `<Toast>`. Most apps should use `<Toast.List>` for rendering and reach for this hook only when they need custom list control.
```tsx
const { toasts, expanded, position } = useToasts()
```
## Features
### Stacking
Multiple toasts stack visually. Hover over the stack to expand and see all toasts.
### Swipe to Dismiss
Toasts can be swiped away. The direction is auto-detected based on position:
- **Web:** Left/right positioned toasts swipe toward their edge, center toasts swipe horizontally
- **Native:** All positions swipe vertically (up/down) to avoid conflicting with iOS/Android navigation back gesture
When `react-native-gesture-handler` is set up via `@tamagui/native/setup-gesture-handler`, the toast uses native gesture coordination (`activeOffsetY`, `failOffsetX`) to prevent ScrollView from scrolling while swiping.
### Native Safe Area
On native, toasts portal to root and automatically respect safe area insets (status bar, Dynamic Island, home indicator) when configured:
```tsx
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { TamaguiProvider } from 'tamagui'
function App() {
return (
<SafeAreaProvider>
<AppInner />
</SafeAreaProvider>
)
}
function AppInner() {
const insets = useSafeAreaInsets()
return (
<TamaguiProvider config={config} insets={insets}>
{/* toasts will automatically avoid status bar and home indicator */}
</TamaguiProvider>
)
}
```
The toast reads insets from `TamaguiProvider` via `useConfiguration().insets` — the same mechanism used by other Tamagui components like Slider.
### Custom Styling
Each part can be styled independently:
```tsx
<Toast.Item toast={t} index={i} backgroundColor="$blue5" borderRadius="$6">
<Toast.Title color="$blue12" fontWeight="bold">
{t.title}
</Toast.Title>
<Toast.Description color="$blue11">
{t.description}
</Toast.Description>
</Toast.Item>
```
## Toaster (All-in-One)
For simpler use cases, use the `Toaster` component which handles rendering internally:
```tsx
import { toast, Toaster } from '@tamagui/toast/v2'
function App() {
return (
<>
<Toaster position="bottom-right" />
<Button onPress={() => toast('Hello!')}>Show Toast</Button>
</>
)
}
```
Mount `<Toaster>` inside your `TamaguiProvider`, usually next to your app navigation or root layout.
## components/toggle-group/1.10.0
---
title: ToggleGroup
description: Two-state buttons that can be toggled on or off.
name: toggleGroup
component: ToggleGroup
package: toggle-group
demoName: ToggleGroup
---
<HeroContainer>
<ToggleGroupDemo />
</HeroContainer>
```tsx hero template=ToggleGroup
```
<Highlights
features={[
`Full keyboard navigation.`,
`Supports horizontal/vertical orientation.`,
`Support single and multiple pressed buttons.`,
`Can be controlled or uncontrolled.`,
]}
/>
## Installation
ToggleGroup is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/toggle-group
```
## Usage
```tsx
import { ToggleGroup } from 'tamagui'
export default () => {
return (
<ToggleGroup type="single">
<ToggleGroup.Item value="foo"></ToggleGroup.Item>
<ToggleGroup.Item value="bar"></ToggleGroup.Item>
</ToggleGroup>
)
}
```
## API Reference
### ToggleGroup
`ToggleGroup` extends the [Group](/docs/components/group) component. You can disable passing border radius to children by passing `disablePassBorderRadius`. plus:
<PropsTable
data={[
{
name: 'asChild',
type: 'boolean',
description: `When true, Tamagui expects a single child element. Instead of rendering its own element, it will pass all props to that child, merging together any event handling props.`,
default: `false`,
},
{
name: 'type',
type: 'enum',
description: `Determines whether a single or multiple items can be pressed at a time.`,
},
{
name: 'value',
type: 'string',
description: `The controlled value of the pressed item when type is "single". Must be used in conjunction with onValueChange.`,
},
{
name: 'defaultValue',
description: 'The values of the items to show as pressed when initially rendered.',
type: 'string',
default: ``,
},
{
name: 'orientation',
type: 'enum',
description: `The orientation of the component, which determines how focus moves: horizontal for left/right arrows and vertical for up/down arrows.`,
default: `horizontal`,
},
{
name: 'disabled',
type: 'boolean',
description: `When true, prevents the user from interacting with the toggle group and all its items.`,
default: `false`,
},
{
name: 'onValueChange',
type: '(value: string[]) => void',
description: `Event handler called when the pressed state of an item changes and type is "multiple".`,
},
{
name: 'loop',
type: 'boolean',
description: `Whether or not to loop over after reaching the end or start of the items. Used mainly for managing keyboard navigation.`,
default: `true`,
},
{
name: 'disableDeactivation',
type: 'boolean',
description: `Won't let the user turn the active item off. Only applied to single toggle group.`,
default: 'false',
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
{
name: 'sizeAdjust',
type: 'number',
description: `Adjust the component's size scaling by this number.`,
},
]}
/>
### ToggleGroup.Item
`ToggleGroup.Item` extend Stack views inheriting all the [Tamagui standard props](notion://www.notion.so/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'asChild',
type: 'boolean',
description: `When true, Tamagui expects a single child element. Instead of rendering its own element, it will pass all props to that child, merging together any event handling props.`,
default: `false`,
},
{
name: 'value',
type: 'string',
description: `The controlled value of the pressed item when type is "single".`,
},
{
name: 'disabled',
type: 'boolean',
description: `When true, prevents the user from interacting with the toggle group item.`,
default: `false`,
},
{
name: 'unstyled',
type: 'boolean',
default: 'false',
description: `When true, remove all default tamagui styling.`,
},
]}
/>
When it is active, it will receive an `active` prop set to true. This means you can customize the active styles like so:
```tsx
import { ToggleGroup } from '@tamagui/toggle-group'
import { styled } from 'tamagui'
const MyToggleGroupItem = styled(ToggleGroup.Item, {
variants: {
active: {
true: {
backgroundColor: 'red',
},
},
},
})
```
## components/toggle-group/2.0.0
---
title: ToggleGroup
description: Two-state buttons that can be toggled on or off
name: toggleGroup
component: ToggleGroup
package: toggle-group
demoName: ToggleGroup
---
<HeroContainer>
<ToggleGroupDemo />
</HeroContainer>
```tsx hero template=ToggleGroup
```
<Highlights
features={[
`Full keyboard navigation.`,
`Supports horizontal/vertical orientation.`,
`Support single and multiple pressed buttons.`,
`Can be controlled or uncontrolled.`,
]}
/>
## Installation
ToggleGroup is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/toggle-group
```
## Usage
ToggleGroup handles the toggle state logic. For visual grouping with border radius, compose with `XGroup` or `YGroup`. Use `activeStyle` to customize the active state appearance:
```tsx
import { ToggleGroup, XGroup } from 'tamagui'
export default () => {
return (
<ToggleGroup type="single">
<XGroup>
<XGroup.Item>
<ToggleGroup.Item
value="foo"
borderRadius="$4"
activeStyle={{ backgroundColor: '$color5' }}
>
Foo
</ToggleGroup.Item>
</XGroup.Item>
<XGroup.Item>
<ToggleGroup.Item
value="bar"
borderRadius="$4"
activeStyle={{ backgroundColor: '$color5' }}
>
Bar
</ToggleGroup.Item>
</XGroup.Item>
</XGroup>
</ToggleGroup>
)
}
```
For vertical layouts, use `YGroup`:
```tsx
<ToggleGroup type="single" orientation="vertical">
<YGroup>
<YGroup.Item>
<ToggleGroup.Item value="top" borderRadius="$4">
Top
</ToggleGroup.Item>
</YGroup.Item>
<YGroup.Item>
<ToggleGroup.Item value="bottom" borderRadius="$4">
Bottom
</ToggleGroup.Item>
</YGroup.Item>
</YGroup>
</ToggleGroup>
```
## API Reference
### ToggleGroup
ToggleGroup manages toggle state and keyboard navigation. It does not render any visible element by default - use `XGroup`/`YGroup` for visual grouping.
<PropsTable
data={[
{
name: 'type',
type: '"single" | "multiple"',
description: `Determines whether a single or multiple items can be pressed at a time.`,
},
{
name: 'value',
type: 'string | string[]',
description: `The controlled value of the pressed item(s). Use string for type="single", string[] for type="multiple".`,
},
{
name: 'defaultValue',
description: 'The value of the item(s) to show as pressed when initially rendered.',
type: 'string | string[]',
},
{
name: 'orientation',
type: '"horizontal" | "vertical"',
description: `The orientation of the component, which determines how focus moves: horizontal for left/right arrows and vertical for up/down arrows.`,
default: `"horizontal"`,
},
{
name: 'disabled',
type: 'boolean',
description: `When true, prevents the user from interacting with the toggle group and all its items.`,
default: `false`,
},
{
name: 'onValueChange',
type: '(value: string | string[]) => void',
description: `Event handler called when the pressed state of an item changes.`,
},
{
name: 'loop',
type: 'boolean',
description: `Whether or not to loop over after reaching the end or start of the items. Used for keyboard navigation.`,
default: `true`,
},
{
name: 'disableDeactivation',
type: 'boolean',
description: `Won't let the user turn the active item off. Only applies to type="single".`,
default: 'false',
},
{
name: 'rovingFocus',
type: 'boolean',
description: `Enable roving focus keyboard navigation between items.`,
default: 'true',
},
]}
/>
### ToggleGroup.Item
`ToggleGroup.Item` extends Stack views inheriting all the [Tamagui standard props](/docs/intro/props), plus:
<PropsTable
data={[
{
name: 'value',
type: 'string',
description: `A unique value for this toggle item.`,
},
{
name: 'disabled',
type: 'boolean',
description: `When true, prevents the user from interacting with this item.`,
default: `false`,
},
{
name: 'activeStyle',
type: 'StyleProp',
description: `Style object applied when the item is in its active/pressed state.`,
},
{
name: 'activeTheme',
type: 'string | null',
description: `Theme to apply when the item is active. Set to null for no theme change.`,
},
]}
/>
### Styling Active State
Customize the active/pressed state using `activeStyle`:
```tsx
// Inline usage
;<ToggleGroup.Item
value="left"
activeStyle={{ backgroundColor: '$green9', color: '$yellow9' }}
>
Left
</ToggleGroup.Item>
// Or via styled()
const GreenItem = styled(ToggleGroup.Item, {
activeStyle: {
backgroundColor: '$green9',
color: '$yellow9',
},
})
```
You can also use `activeTheme` to apply a theme when active:
```tsx
<ToggleGroup.Item value="option1" activeTheme="green">
Option 1
</ToggleGroup.Item>
```
### useToggleGroupItem
For custom components inside `ToggleGroup.Item` that need to know the active state, use the `useToggleGroupItem` hook:
```tsx
import { useToggleGroupItem, ToggleGroup } from '@tamagui/toggle-group'
function CustomLabel({ children }) {
const { active, color } = useToggleGroupItem()
return <Text color={active ? '$green10' : '$color'}>{children}</Text>
}
// Usage
;<ToggleGroup.Item value="option1">
<CustomLabel>Option 1</CustomLabel>
</ToggleGroup.Item>
```
## components/tooltip/1.0.0
---
title: Tooltip
description: A simple tooltip component
name: tooltip
component: Tooltip
package: tooltip
demoName: Tooltip
---
# Tooltip
<Description>A tooltip on web, with only accessibility output on native</Description>
<HeroContainer showAnimationDriverControl>
<TooltipDemo />
</HeroContainer>
```tsx hero template=Tooltip
```
<Highlights
features={[
`Doesn't open until your mouse stops moving.`,
`Easy to animate enter and exit.`,
`Sizable, positionable, unstyled or styled.`,
]}
/>
<Notice>Note that Tooltip doesn't render on native platforms.</Notice>
## Installation
Tooltip is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/tooltip
```
### PortalProvider
When rendering into root of app instead of inline, you'll first need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Tooltip } from 'tamagui' // or '@tamagui/tooltip'
export default () => (
<Tooltip>
<Tooltip.Trigger />
<Tooltip.Content>
<Tooltip.Arrow />
{/* ... */}
</Tooltip.Content>
</Tooltip>
)
```
## API Reference
### Tooltip
Contains every component for the tooltip.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'groupId',
type: 'string',
required: false,
description: `If given, will work alongside TooltipGroup to ensure only one tooltip in the groups stays open.`,
},
{
name: 'restMs',
type: 'number',
required: false,
description: `Time needed for cursor to rest before showing.`,
},
{
name: 'delay',
type: `number | { open?: number; close?: number }`,
required: false,
description: `Maximum time before showing (can be set independently for open/close).`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description: `Renders into root of app instead of inline`,
},
{
name: 'stayInFrame',
type: 'ShiftProps',
required: false,
description: `See floating-ui shift()`,
},
{
name: 'allowFlip',
type: 'FlipProps',
required: false,
description: `See floating-ui flip`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
]}
/>
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/tooltip/1.0.0#portalprovider) for more information.
</Notice>
### Tooltip.Trigger
Used to trigger opening of the popover when uncontrolled, see YStack in [Stacks](/docs/components/stacks).
### Tooltip.Content
Renders as SizableStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
### Tooltip.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Tooltip attaches, use Anchor. When used, Anchor is where the Tooltip will attach, while Trigger will open it.
## components/tooltip/1.105.0
---
title: Tooltip
description: A simple tooltip component
name: tooltip
component: Tooltip
package: tooltip
demoName: Tooltip
---
# Tooltip
<Description>A tooltip on web, with only accessibility output on native</Description>
<HeroContainer showAnimationDriverControl>
<TooltipDemo />
</HeroContainer>
```tsx hero template=Tooltip
```
<Highlights
features={[
`Doesn't open until your mouse stops moving.`,
`Easy to animate enter and exit.`,
`Sizable, positionable, unstyled or styled.`,
]}
/>
<Notice>Note that Tooltip doesn't render on native platforms.</Notice>
## Installation
Tooltip is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/tooltip
```
### PortalProvider
Only if you aren't not using `tamagui` (but rather `@tamagui/core`) you'll need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Tooltip } from 'tamagui' // or '@tamagui/tooltip'
export default () => (
<Tooltip>
<Tooltip.Trigger />
<Tooltip.Content>
<Tooltip.Arrow />
{/* ... */}
</Tooltip.Content>
</Tooltip>
)
```
## API Reference
### Tooltip
Contains every component for the tooltip.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'groupId',
type: 'string',
required: false,
description: `If given, will work alongside TooltipGroup to ensure only one tooltip in the groups stays open.`,
},
{
name: 'restMs',
type: 'number',
required: false,
description: `Time needed for cursor to rest before showing.`,
},
{
name: 'delay',
type: `number | { open?: number; close?: number }`,
required: false,
description: `Maximum time before showing (can be set independently for open/close).`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down too all sub-components when set for padding, arrow, borderRadius`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description: `Renders into root of app instead of inline`,
},
{
name: 'stayInFrame',
type: 'ShiftProps',
required: false,
description: `See floating-ui shift()`,
},
{
name: 'allowFlip',
type: 'FlipProps',
required: false,
description: `See floating-ui flip`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
]}
/>
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](/ui/tooltip/1.0.0#portalprovider) for more information.
</Notice>
### Tooltip.Trigger
Used to trigger opening of the popover when uncontrolled, see YStack in [Stacks](/docs/components/stacks).
### Tooltip.Content
Renders as SizableStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
### Tooltip.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Tooltip attaches, use Anchor. When used, Anchor is where the Tooltip will attach, while Trigger will open it.
### TooltipGroup
This allows you to logically group any tooltips rendered below this component. You can control their delay props, and components inside a TooltipGroup will be smart about opening quicker if you are moving between targets with tooltips, ensuring that subsequent tooltips show immediately rather than after a delay.
See the [Floating UI docs](https://floating-ui.com/docs/floatingdelaygroup) for full details on how this works.
<PropsTable
data={[
{
name: 'delay',
type: 'number | { open?: number; close?: number }',
default: 0,
required: false,
description: `The delay to use for the group.`,
},
{
name: 'timeoutMs',
type: 'number',
default: 0,
required: false,
description: `An optional timeout to use for the group, which represents when grouping logic will no longer be active after the close delay completes. Useful if you want grouping to last longer than the close delay, for example if there is no close delay at all.`,
},
{
name: 'preventAnimation',
type: 'boolean',
default: false,
required: false,
description: `Will disable the enter/exit animations while the group is active, but not for the initial enter animation of the first hovered tooltip.`,
},
]}
/>
## components/tooltip/1.122.0
---
title: Tooltip
description: A tooltip on web, with only accessibility output on native.
name: tooltip
component: Tooltip
package: tooltip
demoName: Tooltip
---
<HeroContainer showAnimationDriverControl>
<TooltipDemo />
</HeroContainer>
```tsx hero template=Tooltip
```
<Highlights
features={[
`Doesn't open until your mouse stops moving.`,
`Easy to animate enter and exit.`,
`Sizable, positionable, unstyled or styled.`,
]}
/>
<Notice>Note that Tooltip doesn't render on native platforms.</Notice>
## Installation
Tooltip is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/tooltip
```
### PortalProvider
If you are not using `tamagui` (but rather `@tamagui/core`) you'll need to install the `@tamagui/portal` package:
```bash
npm install @tamagui/portal
```
Then add `PortalProvider` to the root of your app:
```tsx fileName="App.tsx"
import { PortalProvider } from '@tamagui/portal'
import YourApp from './components/YourApp'
function App() {
return (
<PortalProvider shouldAddRootHost>
<YourApp />
</PortalProvider>
)
}
export default App
```
<PropsTable
data={[
{
name: 'shouldAddRootHost',
type: 'boolean',
required: false,
description: `Defines whether to add a default root host or not.`,
},
]}
/>
## Anatomy
```tsx
import { Tooltip } from 'tamagui' // or '@tamagui/tooltip'
export default () => (
<Tooltip>
<Tooltip.Trigger />
<Tooltip.Content>
<Tooltip.Arrow />
{/* ... */}
</Tooltip.Content>
</Tooltip>
)
```
## API Reference
### Tooltip
Contains every component for the tooltip.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'groupId',
type: 'string',
required: false,
description: `If given, will work alongside TooltipGroup to ensure only one tooltip in the groups stays open.`,
},
{
name: 'restMs',
type: 'number',
required: false,
description: `Time needed for cursor to rest before showing.`,
},
{
name: 'delay',
type: `number | { open?: number; close?: number }`,
required: false,
description: `Maximum time before showing (can be set independently for open/close).`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down to all sub-components when set for padding, arrow, borderRadius.`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: ``,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description: `Renders into root of app instead of inline`,
},
{
name: 'stayInFrame',
type: 'ShiftProps',
required: false,
description: `See floating-ui shift()`,
},
{
name: 'allowFlip',
type: 'FlipProps',
required: false,
description: `See floating-ui flip`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
]}
/>
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](#portalprovider) for more information.
</Notice>
### Tooltip.Trigger
Used to trigger opening of the popover when uncontrolled, see YStack in [Stacks](/docs/components/stacks).
### Tooltip.Content
Renders as SizableStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
### Tooltip.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Tooltip attaches, use Anchor. When used, Anchor is where the Tooltip will attach, while Trigger will open it.
### TooltipGroup
This allows you to logically group any tooltips rendered below this component. You can control their delay props, and components inside a TooltipGroup will be smart about opening quicker if you are moving between targets with tooltips, ensuring that subsequent tooltips show immediately rather than after a delay.
See the [Floating UI docs](https://floating-ui.com/docs/floatingdelaygroup) for full details on how this works.
<PropsTable
data={[
{
name: 'delay',
type: 'number | { open?: number; close?: number }',
default: 0,
required: false,
description: `The delay to use for the group.`,
},
{
name: 'timeoutMs',
type: 'number',
default: 0,
required: false,
description: `An optional timeout to use for the group, which represents when grouping logic will no longer be active after the close delay completes. Useful if you want grouping to last longer than the close delay, for example if there is no close delay at all.`,
},
{
name: 'preventAnimation',
type: 'boolean',
default: false,
required: false,
description: `Will disable the enter/exit animations while the group is active, but not for the initial enter animation of the first hovered tooltip.`,
},
]}
/>
### closeOpenTooltips
A small helper function of type `() => void` that will close any open tooltips.
## components/tooltip/2.0.0
---
title: Tooltip
description: A tooltip on web, with only accessibility output on native
name: tooltip
component: Tooltip
package: tooltip
demoName: Tooltip
---
<HeroContainer showAnimationDriverControl>
<TooltipDemo />
</HeroContainer>
```tsx hero template=Tooltip
```
<Highlights
features={[
`Doesn't open until your mouse stops moving.`,
`Easy to animate enter and exit.`,
`Sizable, positionable, unstyled or styled.`,
]}
/>
Tooltip displays contextual information when hovering over an element. It waits for your mouse to stop moving before appearing, supports animations, and [automatically stacks](/ui/z-index) above other content.
<Notice>Note that Tooltip doesn't render on native platforms.</Notice>
## Installation
Tooltip is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/tooltip
```
## Anatomy
```tsx
import { Tooltip } from 'tamagui' // or '@tamagui/tooltip'
export default () => (
<Tooltip>
<Tooltip.Trigger />
<Tooltip.Content>
<Tooltip.Arrow />
{/* ... */}
</Tooltip.Content>
</Tooltip>
)
```
## API Reference
### Tooltip
Contains every component for the tooltip.
<PropsTable
data={[
{
name: 'children',
type: 'React.ReactNode',
required: true,
description: `Must contain Popover.Content`,
},
{
name: 'groupId',
type: 'string',
required: false,
description: `If given, will work alongside TooltipGroup to ensure only one tooltip in the groups stays open.`,
},
{
name: 'restMs',
type: 'number',
required: false,
description: `Time needed for cursor to rest before showing.`,
},
{
name: 'delay',
type: `number | { open?: number; close?: number }`,
required: false,
description: `Maximum time before showing (can be set independently for open/close).`,
},
{
name: 'size',
type: 'SizeTokens',
required: false,
description: `Passes size down to all sub-components when set for padding, arrow, borderRadius.`,
},
{
name: 'placement',
type: 'Placement',
required: false,
description: `'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'right-start' | 'right-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end'`,
},
{
name: 'open',
type: 'boolean',
required: false,
description: `The controlled open state of the tooltip.`,
},
{
name: 'defaultOpen',
type: 'boolean',
required: false,
description: `The open state when initially rendered.`,
},
{
name: 'onOpenChange',
type: '(open: boolean) => void',
required: false,
description: `Event handler called when the open state changes.`,
},
{
name: 'modal',
type: 'boolean',
default: 'true',
required: false,
description: `Renders into root of app instead of inline.`,
},
{
name: 'stayInFrame',
type: 'ShiftProps | boolean',
required: false,
default: '{ padding: 10 }',
description: `Shifts the tooltip horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).`,
},
{
name: 'allowFlip',
type: 'FlipProps',
required: false,
description: `See floating-ui flip().`,
},
{
name: 'offset',
type: 'OffsetOptions',
required: false,
description: `Determines the distance the Popover appears from the target, see floating-ui offset().`,
},
{
name: 'zIndex',
type: 'number',
required: false,
description: `Sets the z-index of the tooltip portal. Only needed for global/scoped tooltips mounted at the root - inline tooltips automatically participate in z-index stacking. See the Scoping section below.`,
},
]}
/>
<Notice>
If using `modal={true}` (which is `true` by default), refer to the [PortalProvider
installation](#portalprovider) for more information.
</Notice>
### Tooltip.Trigger
Used to trigger opening of the popover when uncontrolled, see YStack in [Stacks](/docs/components/stacks).
### Tooltip.Content
Renders as SizableStack (see [Stacks](/docs/components/stacks)) with an extra `size` prop that accepts any `SizeTokens`.
### Tooltip.Anchor
Renders as YStack, see [Stacks](/docs/components/stacks).
When you want the Trigger to be in another location from where the Tooltip attaches, use Anchor. When used, Anchor is where the Tooltip will attach, while Trigger will open it.
### TooltipGroup
This allows you to logically group any tooltips rendered below this component. You can control their delay props, and components inside a TooltipGroup will be smart about opening quicker if you are moving between targets with tooltips, ensuring that subsequent tooltips show immediately rather than after a delay.
See the [Floating UI docs](https://floating-ui.com/docs/floatingdelaygroup) for full details on how this works.
<PropsTable
data={[
{
name: 'delay',
type: 'number | { open?: number; close?: number }',
default: 0,
required: false,
description: `The delay to use for the group.`,
},
{
name: 'timeoutMs',
type: 'number',
default: 0,
required: false,
description: `An optional timeout to use for the group, which represents when grouping logic will no longer be active after the close delay completes. Useful if you want grouping to last longer than the close delay, for example if there is no close delay at all.`,
},
{
name: 'preventAnimation',
type: 'boolean',
default: false,
required: false,
description: `Will disable the enter/exit animations while the group is active, but not for the initial enter animation of the first hovered tooltip.`,
},
]}
/>
### closeOpenTooltips
A small helper function of type `() => void` that will close any open tooltips.
## Scoping
Tooltip supports scoping which lets you mount a single Tooltip instance at
the root of your app, while having deeply nested Trigger components anywhere
in your tree attach to that single parent Tooltip.
This is useful for performance - you only render `Tooltip.Trigger` in your
list items or buttons, while the heavier `Tooltip` and `Tooltip.Content` live
at the root. It also lets you style all your tooltips consistently in one place.
<Notice>
When using a global scoped tooltip, you should set `zIndex` to ensure tooltips appear above dialogs and other portaled content. This is because the global tooltip's portal is mounted outside the stacking context of other portals. Inline tooltips (rendered alongside their trigger) automatically participate in z-index stacking and don't need this.
</Notice>
```tsx fileName=_layout.tsx
import { Tooltip } from 'tamagui'
export default ({ children }) => (
// zIndex needed for global tooltips to appear above dialogs
<Tooltip scope="tooltip" zIndex={1_000_000}>
<Tooltip.Content>
<Tooltip.Arrow />
{/* your tooltip content renders here */}
</Tooltip.Content>
{/* the rest of your app renders inside */}
{children}
</Tooltip>
)
```
```tsx fileName=MyButton.tsx
export default () => (
<Tooltip.Trigger scope="tooltip" aria-label="Settings">
<Button icon={Settings} />
</Tooltip.Trigger>
)
```
The `scope` prop on `Trigger` connects it to the `Tooltip` with the matching scope.
## components/unspaced/1.0.0
---
title: Unspaced
description: Avoids spacing for children inside a spacing container.
name: html
component: Unspaced
---
While the `space` property is deprecated, Unspaced lives on in a better form, one which is easily back-ported to add Unspaced support (as well as adding `gap` support).
When using the `space` style prop, you may want some children to not be spaced.
Use Unspaced:
```tsx
import { Text, Unspaced, View } from '@tamagui/core'
export default () => (
<View position="relative" gap="$4">
<View width={20} height={20} />
{/* space */}
<View width={20} height={20} />
{/* no */}
<Unspaced>
<Text position="absolute">Some absolute positioned text</Text>
</Unspaced>
</View>
)
```
If you want the item to be visually hidden as well as unspaced, see
[VisuallyHidden](/docs/components/visually-hidden), which uses Unspaced but also hides the contents in an accessible manner.
## components/unspaced/2.0.0
---
title: Unspaced
description: Avoids spacing for children inside a spacing container
name: unspaced
component: Unspaced
package: core
---
While the `space` property is deprecated, Unspaced lives on in a better form, one which is easily back-ported to add Unspaced support (as well as adding `gap` support).
When using the `space` style prop, you may want some children to not be spaced. Use Unspaced:
## Usage
```tsx
import { Text, Unspaced, View } from '@tamagui/core'
export default () => (
<View position="relative" space>
<View width={20} height={20} />
{/* space */}
<View width={20} height={20} />
{/* no */}
<Unspaced>
<Text position="absolute">Some absolute positioned text</Text>
</Unspaced>
</View>
)
```
If you want the item to be visually hidden as well as unspaced, see
[VisuallyHidden](/docs/components/visually-hidden), which uses Unspaced but also hides the contents in an accessible manner.
## components/visually-hidden/1.0.0
---
title: Visually Hidden
description: Hide content accessibly.
name: html
component: VisuallyHidden
---
VisuallyHidden hides an item but ensures it remains visible to accessibility readers.
<Highlights
features={[
'Keeps content hidden on screen but visible to assistive tech.',
'Works with "space" prop to not disturb spacing.',
]}
/>
## Installation
VisuallyHidden is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/visually-hidden
```
## Usage
Simply wrap the content you want hidden in VisuallyHidden:
```tsx
import { Text, VisuallyHidden } from 'tamagui'
export default () => (
<VisuallyHidden>
<Text>Add annotations here</Text>
</VisuallyHidden>
)
```
When using with the `space` property, it will avoid double-spacing:
```tsx
import { H1, Text, VisuallyHidden, YStack } from 'tamagui'
export default () => (
<YStack gap="$4">
<H1>Title</H1>
<VisuallyHidden>
<Text>Add annotations here</Text>
</VisuallyHidden>
</YStack>
)
```
## components/visually-hidden/2.0.0
---
title: Visually Hidden
description: Hide content accessibly
name: visually-hidden
component: VisuallyHidden
package: visually-hidden
---
VisuallyHidden hides an item but ensures it remains visible to accessibility readers.
<Highlights
features={[
'Keeps content hidden on screen but visible to assistive tech.',
'Works with "space" prop to not disturb spacing.',
]}
/>
## Installation
VisuallyHidden is already installed in `tamagui`, or you can install it independently:
```bash
npm install @tamagui/visually-hidden
```
## Usage
Simply wrap the content you want hidden in VisuallyHidden:
```tsx
import { Text, VisuallyHidden } from 'tamagui'
export default () => (
<VisuallyHidden>
<Text>Add annotations here</Text>
</VisuallyHidden>
)
```
When using with the `space` property, it will avoid double-spacing:
```tsx
import { H1, Text, VisuallyHidden, YStack } from 'tamagui'
export default () => (
<YStack space>
<H1>Title</H1>
<VisuallyHidden>
<Text>Add annotations here</Text>
</VisuallyHidden>
</YStack>
)
```
## components/z-index/2.0.0
---
title: Z-Index & Stacking
description: How Tamagui automatically stacks overlays and floating content
name: z-index
---
Tamagui includes an automatic stacking system that ensures overlays like dialogs, popovers, sheets, and tooltips layer correctly without manual z-index management.
## How It Works
When you open overlay components, Tamagui automatically assigns z-index values so that:
- **Later-opened content appears above earlier content** - Open two dialogs? The second one stacks above the first.
- **Nested content appears above its parent** - A tooltip inside a dialog automatically renders above the dialog.
- **Closing restores order** - When content closes, z-index values are reclaimed.
This means you typically don't need to think about z-index at all.
## When to Set zIndex
Only set `zIndex` when you need to override the automatic stacking. Common cases:
- **Fixed headers/footers** - If you have a sticky header at `z-index: 100`, overlays need to appear above it (they do by default).
- **Third-party libraries** - If integrating with libraries that use high z-index values.
- **Custom stacking requirements** - When you need specific layering that differs from open-order.
```tsx
// Only if you need to override automatic stacking
<Dialog zIndex={200000}>
...
</Dialog>
```
## Component Defaults
All overlay components use the automatic stacking system. Here's where to set `zIndex` if needed:
| Component | Where to set zIndex |
|-----------|---------------------|
| [Dialog](/ui/dialog) | `<Dialog zIndex={...}>` |
| [AlertDialog](/ui/alert-dialog) | `<AlertDialog zIndex={...}>` |
| [Sheet](/ui/sheet) | `<Sheet zIndex={...}>` |
| [Select](/ui/select) | `<Select zIndex={...}>` |
| [Popover](/ui/popover) | `<Popover zIndex={...}>` |
| [Tooltip](/ui/tooltip) | `<Tooltip zIndex={...}>` |
| [Menu](/ui/menu) | `<Menu.Portal zIndex={...}>` |
| [ContextMenu](/ui/context-menu) | `<ContextMenu.Portal zIndex={...}>` |
| [Toast](/ui/toast) | Via `ToastViewport` |
## Stacking Hierarchy
By default, components stack in this order (lowest to highest):
1. **Popover, Tooltip, Menu** - Pure auto-stacking, starts around z-index 5001
2. **Dialog, AlertDialog, Select, Sheet** - Use a higher base (100000+) to ensure they appear above most UI
Within each category, **mount order determines stacking** - open a popover, then another, the second is on top.
## Nested Stacking
Nested floating elements automatically stack above their parent:
```tsx
<Dialog>
<Dialog.Content>
{/* This tooltip will appear above the dialog */}
<Tooltip>
<Tooltip.Trigger>
<Button>Hover me</Button>
</Tooltip.Trigger>
<Tooltip.Content>
I appear above the dialog!
</Tooltip.Content>
</Tooltip>
</Dialog.Content>
</Dialog>
```
The system tracks parent-child relationships through React context, so nested portals always stack correctly.
## Technical Details
The stacking system uses two mechanisms:
### Horizontal Stacking (Siblings)
When multiple overlays open at the same level, each gets an incrementing z-index:
- First dialog: 105001
- Second dialog: 105002
- Third dialog: 105003
### Vertical Stacking (Nested)
When content opens inside another portal, it reads the parent's z-index from context and adds 1:
- Dialog opens at 105001
- Popover inside dialog: 105002
- Tooltip inside that popover: 105003
This ensures nested content is always visible above its parent, but sibling content opened later can still stack above.
## Portal Component
The [Portal](/ui/portal) component powers this stacking system. If building custom overlay components, you can use Portal's `stackZIndex` prop:
```tsx
import { Portal } from 'tamagui'
// Enable automatic stacking
<Portal stackZIndex>
{/* content */}
</Portal>
// Or use a high base value (like Dialog does)
<Portal stackZIndex={100000}>
{/* content */}
</Portal>
```
See the [Portal documentation](/ui/portal) for more details.
## core/animate-presence
---
title: AnimatePresence
description: Animate components as they mount and unmount
---
AnimatePresence animates direct children before they unmount. It is inspired by and forked from [Framer Motion](https://github.com/framer/motion), but works with any animation driver in Tamagui.
To use with `@tamagui/core`, install and import `@tamagui/animate-presence`. It's already bundled and exported from `tamagui`.
## Basic Usage
Use `enterStyle` and `exitStyle` to define how components animate in and out:
```tsx
import { AnimatePresence, View } from 'tamagui'
export const MyComponent = ({ isVisible }) => (
<AnimatePresence>
{isVisible && (
<View
key="my-square"
transition="bouncy"
backgroundColor="green"
size={50}
enterStyle={{
opacity: 0,
y: 10,
scale: 0.9,
}}
exitStyle={{
opacity: 0,
y: -10,
scale: 0.9,
}}
/>
)}
</AnimatePresence>
)
```
<Notice theme="blue">
Note you don't even need to set `opacity` on the base style. Tamagui knows to normalize
styles like opacity and scale to 1 (and y to 0) if it's not defined on the base styles
but is defined on `enterStyle` or `exitStyle`.
</Notice>
Wrap one or more Tamagui components with AnimatePresence. The child components will animate on enter and exit.
<Notice theme="blue">
Animated child components must each have a unique key prop so AnimatePresence can track
their presence in the tree.
</Notice>
## Enter/Exit Transitions
You can specify different animations for enter (mount) and exit (unmount) transitions. This is useful when you want elements to enter slowly but exit quickly, or vice versa:
```tsx
import { AnimatePresence, View } from 'tamagui'
export default ({ show }) => (
<AnimatePresence>
{show && (
<View
key="panel"
transition={{ enter: 'lazy', exit: 'quick' }}
enterStyle={{ opacity: 0, y: 20 }}
exitStyle={{ opacity: 0, y: -20 }}
/>
)}
</AnimatePresence>
)
```
The `enter` and `exit` keys accept any animation name from your config. You can also combine them with a `default` for property changes that happen while the element is mounted:
```tsx
// enter slowly, exit quickly, property changes use medium speed
<View
transition={{ enter: 'lazy', exit: 'quick', default: 'bouncy' }}
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
```
Or use enter/exit with the array syntax to include delay and per-property animations:
```tsx
// enter with lazy, exit with quick, delay 200ms, x uses its own animation
<View
transition={['bouncy', { enter: 'lazy', exit: 'quick', delay: 200, x: 'slow' }]}
enterStyle={{ opacity: 0, x: -100 }}
exitStyle={{ opacity: 0, x: 100 }}
/>
```
This works with all four animation drivers (CSS, React Native, Reanimated, Motion).
## The `custom` prop
<HeroContainer noPad>
<AnimationsPresenceDemo />
</HeroContainer>
```tsx hero template=AnimationsPresence
```
AnimatePresence takes a `custom` prop that allows you to update a variant of the animated child before it runs its exit animation. This is useful for animating a child out of the screen in a different direction before it unmounts, as shown in the example above:
```tsx
import { AnimatePresence } from '@tamagui/animate-presence'
import { ArrowLeft, ArrowRight } from '@tamagui/lucide-icons-2'
import { useState } from 'react'
import { Button, Image, XStack, YStack, styled } from 'tamagui'
const GalleryItem = styled(YStack, {
zIndex: 1,
x: 0,
opacity: 1,
fullscreen: true,
variants: {
// 1 = right, 0 = nowhere, -1 = left
going: {
':number': (going) => ({
enterStyle: {
x: going > 0 ? 1000 : -1000,
opacity: 0,
},
exitStyle: {
zIndex: 0,
x: going < 0 ? 1000 : -1000,
opacity: 0,
},
}),
},
} as const,
})
const photos = [
'https://picsum.photos/500/300',
'https://picsum.photos/501/300',
'https://picsum.photos/502/300',
]
const wrap = (min: number, max: number, v: number) => {
const rangeSize = max - min
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min
}
export function Demo() {
const [[page, going], setPage] = useState([0, 0])
const imageIndex = wrap(0, photos.length, page)
const paginate = (going: number) => {
setPage([page + going, going])
}
return (
<XStack
overflow="hidden"
backgroundColor="#000"
position="relative"
height={300}
width="100%"
alignItems="center"
>
<AnimatePresence initial={false} custom={{ going }}>
<GalleryItem key={page} transition="slowest" going={going}>
<Image src={photos[imageIndex]} width={500} height={300} />
</GalleryItem>
</AnimatePresence>
<Button
accessibilityLabel="Carousel left"
icon={ArrowLeft}
size="$5"
position="absolute"
left="$4"
circular
elevate
onPress={() => paginate(-1)}
zi={100}
/>
<Button
accessibilityLabel="Carousel right"
icon={ArrowRight}
size="$5"
position="absolute"
right="$4"
circular
elevate
onPress={() => paginate(1)}
zi={100}
/>
</XStack>
)
}
```
## API Reference
### AnimatePresence Props
- **`children`** - One or more Tamagui components with unique `key` props
- **`initial`** - If `false`, children entering on initial mount won't animate (default: `true`)
- **`custom`** - Pass data to children's variants for dynamic exit animations
- **`exitBeforeEnter`** - If `true`, only one child renders at a time (default: `false`)
- **`onExitComplete`** - Callback fired when all exiting children have finished animating
### Component Props
When used inside AnimatePresence:
- **`enterStyle`** - Styles to animate from when mounting
- **`exitStyle`** - Styles to animate to when unmounting
- **`transition`** - Animation configuration (can use `{ enter, exit }` syntax)
- **`key`** - Required unique identifier for AnimatePresence tracking
## See Also
- [Animations Overview](/docs/core/animations)
- [Animation Drivers](/docs/core/animation-drivers)
## core/animation-drivers
---
title: Animation Drivers
description: Choose and configure animation drivers for your platform
---
Tamagui supports four animation drivers, each with different strengths for different use cases:
<SimpleTable
headers={['Driver', 'Platform', 'Bundle Impact', 'Performance', 'Spring Physics']}
rows={[
['**CSS**', 'Web only', 'Lightest', 'Fast (CSS transitions)', 'No (easing only)'],
[
'**React Native**',
'Web + Native',
'Web: heavy (RNW), Native: none',
'On-thread',
'Yes (basic)',
],
[
'**Reanimated**',
'Web + Native',
'Larger',
'Off-thread (native), medium (web)',
'Yes',
],
['**Motion**', 'Web only', 'Medium', 'Off-thread (WAAPI)', 'Yes'],
]}
/>
You can swap drivers per-platform, or even dynamically load heavier drivers later in your app — for example, starting with the lightweight CSS driver on initial load and upgrading to Motion once the user is authenticated.
---
## Using Config v5
The easiest way to get started is with [`@tamagui/config/v5`](/docs/core/config-v5), which provides pre-configured animations for all four drivers with matching keys:
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css' // or v5-motion, v5-rn, v5-reanimated
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations,
})
```
The v5 presets include timing animations (`100ms`, `200ms`, etc.) and spring animations (`bouncy`, `quick`, `lazy`, etc.) that work identically across all drivers. See [Config v5](/docs/core/config-v5#choosing-an-animation-driver) for the full list.
---
## CSS Driver
The lightest bundle size, but lacks spring physics (easing curves only). Best for simple web-only apps.
### Installation
```bash
yarn add @tamagui/animations-css
```
### Configuration
```tsx
import { createAnimations } from '@tamagui/animations-css'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
fast: 'ease-in 150ms',
medium: 'ease-in 300ms',
slow: 'ease-in 450ms',
bouncy: 'cubic-bezier(0.68, -0.55, 0.265, 1.55) 300ms',
}),
// ...
})
```
The format is: `<easing-function> <duration>`
**Supported easing functions:** `ease`, `linear`, `ease-in`, `ease-out`, `ease-in-out`, `cubic-bezier(x1, y1, x2, y2)`
---
## React Native Driver
Uses React Native's built-in Animated API. No extra bundle on native, but requires React Native Web on the web side. Runs on-thread. Good for basic cross-platform animations.
### Installation
```bash
yarn add @tamagui/animations-react-native
```
### Configuration
```tsx
import { createAnimations } from '@tamagui/animations-react-native'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
fast: {
damping: 20,
mass: 1.2,
stiffness: 250,
},
medium: {
damping: 10,
mass: 0.9,
stiffness: 100,
},
slow: {
damping: 20,
stiffness: 60,
},
}),
// ...
})
```
**Spring parameters:**
- `damping` - How quickly the spring settles (higher = less bouncy)
- `mass` - Mass of the object (higher = more inertia)
- `stiffness` - Spring stiffness coefficient (higher = faster)
---
## Reanimated Driver
Supports both native and web with advanced features, but has a larger bundle size. Best performance on native platforms with off-thread animations.
### Installation
```bash
yarn add @tamagui/animations-reanimated react-native-reanimated
```
Follow the [Reanimated installation guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/) to complete setup.
### Configuration
```tsx
import { createAnimations } from '@tamagui/animations-reanimated'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
fast: {
type: 'spring',
damping: 20,
mass: 1.2,
stiffness: 250,
},
medium: {
type: 'spring',
damping: 10,
mass: 0.9,
stiffness: 100,
},
// timing animations also supported
quick: {
type: 'timing',
duration: 300,
},
}),
// ...
})
```
---
## Motion Driver
Off-thread performance via WAAPI with excellent spring physics and a medium bundle size. Best for web-only apps that need smooth, physics-based animations.
### Installation
```bash
yarn add @tamagui/animations-motion motion
```
### Configuration
```tsx
import { createAnimations } from '@tamagui/animations-motion'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
'100ms': { duration: 100 },
'200ms': { duration: 200 },
bouncy: {
type: 'spring',
damping: 9,
mass: 0.9,
stiffness: 150,
},
quick: {
type: 'spring',
damping: 20,
mass: 1.2,
stiffness: 250,
},
}),
// ...
})
```
Motion uses the Web Animations API (WAAPI) which runs animations on the compositor thread when possible.
---
## Swapping Drivers
Tamagui provides several ways to use different drivers — at build time per platform, at runtime in specific parts of your app, or dynamically loaded after initial render.
### Per-Platform with File Extensions
Use `.native.ts` and `.ts` file extensions to bundle different drivers per platform:
```tsx fileName="animations.ts"
// web
import { createAnimations } from '@tamagui/animations-motion'
export const animations = createAnimations({
bouncy: { type: 'spring', damping: 10, stiffness: 100 },
})
```
```tsx fileName="animations.native.ts"
// native
import { createAnimations } from '@tamagui/animations-reanimated'
export const animations = createAnimations({
bouncy: { type: 'spring', damping: 10, stiffness: 100 },
})
```
```tsx fileName="tamagui.config.ts"
import { animations } from './animations'
export default createTamagui({
animations,
// ...
})
```
### Dynamic with Configuration
Use the `<Configuration>` component to swap the animation driver for a subtree. This is useful for lazy loading heavier drivers only when needed — for example, keeping initial page load fast with CSS animations, then upgrading to Motion for authenticated users:
```tsx fileName="app/_layout.tsx"
import { Configuration, Slot } from 'tamagui'
import { createAnimations } from '@tamagui/animations-motion'
const motionDriver = createAnimations({
bouncy: { type: 'spring', damping: 10, stiffness: 100 },
})
export function AuthenticatedLayout() {
return (
<Configuration animationDriver={motionDriver}>
<Slot />
</Configuration>
)
}
```
This pattern works well with route-based code splitting — the Motion driver is only loaded when the user navigates to an authenticated route.
### Multiple Drivers with animatedBy
Configure multiple drivers at the root and select per-component:
```tsx fileName="tamagui.config.ts"
import { createAnimations as createCSS } from '@tamagui/animations-css'
import { createAnimations as createSpring } from '@tamagui/animations-motion'
export default createTamagui({
animations: {
default: createCSS({ bouncy: 'ease-in 200ms' }),
spring: createSpring({ bouncy: { type: 'spring', damping: 10 } }),
},
})
```
```tsx
<Square transition="bouncy" /> {/* uses default (CSS) */}
<Square animatedBy="spring" transition="bouncy" /> {/* uses spring (Motion) */}
```
### Runtime Loading with loadAnimationDriver
Add drivers at runtime after app initialization:
```tsx
import { loadAnimationDriver } from 'tamagui'
import { createAnimations } from '@tamagui/animations-motion'
// call this after some condition (user auth, route change, etc.)
const driver = createAnimations({ bouncy: { type: 'spring', damping: 10 } })
loadAnimationDriver('spring', driver)
// now components can use animatedBy="spring"
```
---
## See Also
- [Animations Overview](/docs/core/animations)
- [AnimatePresence](/docs/core/animate-presence)
- [Config v5](/docs/core/config-v5#choosing-an-animation-driver)
## core/animations-css
---
title: CSS Driver
description: Lightweight CSS transition-based animations
---
The `@tamagui/animations-css` package works with the Tamagui compiler and
runtime to provide simple ways to share typed animations across all your
components.
## Installation
```bash
yarn add @tamagui/animations-css
```
## Configuration
Add it to your [Tamagui config](/docs/core/configuration):
```tsx
import { createAnimations } from '@tamagui/animations-css'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
fast: 'ease-in 150ms',
medium: 'ease-in 300ms',
slow: 'ease-in 450ms',
}),
// ...
})
```
## How it Works
At runtime, the plugin does very little except to set the `transition` property
in CSS. At compile-time, the compiler does the same, ensuring you get all the
benefits of prop removal and view flattening even when using animations.
## Animation Configuration
CSS animations use CSS transition syntax:
```tsx
createAnimations({
quick: 'ease-out 100ms',
bouncy: 'cubic-bezier(0.68, -0.55, 0.265, 1.55) 300ms',
slow: 'ease-in-out 500ms',
})
```
The format is: `<easing-function> <duration>`
### Delay
You can add animation delays using the array syntax:
```tsx
<Square transition={['quick', { delay: 200 }]} />
```
The delay (in milliseconds) is applied as CSS `transition-delay`.
### Supported Easing Functions
- `ease` - Default easing (equivalent to `cubic-bezier(0.25, 0.1, 0.25, 1)`)
- `linear` - No easing, constant speed
- `ease-in` - Slow start
- `ease-out` - Slow end
- `ease-in-out` - Slow start and end
- `cubic-bezier(x1, y1, x2, y2)` - Custom cubic bezier curve
## When to Use
**Advantages:**
- **Smallest bundle size** - Minimal JavaScript overhead
- **Best compatibility** - Works everywhere CSS works
- **Compiler optimizations** - Benefits from static extraction
- **Simple** - Easiest to understand and configure
**Limitations:**
- No spring physics (only easing curves)
- Limited to CSS animatable properties
- Less control over animation lifecycle
## Example
```tsx
import { createAnimations } from '@tamagui/animations-css'
import { YStack, createTamagui } from 'tamagui'
const animations = createAnimations({
quick: 'ease-out 150ms',
bouncy: 'cubic-bezier(0.68, -0.55, 0.265, 1.55) 400ms',
})
export default createTamagui({
animations,
// ... rest of config
})
// Usage in components
export const MyComponent = () => (
<YStack
transition="quick"
hoverStyle={{
scale: 1.1,
}}
/>
)
```
## See Also
- [Animations Introduction](/docs/core/animations)
- [React Native Driver](/docs/core/animations-react-native)
- [Motion Driver](/docs/core/animations-motion)
- [Reanimated Driver](/docs/core/animations-reanimated)
## core/animations-motion
---
title: Motion Driver
description: High-performance web animations with WAAPI
---
[Motion](https://motion.dev) is a modern animation library built on the Web
Animations API (WAAPI). It features a unique hybrid engine that combines
JavaScript animations with native browser APIs to deliver high-performance,
GPU-accelerated animations.
## Installation
```bash
yarn add @tamagui/animations-motion motion
```
## Configuration
Add your animations to your [Tamagui config](/docs/core/configuration):
```tsx
import { createAnimations } from '@tamagui/animations-motion'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
'75ms': { duration: 75 },
'100ms': { duration: 100 },
'200ms': { duration: 200 },
superBouncy: {
type: 'spring',
damping: 5,
mass: 0.7,
stiffness: 200,
},
bouncy: {
type: 'spring',
damping: 9,
mass: 0.9,
stiffness: 150,
},
}),
// ...
})
```
## How it Works
At runtime, the Motion driver uses the `useAnimate()` hook from the Motion
library to create animation scopes and orchestrate animations. It intelligently
batches style changes and only animates properties that have changed, optimizing
performance.
The driver leverages the Web Animations API (WAAPI) which runs animations on the
compositor thread when possible, keeping animations smooth even when the main
thread is busy.
## Animation Configuration
Motion animations support both spring and tween (timing) animations:
### Spring Animations
```tsx
{
type: 'spring',
damping: 10, // Higher = less bouncy
mass: 0.9, // Higher = more inertia
stiffness: 100, // Higher = faster
}
```
### Tween Animations
```tsx
{
duration: 200, // Duration in milliseconds
}
```
### Delay
You can add animation delays using the array syntax:
```tsx
<Square transition={['bouncy', { delay: 200 }]} />
```
The delay is specified in milliseconds and is converted to seconds for Motion
internally.
## Highlights
### Zero Re-renders
The Motion driver now works with Tamagui to bypasses all internal style changes
to instead directly hand off directly to Motion.
### WAAPI Performance
The Web Animations API runs animations on the compositor thread when possible,
keeping animations smooth even when the main thread is busy. This provides
off-thread performance comparable to Reanimated, but for web-only applications.
## Platform-Specific Configuration
For cross-platform apps, use Motion on web and Reanimated on native:
```tsx
// animations.ts (web)
import { createAnimations } from '@tamagui/animations-motion'
export const animations = createAnimations({
bouncy: {
type: 'spring',
damping: 10,
stiffness: 100,
},
})
```
```tsx
// animations.native.ts
import { createAnimations } from '@tamagui/animations-reanimated'
export const animations = createAnimations({
bouncy: {
type: 'spring',
damping: 10,
stiffness: 100,
},
})
```
Then import without the extension:
```tsx
import { animations } from './animations'
```
Your bundler will automatically pick the right file based on the platform.
## See Also
- [Animations Introduction](/docs/core/animations)
- [CSS Driver](/docs/core/animations-css)
- [React Native Driver](/docs/core/animations-react-native)
- [Reanimated Driver](/docs/core/animations-reanimated)
- [Motion Documentation](https://motion.dev)
## core/animations-react-native
---
title: React Native Driver
description: Spring-based animations using React Native Animated
---
[React Native's Animated library](https://reactnative.dev/docs/animated) is the
animation library that comes built into React Native and React Native Web.
## Installation
```bash
yarn add @tamagui/animations-react-native
```
## Configuration
Add it to your [Tamagui config](/docs/core/configuration):
```tsx
import { createAnimations } from '@tamagui/animations-react-native'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
fast: {
damping: 20,
mass: 1.2,
stiffness: 250,
},
medium: {
damping: 10,
mass: 0.9,
stiffness: 100,
},
slow: {
damping: 20,
stiffness: 60,
},
}),
// ...
})
```
## Animation Configuration
React Native animations use spring physics with these parameters:
### Spring Parameters
- **`damping`** - How quickly the spring settles. Higher = less bouncy
- **`mass`** - Mass of the object attached to the spring. Higher = more inertia
- **`stiffness`** - Spring stiffness coefficient. Higher = faster animation
```tsx
createAnimations({
bouncy: {
damping: 10,
mass: 0.9,
stiffness: 100,
},
quick: {
damping: 20,
mass: 1.2,
stiffness: 250,
},
})
```
### Delay
You can add animation delays using the array syntax:
```tsx
<Square transition={['bouncy', { delay: 200 }]} />
```
The delay is specified in milliseconds and uses `Animated.delay()` internally to
sequence animations.
## When to Use
**Advantages:**
- **Cross-platform** - Works on web and React Native
- **Built-in** - No additional animation library needed
- **Spring physics** - Natural, physics-based motion
- **Mature** - Battle-tested in React Native ecosystem
**Limitations:**
- Larger bundle than CSS driver
- Less advanced than Reanimated
- Web-only apps might prefer lighter alternatives
## Platform-Specific Configuration
You can use different drivers on web vs native using platform-specific file
extensions:
```tsx
// animations.ts (web)
import { createAnimations } from '@tamagui/animations-css'
export const animations = createAnimations({
quick: 'ease-out 150ms',
})
```
```tsx
// animations.native.ts (native)
import { createAnimations } from '@tamagui/animations-react-native'
export const animations = createAnimations({
quick: {
damping: 20,
stiffness: 250,
},
})
```
Then import without the extension:
```tsx
import { animations } from './animations'
```
Your bundler will automatically pick the right file based on the platform.
## Example
```tsx
import { createAnimations } from '@tamagui/animations-react-native'
import { YStack, createTamagui } from 'tamagui'
const animations = createAnimations({
bouncy: {
damping: 10,
mass: 0.9,
stiffness: 100,
},
quick: {
damping: 20,
mass: 1.2,
stiffness: 250,
},
})
export default createTamagui({
animations,
// ... rest of config
})
// Usage in components
export const MyComponent = () => (
<YStack
transition="bouncy"
enterStyle={{
opacity: 0,
scale: 0.9,
}}
/>
)
```
## See Also
- [Animations Introduction](/docs/core/animations)
- [CSS Driver](/docs/core/animations-css)
- [Motion Driver](/docs/core/animations-motion)
- [Reanimated Driver](/docs/core/animations-reanimated)
## core/animations-reanimated
---
title: Reanimated Driver
description: Advanced off-thread animations with Reanimated
---
[Reanimated](https://docs.swmansion.com/react-native-reanimated/) is an
animation library that targets React Native and React Native Web. It runs
animations off-thread and provides smooth spring and timing animations.
## Installation
```bash
yarn add @tamagui/animations-reanimated react-native-reanimated
```
Follow the
[Reanimated installation guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/)
to complete setup for your platform.
## Configuration
Add your animations to your [Tamagui config](/docs/core/configuration):
```tsx
import { createAnimations } from '@tamagui/animations-reanimated'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
fast: {
type: 'spring',
damping: 20,
mass: 1.2,
stiffness: 250,
},
medium: {
type: 'spring',
damping: 10,
mass: 0.9,
stiffness: 100,
},
slow: {
type: 'spring',
damping: 20,
stiffness: 60,
},
}),
// ...
})
```
## How it Works
At runtime, this driver parses animatable style properties and hands them over
to Reanimated using worklets. Animations run on the UI thread for smooth 60fps
performance.
## Animation Configuration
Reanimated animations support both spring and timing (tween) animations:
### Spring Animations
```tsx
{
type: 'spring',
damping: 10, // Higher = less bouncy
mass: 0.9, // Higher = more inertia
stiffness: 100, // Higher = faster
}
```
### Timing Animations
```tsx
{
type: 'timing',
duration: 300, // Duration in milliseconds
}
```
### Delay
You can add animation delays using the array syntax:
```tsx
<Square transition={['bouncy', { delay: 200 }]} />
```
The delay is specified in milliseconds.
## When to Use
**Advantages:**
- **Off-thread animations** - Runs on UI thread for 60fps performance
- **Cross-platform** - Works on web and React Native
- **Advanced features** - Worklets, gesture integration, shared values
- **React Native optimized** - Best performance on native platforms
- **Powerful** - Most feature-rich animation driver
**Considerations:**
- Larger bundle size
- Requires Reanimated setup and configuration
- More complex than simpler drivers
- Web-only apps might prefer Motion or CSS drivers
## Platform-Specific Configuration
You can use Reanimated on native and a lighter driver on web:
```tsx
// animations.native.ts
import { createAnimations } from '@tamagui/animations-reanimated'
export const animations = createAnimations({
bouncy: {
type: 'spring',
damping: 10,
stiffness: 100,
},
})
```
```tsx
// animations.ts (web)
import { createAnimations } from '@tamagui/animations-motion'
export const animations = createAnimations({
bouncy: {
type: 'spring',
damping: 10,
stiffness: 100,
},
})
```
Then import without the extension:
```tsx
import { animations } from './animations'
```
## Example
```tsx
import { createAnimations } from '@tamagui/animations-reanimated'
import { YStack, createTamagui } from 'tamagui'
const animations = createAnimations({
bouncy: {
type: 'spring',
damping: 10,
mass: 0.9,
stiffness: 100,
},
quick: {
type: 'spring',
damping: 20,
mass: 1.2,
stiffness: 250,
},
})
export default createTamagui({
animations,
// ... rest of config
})
// Usage in components
export const MyComponent = () => (
<YStack
transition="bouncy"
enterStyle={{
opacity: 0,
scale: 0.9,
}}
/>
)
```
## Zero Re-renders
The Reanimated driver now works with Tamagui to bypasses all internal style
changes to instead directly hand off to Reanimated worklets.
## Advanced Features
Reanimated provides advanced features:
- **Gesture integration** - Animate based on gesture values
- **Shared values** - Share animated values between components
- **Worklets** - JavaScript functions that run on the UI thread
- **Custom animations** - Full control over animation curves
See the
[Reanimated documentation](https://docs.swmansion.com/react-native-reanimated/)
for more details.
## See Also
- [Animations Introduction](/docs/core/animations)
- [CSS Driver](/docs/core/animations-css)
- [React Native Driver](/docs/core/animations-react-native)
- [Motion Driver](/docs/core/animations-motion)
- [Reanimated Documentation](https://docs.swmansion.com/react-native-reanimated/)
## core/animations
---
title: Animations
description: Swap out animation drivers per-platform or at runtime
---
<HeroContainer>
<AnimationsDemo />
</HeroContainer>
```tsx hero template=Animations
```
<Highlights
disableLinks
features={[
`Animate any style prop with animation config per-prop.`,
`Can animate across all states (media queries, hover, etc).`,
`Multiple drivers you can swap out with type safety.`,
`SSR safe mount animations.`,
`Enter and exit animations with AnimatePresence.`,
]}
/>
Add animations to Tamagui with an animation driver. Animation drivers are designed to be swappable, so you can use lightweight CSS animations or other web-focused animation libraries on the web, while using larger but more advanced libraries like Reanimated on native - all without having to change a line outside of configuration.
For this guide, we'll use the React Native driver as an example, but you can choose from several [Animation Drivers](/docs/core/animation-drivers). For mount and unmount animations, see [AnimatePresence](/docs/core/animate-presence).
## Installation
```bash
yarn add @tamagui/animations-react-native
```
Then add it to your [Tamagui config](/docs/core/configuration):
```tsx
import { createAnimations } from '@tamagui/animations-react-native'
import { createTamagui } from 'tamagui'
export default createTamagui({
animations: createAnimations({
bouncy: {
damping: 10,
mass: 0.9,
stiffness: 100,
},
lazy: {
damping: 18,
stiffness: 50,
},
quick: {
damping: 20,
mass: 1.2,
stiffness: 250,
},
}),
// ...
})
```
## Usage
The `transition` prop accepts the name of an animation you've configured. By default, animations will
apply to all animatable styles, similar to setting `all` in a CSS `transition`.
Here's an example animating `hoverStyle`:
<HeroContainer>
<AnimationsHoverDemo />
</HeroContainer>
```tsx hero template=AnimationsHover
```
### The transition prop rules
If you add a `transition` prop, you must always keep the prop. If you need the animation to be disabled,
pass false, null or even undefined if it suits you.
The spring-based animation drivers have expensive hooks that would degrade runtime
performance if present on every component. As a workaround, the animation hooks
are called conditionally based on whether the `transition` key is present in the props object.
So, `<Square transition={isActive ? 'bouncy' : null} />` rather than
`<Square {...isActive && { transition: 'bouncy' }} />`.
If you'd like to remove or add a transition prop after a component has already rendered, you'd have
to change the key.
### enterStyle
Setting `enterStyle` on a component tells it to start with those styles,
then animate to the base styles after mount:
<HeroContainer>
<AnimationsEnterDemo />
</HeroContainer>
```tsx hero template=AnimationsEnter
```
### Granular animations
The `transition` prop accepts a string or a more complex object to customize
animations per-property.
You can specify animations for specific properties using an object:
```tsx
import { YStack } from 'tamagui'
export default () => (
<YStack
transition={{
// only x and y will apply animations
x: 'bouncy',
y: {
type: 'bouncy',
overshootClamping: true,
},
}}
/>
)
```
Note that values can either map to `AnimationKey` as a string, or to
`{ type: AnimationKey, ...configuration }`
You can set a default animation using a two-element array with the default in the
first position:
```tsx
import { YStack } from 'tamagui'
export default () => (
<YStack
transition={[
// all attributes get "bouncy"
'bouncy',
// these are customized
{
y: 'slow',
scale: {
type: 'fast',
repeat: 2,
},
},
]}
/>
)
```
### Delay
You can add a delay before animations start using the array syntax with a `delay` property (in milliseconds):
```tsx
import { Square, XStack } from 'tamagui'
export default () => (
<XStack gap="$2">
{[0, 1, 2, 3].map((i) => (
<Square
key={i}
transition={['bouncy', { delay: i * 100 }]}
enterStyle={{ opacity: 0, scale: 0.5, y: 20 }}
size={50}
bg="$color10"
/>
))}
</XStack>
)
```
This creates a staggered animation effect where each square animates 100ms after the previous one. The delay works with all animation drivers and applies to enter, exit, and style change animations.
### Enter/Exit Transitions
You can specify different animations for enter (mount) and exit (unmount) transitions. This is useful when you want elements to enter slowly but exit quickly, or vice versa:
```tsx
import { AnimatePresence, View } from 'tamagui'
export default ({ show }) => (
<AnimatePresence>
{show && (
<View
key="panel"
transition={{ enter: 'lazy', exit: 'quick' }}
enterStyle={{ opacity: 0, y: 20 }}
exitStyle={{ opacity: 0, y: -20 }}
/>
)}
</AnimatePresence>
)
```
The `enter` and `exit` keys accept any animation name from your config. You can also combine them with a `default` for property changes that happen while the element is mounted:
```tsx
// enter slowly, exit quickly, property changes use medium speed
<View
transition={{ enter: 'lazy', exit: 'quick', default: 'bouncy' }}
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
```
Or use enter/exit with the array syntax to include delay and per-property animations:
```tsx
// enter with lazy, exit with quick, delay 200ms, x uses its own animation
<View
transition={['bouncy', { enter: 'lazy', exit: 'quick', delay: 200, x: 'slow' }]}
enterStyle={{ opacity: 0, x: -100 }}
exitStyle={{ opacity: 0, x: 100 }}
/>
```
This works with all four animation drivers (CSS, React Native, Reanimated, Motion).
### Pseudo-Style Transitions
You can specify different transitions for entering and exiting pseudo states like hover, press, and focus. This follows CSS-like semantics where entering a state uses that state's transition, and exiting uses the base transition:
```tsx
import { Square } from 'tamagui'
export default () => (
<Square
size={100}
bg="$color5"
transition="1000ms" // slow exit (returning to base)
hoverStyle={{
transition: '200ms', // fast enter (going to hover)
bg: '$color10',
}}
/>
)
```
In this example:
- **Hover enter**: Uses the 200ms transition (snaps quickly to hover state)
- **Hover exit**: Uses the 1000ms base transition (fades slowly back)
This works with all pseudo styles:
```tsx
<View
transition="slow"
hoverStyle={{
transition: 'quick',
scale: 1.1,
}}
pressStyle={{
transition: 'bouncy',
scale: 0.95,
}}
focusStyle={{
transition: 'quick',
borderColor: '$blue10',
}}
/>
```
And with group pseudo states:
```tsx
<View group="card">
<Square
size={50}
bg="$color5"
transition="1000ms"
$group-card-hover={{
transition: '200ms',
bg: '$color10',
}}
/>
</View>
```
Priority follows CSS specificity intuition: press > hover > focus. This works with all animation drivers.
### animateOnly
The `animateOnly` prop will limit your animation config to certain keys. It
accepts an array of strings that correspond to style property names.
## What to know when animating
### Conditional animations and HMR
The animation hooks are heavy, which initially meant we either had to choose
great performance or animations. We settled on a trade-off: we track if the
`transition` prop is set, and if so, we enable the hook. If it is ever set, even
just once, then the hooks will continue to run for the remainder of the
component lifecycle. This means if you ever plan to animate a component you
should keep `transition` always set on the component props. You can disable it
like so:
```tsx
<View transition={condition ? 'animation-name' : null} />
```
Note that because of this constraint, you may see an error if you add the
`transition` prop to a component in dev mode during HMR. Often just saving once more will remove the error, or reload the page if needed.
## See Also
- [AnimatePresence](/docs/core/animate-presence) - Mount and unmount animations
- [Animation Drivers](/docs/core/animation-drivers) - Choose and configure animation drivers
## core/config-v4
---
title: Quick Config
description: Easy config and themes with @tamagui/config/v4
---
<IntroParagraph>
Config v4 simplifies configuration, generates much more useful themes, and adds a new
helper function for generating your own theme suite called `createThemes`.
</IntroParagraph>
Tamagui is incredibly customizable, but if you want to get started faster and
enjoy using a configuration that is standardized and has been tested in many
apps, we recommend our `@tamagui/config` package.
First you'll want to add it:
```bash
npm install @tamagui/config
```
Using the new `defaultConfig` is the easiest way to start:
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v4'
import { createTamagui } from 'tamagui'
export const config = createTamagui(defaultConfig)
type CustomConfig = typeof config
// ensure types work
declare module 'tamagui' {
interface TamaguiCustomConfig extends CustomConfig {}
}
```
It gives you a robust suite of themes, a few shorthands that mostly align with
Tailwind, and default settings that prevent bugs.
If you like it but want to make some changes, you can
[copy and paste the source code](https://github.com/tamagui/tamagui/blob/5b0c075e92688ac85737fe97c98cd77322f034b0/code/core/config/src/v4.ts)
into your app and customize to your taste.
<Notice theme="green" title="Custom Themes">
If you want to learn to create your own custom theme suite rather than use the included
defaults, check out the [Creating Custom Themes Guide](/docs/guides/theme-builder).
</Notice>
We recommend adding `settings.styleCompat` to the default config, setting it to
be "react-native". This will align the default flexBasis of `flex` to be `0`,
not `auto`, matching the default in React Native. This setting will become the
default in the upcoming Tamagui v2 release. We left it out of Config v4 because
we wanted an easier transition for existing users, but if you are a new user, we
recommend this setting.
---
### Default Configuration
You can take or leave as much of the default configuration as you please. Each
part is exported separately, or all together as `defaultConfig`.
#### Themes
Config v4 has a new helper function `createThemes`, an opinionated way to
generate a suite of themes. It also comes with default themes that include
minimal grayscale light and dark themes, and an inverted "accent" theme.
It's easiest to get a feel for both createThemes and the default themes by reading
the source for how `defaultThemes` are defined:
```tsx fileName=themes.ts
import * as Colors from '@tamagui/colors'
import { createThemes, defaultComponentThemes } from '@tamagui/config/v4'
const darkPalette = [
'#050505',
'#151515',
'#191919',
'#232323',
'#282828',
'#323232',
'#424242',
'#494949',
'#545454',
'#626262',
'#a5a5a5',
'#fff',
]
const lightPalette = [
'#fff',
'#f8f8f8',
'hsl(0, 0%, 96.3%)',
'hsl(0, 0%, 94.1%)',
'hsl(0, 0%, 92.0%)',
'hsl(0, 0%, 90.0%)',
'hsl(0, 0%, 88.5%)',
'hsl(0, 0%, 81.0%)',
'hsl(0, 0%, 56.1%)',
'hsl(0, 0%, 50.3%)',
'hsl(0, 0%, 42.5%)',
'hsl(0, 0%, 9.0%)',
]
const lightShadows = {
shadow1: 'rgba(0,0,0,0.04)',
shadow2: 'rgba(0,0,0,0.08)',
shadow3: 'rgba(0,0,0,0.16)',
shadow4: 'rgba(0,0,0,0.24)',
shadow5: 'rgba(0,0,0,0.32)',
shadow6: 'rgba(0,0,0,0.4)',
}
const darkShadows = {
shadow1: 'rgba(0,0,0,0.2)',
shadow2: 'rgba(0,0,0,0.3)',
shadow3: 'rgba(0,0,0,0.4)',
shadow4: 'rgba(0,0,0,0.5)',
shadow5: 'rgba(0,0,0,0.6)',
shadow6: 'rgba(0,0,0,0.7)',
}
const extraColors = {
black1: darkPalette[0],
black2: darkPalette[1],
black3: darkPalette[2],
black4: darkPalette[3],
black5: darkPalette[4],
black6: darkPalette[5],
black7: darkPalette[6],
black8: darkPalette[7],
black9: darkPalette[8],
black10: darkPalette[9],
black11: darkPalette[10],
black12: darkPalette[11],
white1: lightPalette[0],
white2: lightPalette[1],
white3: lightPalette[2],
white4: lightPalette[3],
white5: lightPalette[4],
white6: lightPalette[5],
white7: lightPalette[6],
white8: lightPalette[7],
white9: lightPalette[8],
white10: lightPalette[9],
white11: lightPalette[10],
white12: lightPalette[11],
}
const generatedThemes = createThemes({
componentThemes: defaultComponentThemes,
base: {
palette: {
dark: darkPalette,
light: lightPalette,
},
// for values we don't want being inherited onto sub-themes
extra: {
light: {
...Colors.blue,
...Colors.green,
...Colors.red,
...Colors.yellow,
...lightShadows,
...extraColors,
shadowColor: lightShadows.shadow1,
},
dark: {
...Colors.blueDark,
...Colors.greenDark,
...Colors.redDark,
...Colors.yellowDark,
...darkShadows,
...extraColors,
shadowColor: darkShadows.shadow1,
},
},
},
// inverse accent theme
accent: {
palette: {
dark: lightPalette,
light: darkPalette,
},
},
childrenThemes: {
blue: {
palette: {
dark: Object.values(Colors.blueDark),
light: Object.values(Colors.blue),
},
},
red: {
palette: {
dark: Object.values(Colors.redDark),
light: Object.values(Colors.red),
},
},
yellow: {
palette: {
dark: Object.values(Colors.yellowDark),
light: Object.values(Colors.yellow),
},
},
green: {
palette: {
dark: Object.values(Colors.greenDark),
light: Object.values(Colors.green),
},
},
},
})
export type TamaguiThemes = typeof generatedThemes
/**
* This is an optional production optimization: themes JS can get to 20Kb or more.
* Tamagui has ~1Kb of logic to hydrate themes from CSS, so you can remove the JS.
* So long as you server render your Tamagui CSS, this will save you bundle size:
*/
export const themes: TamaguiThemes =
// we define this in our bundler plugins, but you can also use your framework/bundlers
// built in environment variables to ensure this is shaken away on client js bundles
process.env.TAMAGUI_ENVIRONMENT === 'client' && process.env.NODE_ENV === 'production'
? {}
: (generatedThemes as any)
```
So what have we done?
- `base` will always generate `light` and `dark` as your root themes. We plan to
add support for generating just one base theme, but you can manually remove
either light or dark if you please.
- Our `base` themes are grayscale, defined by their palettes.
- The `accent` option is an opinionated way to have a "contrasting theme". When
set, it generates a few things:
- `light_accent` and `dark_accent` themes
- `$accent1` through `$accent12` on the base themes
- `childrenThemes` allows us to set up some sub-themes easily, this is
especially useful for when you want to make an area look like an error,
success, or warning state. That's why we default to setting `green`, `yellow`,
and `red`. Of course, you can swap these out for whatever you like.
- We also are setting `componentThemes` to `defaultComponentThemes`, which will
generate for us default styling for the Tamagui UI components. If you prefer
to not have default component themes, you can leave this out.
- You can use the `getTheme` callback to customize any generated theme. See the
[theme-builder guide](/docs/guides/theme-builder#customizing-themes-with-gettheme)
for details.
#### Settings
<PropsTable
data={[
{ name: 'mediaQueryDefaultActive', description: 'See Media' },
{ name: 'defaultFont', description: '"body"' },
{
name: 'fastSchemeChange',
description:
'true - On iOS, leverages DynamicColorIOS for fast light/dark theme changes.',
},
{
name: 'shouldAddPrefersColorThemes',
description:
'true - Generates CSS for prefers-color-scheme, so even with JS off you have proper light/dark styling.',
},
{
name: 'allowedStyleValues',
description:
'"somewhat-strict-web" - More strict than just a string, but still allows web-only style values like vh, vw.',
},
{
name: 'themeClassNameOnRoot',
description:
'true - Defaults to putting your root theme className onto the root html tag so you can use your CSS variables on your body tag.',
},
{
name: 'onlyAllowShorthands',
description: 'true - For any shorthand defined, remove the types for the longhand.',
},
]}
/>
#### Media
The new media queries in v4 are also more simplified and aligned to Tailwind:
<PropsTable
data={[
{ name: '2xl', description: `minWidth: 1536` },
{ name: 'xl', description: `minWidth: 1280` },
{ name: 'lg', description: `minWidth: 1024` },
{ name: 'md', description: `minWidth: 768` },
{ name: 'sm', description: `minWidth: 640` },
{ name: 'xs', description: `minWidth: 460` },
{ name: '2xs', description: `minWidth: 340` },
]}
/>
On the server, only xs and 2xs default to `true` to align generally give you a
[mobile-first design](https://en.wikipedia.org/wiki/Responsive_web_design)
setup, but of course you can change that. This only really affects `useMedia()`,
as Tamagui generates proper SSR-safe CSS for media queries on the server for any
style properties.
#### Shorthands
Config v4 has fewer shorthands, and aligns shorthands to Tailwind for
familiarity:
<PropsTable
data={[
{ name: 'text', description: 'textAlign' },
{ name: 'b', description: 'bottom' },
{ name: 'bg', description: 'backgroundColor' },
{ name: 'content', description: 'alignContent' },
{ name: 'flex', description: 'flexDirection' },
{ name: 'grow', description: 'flexGrow' },
{ name: 'items', description: 'alignItems' },
{ name: 'justify', description: 'justifyContent' },
{ name: 'l', description: 'left' },
{ name: 'm', description: 'margin' },
{ name: 'maxH', description: 'maxHeight' },
{ name: 'maxW', description: 'maxWidth' },
{ name: 'mb', description: 'marginBottom' },
{ name: 'minH', description: 'minHeight' },
{ name: 'minW', description: 'minWidth' },
{ name: 'ml', description: 'marginLeft' },
{ name: 'mr', description: 'marginRight' },
{ name: 'mt', description: 'marginTop' },
{ name: 'mx', description: 'marginHorizontal' },
{ name: 'my', description: 'marginVertical' },
{ name: 'p', description: 'padding' },
{ name: 'pb', description: 'paddingBottom' },
{ name: 'pl', description: 'paddingLeft' },
{ name: 'pr', description: 'paddingRight' },
{ name: 'pt', description: 'paddingTop' },
{ name: 'px', description: 'paddingHorizontal' },
{ name: 'py', description: 'paddingVertical' },
{ name: 'r', description: 'right' },
{ name: 'rounded', description: 'borderRadius' },
{ name: 'select', description: 'userSelect' },
{ name: 'self', description: 'alignSelf' },
{ name: 'shrink', description: 'flexShrink' },
{ name: 't', description: 'top' },
{ name: 'z', description: 'zIndex' },
]}
/>
#### Tokens
Default tokens are defined as:
```tsx
export const size = {
$0: 0,
'$0.25': 2,
'$0.5': 4,
'$0.75': 8,
$1: 20,
'$1.5': 24,
$2: 28,
'$2.5': 32,
$3: 36,
'$3.5': 40,
$4: 44,
$true: 44,
'$4.5': 48,
$5: 52,
$6: 64,
$7: 74,
$8: 84,
$9: 94,
$10: 104,
$11: 124,
$12: 144,
$13: 164,
$14: 184,
$15: 204,
$16: 224,
$17: 224,
$18: 244,
$19: 264,
$20: 284,
}
export const space = {
// ... space is the same as size, but also defines "negative" values for every size, like:
'$-1': -20,
}
export const zIndex = {
0: 0,
1: 100,
2: 200,
3: 300,
4: 400,
5: 500,
}
export const radius = {
0: 0,
1: 3,
2: 5,
3: 7,
4: 9,
true: 9,
5: 10,
6: 16,
7: 19,
8: 22,
9: 26,
10: 34,
11: 42,
12: 50,
}
```
Note that `color` tokens aren't defined, instead we are just using themes.
Also note that the "true" value on tokens is the "default" - Tamagui UI for now
uses defaults to determine how to size various things out of the box. This isn't
mandatory, and in version 2 of Tamagui we are working on a more clear solution.
#### Fonts
Only a `body` and `heading` font are exported, which both use "system" font
families:
<PropsTable
data={[
{
name: 'web',
description:
'-apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
},
{ name: 'native', description: 'System' },
]}
/>
You can create your own system font using the exported `createSystemFont`
helper, or of course swap out your own fonts altogether.
## core/config-v5
---
title: 'Config v5'
description: Modern config and themes with @tamagui/config/v5
---
<IntroParagraph>
The recommended Tamagui configuration gives you a complete design system, flexible theme
generation, Tailwind aligned shorthands, multiple animation drivers, and easy
customization.
</IntroParagraph>
V5 builds on v4 with more colors, theme helpers, and expanded media queries.
Migration from v4 is straightforward - see
[what's changed](#whats-different-from-v4).
First install the config package:
```bash
npm install @tamagui/config
```
### Changes from v4
- **Animations not bundled** - Import separately from `v5-css`, `v5-rn`,
`v5-reanimated`, or `v5-motion`. See
[Choosing an Animation Driver](#choosing-an-animation-driver).
- **Media query naming** - `2xl`/`2xs` → `xxl`/`xxs`/`xxxs`. Max queries use
kebab-case: `max2Xl` → `max-xxl`.
- **`styleCompat: 'react-native'`** - `flex` now uses `flexBasis: 0` by default
(v4 used `'legacy'` with `flexBasis: auto`).
- **`defaultPosition` not set** - Defaults to browser `static` instead of v4's
`'relative'`. You can still set it in your config.
### New Features
- **More color themes**: gray, blue, red, yellow, green, orange, pink, purple,
teal, neutral (v4 had blue, red, yellow, green, black, white)
- **More theme colors**: `shadow1`-`shadow8`, `highlight1`-`highlight8`,
`background01`/`background001` through `background08`, `color01`/`color001`,
`white0`/`white02`-`white08`, `black0`/`black02`-`black08`, and more
- **More animation values**: timing presets from `0ms` to `500ms`, plus named
springs like `quick`, `bouncy`, `lazy`, and variants like `quickLessBouncy`
- **`createV5Theme` helper** for easy theme customization and swapping colors
- **`getTheme` callback** in `createV5Theme` for computing custom values across
every theme automatically based on each theme's palette and scheme
- **Easier theme swapping**: pass only the `childrenThemes` you want to
`createV5Theme` to replace or extend the default color set
- **Height-based media queries**: `height-sm`, `height-md`, `height-lg` and max variants
- **`touch`** and **`hoverable`** media queries for device capability detection
- **Color utilities**: `adjustPalette`, `adjustPalettes` for transforming
palettes
- **Radix Colors v3** with improved accessibility (v4 used legacy Radix)
### Migrating from v4
To restore v4 behavior while using v5:
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations,
settings: {
...defaultConfig.settings,
styleCompat: 'legacy',
defaultPosition: 'relative',
},
})
```
Update media query names in your code: `$2xl` → `$xxl`, `$2xs` → `$xxs`,
`$max2Xl` → `$max-xxl`.
---
## Choosing an Animation Driver
V5 provides separate entry points for different animation drivers, so you only
bundle what you need:
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations,
})
```
Available animation drivers:
- **`@tamagui/config/v5-css`** - CSS animations (smallest bundle, great for web)
- **`@tamagui/config/v5-motion`** - Motion animations (spring physics, smooth)
- **`@tamagui/config/v5-rn`** - React Native Animated API
- **`@tamagui/config/v5-reanimated`** - Reanimated (best native performance)
<Notice>
The base `@tamagui/config/v5` export includes no animations. Import animations
separately from one of the driver entry points above.
</Notice>
### Cross-Platform Animation Setup
For apps targeting both web and native, you can use different animation drivers
per platform:
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v5'
import { animations as animationsCSS } from '@tamagui/config/v5-css'
import { animations as animationsReanimated } from '@tamagui/config/v5-reanimated'
import { createTamagui, isWeb } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations: isWeb ? animationsCSS : animationsReanimated,
})
```
This gives you CSS animations on web (smaller bundle, better performance) and
Reanimated on native (smooth 60fps animations).
### Available Animations
All v5 animation drivers include these preset animations:
**Timing animations** (fixed duration):
<PropsTable
data={[
{ name: '0ms', description: 'Instant (0ms)' },
{ name: '50ms', description: '50 milliseconds' },
{ name: '75ms', description: '75 milliseconds' },
{ name: '100ms', description: '100 milliseconds' },
{ name: '200ms', description: '200 milliseconds' },
{ name: '250ms', description: '250 milliseconds' },
{ name: '300ms', description: '300 milliseconds' },
{ name: '400ms', description: '400 milliseconds' },
{ name: '500ms', description: '500 milliseconds' },
]}
/>
**Spring animations** (physics-based):
<PropsTable
data={[
{ name: 'superBouncy', description: 'Very bouncy, playful spring' },
{ name: 'bouncy', description: 'Bouncy spring with natural feel' },
{ name: 'superLazy', description: 'Very slow, heavy spring' },
{ name: 'lazy', description: 'Slow, relaxed spring' },
{ name: 'medium', description: 'Balanced spring for general use' },
{ name: 'slowest', description: 'Slowest spring animation' },
{ name: 'slow', description: 'Slow spring animation' },
{ name: 'quick', description: 'Fast, responsive spring' },
{ name: 'quickLessBouncy', description: 'Quick with minimal overshoot' },
{ name: 'quicker', description: 'Faster spring' },
{ name: 'quickerLessBouncy', description: 'Faster with minimal overshoot' },
{ name: 'quickest', description: 'Fastest spring' },
{
name: 'quickestLessBouncy',
description: 'Fastest with minimal overshoot',
},
]}
/>
---
## Customizing Themes with createV5Theme
The `createV5Theme` function lets you customize the default v5 themes. The
easiest way to add or change colors is using `@tamagui/colors` which provides
Radix color palettes:
```tsx fileName="tamagui.config.ts"
import { createV5Theme, defaultChildrenThemes, defaultConfig } from '@tamagui/config/v5'
import { cyan, cyanDark, amber, amberDark } from '@tamagui/colors'
import { createTamagui } from 'tamagui'
const themes = createV5Theme({
childrenThemes: {
// include defaults (blue, red, green, yellow, etc.)
...defaultChildrenThemes,
// add new colors
cyan: { light: cyan, dark: cyanDark },
amber: { light: amber, dark: amberDark },
},
})
export const config = createTamagui({
...defaultConfig,
themes,
})
```
Or use only the colors you need:
```tsx fileName="tamagui.config.ts"
import { createV5Theme } from '@tamagui/config/v5'
import { blue, blueDark, gray, grayDark } from '@tamagui/colors'
// minimal theme with just blue and gray
const themes = createV5Theme({
childrenThemes: {
blue: { light: blue, dark: blueDark },
gray: { light: gray, dark: grayDark },
},
})
```
### createV5Theme Options
<PropsTable
data={[
{
name: 'childrenThemes',
type: 'Record<string, { light: ColorObject, dark: ColorObject }>',
description:
'Color themes to include. Use defaultChildrenThemes to include defaults, or pass only the colors you need. Works directly with @tamagui/colors.',
},
{
name: 'darkPalette',
type: 'string[]',
description: 'Override the dark base palette (12 colors from darkest to lightest)',
},
{
name: 'lightPalette',
type: 'string[]',
description: 'Override the light base palette (12 colors from lightest to darkest)',
},
{
name: 'grandChildrenThemes',
type: 'Record<string, { template: string }>',
description: 'Override grandChildren themes (alt1, alt2, surface1, etc.)',
},
{
name: 'componentThemes',
type: 'false | ComponentThemes',
default: 'false',
description:
'Component themes (defaults to false in v5). We recommend using defaultProps instead.',
},
{
name: 'getTheme',
type: '(info: { palette: string[], scheme: "light" | "dark" }) => Record<string, string>',
description:
"Callback that runs for every generated theme, letting you compute custom values based on each theme's palette and color scheme. The default uses this to generate opacity variants like color01, background001, etc.",
},
]}
/>
### Customizing Every Theme with getTheme
The `getTheme` option lets you add computed colors to every generated theme. It
receives each theme's palette and scheme, so your custom values automatically
adapt to every color and light/dark variant:
```tsx fileName="tamagui.config.ts"
import { createV5Theme, opacify } from '@tamagui/config/v5'
const themes = createV5Theme({
getTheme: ({ palette, scheme }) => {
const bg = palette[7]
const fg = palette[palette.length - 2]
return {
// add your own computed theme values
myOverlay: opacify(bg, 0.5),
mySubtleText: opacify(fg, 0.6),
}
},
})
```
This is how v5 generates its built-in opacity tokens (`background01`,
`color001`, etc.) — your custom `getTheme` merges on top of the defaults.
### Subtle Theme Variant
For a more muted look, use the subtle variant which has desaturated colors:
```tsx fileName="tamagui.config.ts"
import { defaultConfig, themes } from '@tamagui/config/v5-subtle'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
themes,
})
```
### Custom Color Adjustments
Use `adjustPalette` to transform colors with a callback function. The callback
receives each color's HSL values and its 1-based index (1-12):
```tsx fileName="tamagui.config.ts"
import {
adjustPalette,
adjustPalettes,
defaultChildrenThemes,
createV5Theme,
} from '@tamagui/config/v5'
// adjust a single palette - desaturate and lighten
const mutedBlue = adjustPalette(blue, (hsl, i) => ({
...hsl,
s: hsl.s * 0.7,
l: Math.min(100, hsl.l * 1.1),
}))
// adjust multiple palettes at once with adjustPalettes
const mutedThemes = adjustPalettes(defaultChildrenThemes, {
// 'default' applies to all colors not explicitly listed
default: {
light: (hsl, i) => ({ ...hsl, s: hsl.s * 0.8 }),
dark: (hsl, i) => ({ ...hsl, s: hsl.s * 0.6, l: hsl.l * 0.9 }),
},
// override specific colors
yellow: {
light: (hsl) => ({ ...hsl, s: hsl.s * 0.5 }),
dark: (hsl, i) => ({ ...hsl, s: hsl.s * (i <= 4 ? 0.3 : 0.8) }),
},
// skip adjustment for specific colors
gray: undefined,
})
const themes = createV5Theme({ childrenThemes: mutedThemes })
```
The callback receives:
- `hsl` - object with `h` (hue 0-360), `s` (saturation 0-100), `l` (lightness
0-100)
- `i` - 1-based index in the palette (1-12)
Use the index to apply different adjustments to background colors (1-4),
mid-tones (5-8), and foreground colors (9-12).
---
## Theme Values
V5 themes include a rich set of color values accessible via `$theme-*` tokens.
### Base Colors (color1-12)
Every theme includes 12 base colors that shift based on the theme:
```tsx
<View bg="$color1" /> // Lightest in light mode, darkest in dark mode
<View bg="$color6" /> // Mid-range
<View bg="$color12" /> // Darkest in light mode, lightest in dark mode
```
### Semantic Colors
Standard semantic values that adapt to each theme:
- `background`, `backgroundHover`, `backgroundPress`, `backgroundFocus`
- `color`, `colorHover`, `colorPress`, `colorFocus`
- `borderColor`, `borderColorHover`, `borderColorPress`, `borderColorFocus`
- `placeholderColor`, `outlineColor`
### Opacity Variants
Foreground and background colors with opacity variants. The naming uses the
opacity value without the decimal point (e.g., `01` = 10%, `0025` = 2.5%).
- `color01` (10%), `color0075` (7.5%), `color005` (5%), `color0025` (2.5%),
`color002` (2%), `color001` (1%)
- `background01` (10%), `background0075` (7.5%), `background005` (5%),
`background0025` (2.5%), `background002` (2%), `background001` (1%)
- `background02` (20%), `background04` (40%), `background06` (60%),
`background08` (80%)
### White & Black (Fixed)
Always available regardless of theme, with opacity variants:
- `white`, `white0` (transparent), `white02`, `white04`, `white06`, `white08`
- `black`, `black0` (transparent), `black02`, `black04`, `black06`, `black08`
- `white1`-`white12`, `black1`-`black12` (12-step scales)
### Shadow & Highlight Colors
Pre-tuned shadow opacities (black) for light and dark modes:
- `shadow1` through `shadow8` (light shadows are subtler, dark are stronger)
- `shadowColor` - Default shadow color for the theme
Pre-tuned highlight opacities (white) for light and dark modes:
- `highlight1` through `highlight8` (matching shadow scale, useful for glows/overlays)
### Radix Color Scales
All color themes expose their full 12-step scale:
```tsx
<View bg="$blue5" />
<Text color="$red11" />
<View borderColor="$green7" />
```
Available scales: `gray1-12`, `blue1-12`, `red1-12`, `green1-12`, `yellow1-12`,
`orange1-12`, `pink1-12`, `purple1-12`, `teal1-12`
### Neutral Colors
The `neutral` scale maintains sufficient contrast on both light and dark
backgrounds - useful for text or UI that needs to work regardless of theme:
- `neutral1`-`neutral12`
### Accent Colors
Every theme includes an inverted accent scale for contrast elements:
- `accent1`-`accent12` - Inverted palette (light colors in dark mode, vice
versa)
- `accentBackground`, `accentColor` - Quick access to accent bg/fg
```tsx
<Button theme="accent">Primary Action</Button>
<View bg="$accentBackground" color="$accentColor" />
```
---
## Color Themes
V5 includes these color themes by default:
- **Grayscale**: gray, neutral
- **Colors**: blue, green, red, yellow, orange, pink, purple, teal
- **Fixed**: black, white (generated from base palettes, don't change between
light/dark)
Each color theme has 12 steps following the
[Radix Colors](https://www.radix-ui.com/colors) pattern.
Use any color theme with the `theme` prop:
```tsx
<Button theme="blue">Blue button</Button>
<Card theme="purple">Purple card</Card>
```
Access individual color values in any theme:
```tsx
<View bg="$orange5" borderColor="$teal7" />
<Text color="$pink11" />
```
---
## Templates
V5 includes default templates that control how theme values map to palette
indices. Available templates: `base`, `surface1`, `surface2`, `surface3`,
`alt1`, `alt2`, `inverse`.
For more details on how templates work, see the
[Creating Custom Themes Guide](/docs/guides/theme-builder).
## Component Themes
V5 keeps component themes because they solve a hard problem — how do you change
the theme of an entire area (like from `dark` to `dark_green`) but have the
components inside it still keep their relative shades?
One of the great things in Tamagui is being able to set component themes, and
sub-themes, and have an entire area of your UI re-theme and look perfect.
We'd exported just having the ability to set a default theme, like "surface1 =>
4", but then if you try and re-theme a component to `green`, that would
conflict. For example, if you made your default `<Button theme="surface3" />`,
then if someone uses it `<Button theme="green" />` now it's green, but not the
Button green that is typically stronger than your base green.
So for now we've left them on! We do want to move away from component themes in
general as they can be tricky to understand. But the benefits are also strong,
and until we find a great solution we're leaving them. You can always set
`componentThemes: false` in your `createV5Themes` function to turn it off.
---
## Settings
V5 config includes these default settings:
<PropsTable
data={[
{ name: 'defaultFont', description: '"body"' },
{
name: 'fastSchemeChange',
description:
'true - Uses DynamicColorIOS on iOS for fast light/dark theme changes.',
},
{
name: 'shouldAddPrefersColorThemes',
description: 'true - Generates CSS for prefers-color-scheme.',
},
{
name: 'allowedStyleValues',
description: '"somewhat-strict-web" - Allows web-only values like vh, vw.',
},
{
name: 'addThemeClassName',
description: '"html" - Adds theme className to html tag for CSS variable access.',
},
{
name: 'onlyAllowShorthands',
description: 'true - Removes types for longhand versions of shorthands.',
},
{
name: 'styleCompat',
description:
'"react-native" - Aligns flex defaults to React Native (flexBasis: 0).',
},
]}
/>
---
## Media Queries
V5 includes an expanded set of media queries with height-based, max-width, and
min-width variants.
The `$sm` through `$xxl` breakpoints match Tailwind CSS exactly (640, 768, 1024,
1280, 1536), making it easy to work across both ecosystems. The smaller
breakpoints (`$xxxs`, `$xxs`, `$xs`) provide finer control below 640px, which is
especially useful for container queries where you need granularity at smaller
sizes.
### Min-Width (Mobile-First)
<PropsTable
data={[
{ name: 'xxxs', description: 'minWidth: 260' },
{ name: 'xxs', description: 'minWidth: 340' },
{ name: 'xs', description: 'minWidth: 460' },
{ name: 'sm', description: 'minWidth: 640' },
{ name: 'md', description: 'minWidth: 768' },
{ name: 'lg', description: 'minWidth: 1024' },
{ name: 'xl', description: 'minWidth: 1280' },
{ name: 'xxl', description: 'minWidth: 1536' },
]}
/>
### Max-Width (Desktop-First)
<PropsTable
data={[
{ name: 'max-xxxs', description: 'maxWidth: 260' },
{ name: 'max-xxs', description: 'maxWidth: 340' },
{ name: 'max-xs', description: 'maxWidth: 460' },
{ name: 'max-sm', description: 'maxWidth: 640' },
{ name: 'max-md', description: 'maxWidth: 768' },
{ name: 'max-lg', description: 'maxWidth: 1024' },
{ name: 'max-xl', description: 'maxWidth: 1280' },
{ name: 'max-xxl', description: 'maxWidth: 1536' },
]}
/>
### Height Min (Mobile-First)
<PropsTable
data={[
{ name: 'height-sm', description: 'minHeight: 640' },
{ name: 'height-md', description: 'minHeight: 768' },
{ name: 'height-lg', description: 'minHeight: 1024' },
]}
/>
### Height Max (Desktop-First)
<PropsTable
data={[
{ name: 'max-height-sm', description: 'maxHeight: 640' },
{ name: 'max-height-md', description: 'maxHeight: 768' },
{ name: 'max-height-lg', description: 'maxHeight: 1024' },
]}
/>
<Notice>
Height queries have higher CSS specificity than width queries. When both match, height
wins.
</Notice>
### Device Capability
<PropsTable
data={[
{ name: 'touchable', description: 'pointer: coarse (touch devices)' },
{ name: 'hoverable', description: 'hover: hover (devices with hover capability)' },
]}
/>
These names describe intent rather than CSS implementation details — you're thinking
"is this a touch device?" not "what's the pointer media query name?" They also read
naturally in code:
```tsx
<Button
$touchable={{ p: '$4', minH: 44 }}
$hoverable={{ hoverStyle: { bg: '$color5' } }}
/>
```
On web, these aren't exact opposites. A laptop with a touchscreen might have
`pointer: fine` (trackpad as primary input) but still support touch. Having both
lets you target exactly what you mean.
<Notice>
On native, `touch` is always true and `hoverable` is always false since you can't hover
on a touchscreen.
</Notice>
---
## Shorthands
V5 includes Tailwind-aligned shorthands. With `onlyAllowShorthands: true` (the
default), only the short forms are available in types.
To restore longhand props while extending `defaultConfig`, override
`onlyAllowShorthands`:
```tsx
const config = createTamagui({
...defaultConfig,
settings: {
...defaultConfig.settings,
onlyAllowShorthands: false,
},
})
```
`onlyShorthandStyleProps` is a separate, narrower setting for border, outline, and
shadow style props. Setting it to `false` does not override `onlyAllowShorthands`.
### Text
<PropsTable data={[{ name: 'text', description: 'textAlign' }]} />
### Layout & Spacing
<PropsTable
data={[
{ name: 'p', description: 'padding' },
{ name: 'pt', description: 'paddingTop' },
{ name: 'pb', description: 'paddingBottom' },
{ name: 'pl', description: 'paddingLeft' },
{ name: 'pr', description: 'paddingRight' },
{ name: 'px', description: 'paddingHorizontal' },
{ name: 'py', description: 'paddingVertical' },
{ name: 'm', description: 'margin' },
{ name: 'mt', description: 'marginTop' },
{ name: 'mb', description: 'marginBottom' },
{ name: 'ml', description: 'marginLeft' },
{ name: 'mr', description: 'marginRight' },
{ name: 'mx', description: 'marginHorizontal' },
{ name: 'my', description: 'marginVertical' },
]}
/>
### Position & Size
<PropsTable
data={[
{ name: 't', description: 'top' },
{ name: 'b', description: 'bottom' },
{ name: 'l', description: 'left' },
{ name: 'r', description: 'right' },
{ name: 'minW', description: 'minWidth' },
{ name: 'maxW', description: 'maxWidth' },
{ name: 'minH', description: 'minHeight' },
{ name: 'maxH', description: 'maxHeight' },
{ name: 'z', description: 'zIndex' },
]}
/>
### Flex & Alignment
<PropsTable
data={[
{ name: 'grow', description: 'flexGrow' },
{ name: 'shrink', description: 'flexShrink' },
{ name: 'items', description: 'alignItems' },
{ name: 'self', description: 'alignSelf' },
{ name: 'content', description: 'alignContent' },
{ name: 'justify', description: 'justifyContent' },
]}
/>
### Other
<PropsTable
data={[
{ name: 'bg', description: 'backgroundColor' },
{ name: 'rounded', description: 'borderRadius' },
{ name: 'select', description: 'userSelect' },
]}
/>
---
## Tree Shaking Themes
**Production Optimization** - Theme JS can grow to 20KB or more. Since Tamagui can hydrate themes from CSS variables, you can remove the themes JS from your client bundle for better Lighthouse scores.
This works because Tamagui generates CSS variables for all theme values. On the
client, it reads these from the DOM instead of needing the JS object.
### Setup
```tsx fileName="tamagui.config.ts"
import { defaultConfig, themes } from '@tamagui/config/v5'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
// only load themes on server - client hydrates from CSS
// for non-One Vite apps, use import.meta.env.SSR instead
themes: process.env.VITE_ENVIRONMENT === 'client' ? ({} as typeof themes) : themes,
})
```
<Notice theme="green">
This optimization requires server-side rendering. The CSS must be rendered on the server
for the client to hydrate from it. Make sure you're using `config.getCSS()` or the
bundler plugin's `outputCSS` option.
</Notice>
---
## Summary: v4 to v5 Migration Checklist
1. **Add animations import** - v5 doesn't bundle animations:
```tsx
import { animations } from '@tamagui/config/v5-css'
```
2. **Update media query names** - `2xl` → `xxl`, `2xs` → `xxs`, `max2Xl` →
`max-xxl`
3. **Review flex behavior** - v5 uses `flexBasis: 0` by default (React Native
style)
4. **Review position** - v5 doesn't set `defaultPosition` (defaults to `static`)
5. **Review theme colors** - v5 uses Radix Colors v3 with slightly different
values
## core/configuration
---
title: Configuration
description: Set up media queries, tokens, themes, and more.
---
Configuration in Tamagui controls things like tokens, themes, media queries,
animations, shorthands, and settings.
First, create a `tamagui.config.ts` file.
<Notice theme="blue">
We recommend starting with [`@tamagui/config/v5`](/docs/core/config-v5), our preset
configuration with sensible defaults, Tailwind-aligned shorthands, and a complete theme
system.
</Notice>
```bash
npm install @tamagui/config
```
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v5'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
media: {
...defaultConfig.media,
// add your own media queries here, if wanted
},
})
type OurConfig = typeof config
declare module 'tamagui' {
interface TamaguiCustomConfig extends OurConfig {}
}
```
We find most people are best off with this as it saves a lot of time, shares
shorthands with Tailwind, and has a lot of refinement. For more on our default
configuration, go to [the next page](/docs/core/config-v5).
Here's an example of a stripped down configuration to get a feel for the most
common concepts:
```tsx fileName=tamagui.config.ts
import { createTamagui, getConfig } from 'tamagui'
export const config = createTamagui({
// tokens work like CSS Variables (and compile to them on the web)
// accessible from anywhere, never changing dynamically:
tokens: {
// width="$sm"
size: { sm: 8, md: 12, lg: 20 },
// margin="$sm"
space: { sm: 4, md: 8, lg: 12 },
// radius="$none"
radius: { none: 0, sm: 3 },
color: { white: '#fff', black: '#000' },
},
// themes are like CSS Variables that you can change anywhere in the tree
// you use <Theme name="light" /> to change the theme
themes: {
light: {
bg: '#f2f2f2',
color: '#000',
},
dark: {
bg: '#111',
color: '#fff',
},
// sub-themes are a powerful feature of tamagui, explained later in the docs
// user theme like <Theme name="dark"><Theme name="blue">
// or just <Theme name="dark_blue">
dark_blue: {
bg: 'darkblue',
color: '#fff',
},
},
// media query definitions can be used as style props or with the useMedia hook
// but also are added to "group styles", which work like Container Queries from CSS
media: {
sm: { maxWidth: 860 },
gtSm: { minWidth: 860 + 1 },
short: { maxHeight: 820 },
hoverable: { hover: 'hover' },
touchable: { pointer: 'coarse' },
},
shorthands: {
// <View px={20} />
px: 'paddingHorizontal',
},
// there are more settings, explained below:
settings: {
disableSSR: true,
allowedStyleValues: 'somewhat-strict-web',
},
})
// now, make your types flow nicely back to your `tamagui` import:
type OurConfig = typeof config
declare module 'tamagui' {
interface TamaguiCustomConfig extends OurConfig {}
}
```
<Notice theme="green">
In this guide we import everything from `tamagui`, but if you are using the lower-level
style library only, you can change the imports in this guide out for `@tamagui/core`,
which is a strict subset.
</Notice>
Finally, pass your `config` export to a `<TamaguiProvider />` at the root of
your app:
```tsx
import { TamaguiProvider, View } from 'tamagui'
import { config } from './tamagui.config.ts'
export default () => (
<TamaguiProvider config={config}>
<View margin="$sm" />
</TamaguiProvider>
)
```
You're all set up!
<Notice>
To avoid issues with hot reloading and circular imports, make sure to only import your
`tamagui.config.ts` once near the root of your app. If you need to access the
configuration in other files, you generally do this just by using props on your styled
components, or with hooks like `useMedia` or `useTheme`. We do have some getters like
`getTokenValue`, and if you really need to access the full configuration object (though
we don't find it necessary in most cases), you can use `getConfig`.
</Notice>
## In more detail
Let's start with an example of a more complete `tamagui.config.ts`:
```tsx fileName="tamagui.config.ts"
import { createFont, createTamagui, createTokens, isWeb } from 'tamagui'
// To work with the tamagui UI kit styled components (which is optional)
// you'd want the keys used for `size`, `lineHeight`, `weight` and
// `letterSpacing` to be consistent. The `createFont` function
// will fill-in any missing values if `lineHeight`, `weight` or
// `letterSpacing` are subsets of `size`.
const systemFont = createFont({
family: isWeb ? 'Helvetica, Arial, sans-serif' : 'System',
size: {
1: 12,
2: 14,
3: 15,
},
lineHeight: {
// 1 will be 22
2: 22,
},
weight: {
1: '300',
// 2 will be 300
3: '600',
},
letterSpacing: {
1: 0,
2: -1,
// 3 will be -1
},
// (native only) swaps out fonts by face/style
face: {
300: { normal: 'InterLight', italic: 'InterItalic' },
600: { normal: 'InterBold' },
},
})
// Set up tokens
// The keys can be whatever you want, but if using `tamagui` you'll want 1-10:
const size = {
0: 0,
1: 5,
2: 10,
// ....
}
export const tokens = createTokens({
size,
space: { ...size, '-1': -5, '-2': -10 },
radius: { 0: 0, 1: 3 },
zIndex: { 0: 0, 1: 100, 2: 200 },
color: {
white: '#fff',
black: '#000',
},
})
const config = createTamagui({
fonts: {
heading: systemFont,
body: systemFont,
},
tokens,
themes: {
light: {
bg: '#f2f2f2',
color: tokens.color.black,
},
dark: {
bg: '#111',
color: tokens.color.white,
},
},
media: {
sm: { maxWidth: 860 },
gtSm: { minWidth: 860 + 1 },
short: { maxHeight: 820 },
hoverable: { hover: 'hover' },
touchable: { pointer: 'coarse' },
},
// Shorthands
// Adds <View m={10} /> to <View margin={10} />
// See Settings section on this page to only allow shorthands
// Be sure to have `as const` at the end
shorthands: {
px: 'paddingHorizontal',
f: 'flex',
m: 'margin',
w: 'width',
} as const,
// Change the default props for any styled() component with a name.
// We are discouraging the use of this and have deprecated it, prefer to use
// styled() on any component to change its styles.
defaultProps: {
Text: {
color: 'green',
},
},
})
type AppConfig = typeof config
// this will give you types for your components
// note - if using your own design system, put the package name here instead of tamagui
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'
}
}
export default config
```
The `createTamagui` function receives a configuration object with properties:
- [`animations`](#animations): Configurable animation drivers.
- [`media`](#media): Cross-platform, typed
[media queries](/docs/core/use-media).
- [`themes`](#themes): Define [themes](/docs/intro/themes) to style contextually
anywhere in the tree, much like CSS variables.
- [`tokens`](#tokens): Your base tokens are much like static CSS variables.
- [`settings`](#settings): Many options for strictness and style behavior.
- [`shorthands`](#shorthands): Define shorter names for any style property.
<Notice theme="blue">
On Android you need to set the `face` option in `createFont` or else fonts won't pick up
different weights, due to a React Native restriction.
</Notice>
Note, for `tamagui` (not core), it expects you to define a `true` token that
maps to your default size, this way it knows what token to use by default. So
you'd do something like this:
```tsx
export const tokens = createTokens({
size: {
small: 20,
medium: 30,
true: 30, // note true = 30 just like medium, your default size token
large: 40,
},
space: {
small: 10,
medium: 20,
true: 20, // same goes for space and other token categories
large: 30,
},
})
```
<Notice theme="green">
If using the compiler, your tamagui.config.ts is parsed at build-time. For this reason,
we recommend keeping it relatively simple. Avoid importing heavy dependencies.
</Notice>
## TamaguiProvider
With your config set up, import it near the root of your app and pass it to
`TamaguiProvider`:
```tsx line=3-5 fileName="App.tsx"
import { TamaguiProvider } from 'tamagui'
import { config } from './tamagui.config'
export default function App() {
return (
<TamaguiProvider config={config} defaultTheme="light">
<AppContents />
</TamaguiProvider>
)
}
```
`TamaguiProvider` accepts a few properties:
<PropsTable
data={[
{
name: 'defaultTheme',
required: true,
type: 'string',
description: `The initial top level theme.`,
},
{
name: 'disableInjectCSS',
required: false,
type: 'boolean',
description: `By default Tamagui inserts CSS with a useInsertionEffect on load. But if you're setting up SSR you'll want to use getCSS() on the server instead and then turn on this setting.`,
},
{
name: 'insets',
required: false,
type: '{ top?: number; bottom?: number; left?: number; right?: number }',
description: `Safe area insets for iOS. Required for proper vertical Slider behavior on iOS. Pass the value from useSafeAreaInsets() hook.`,
},
]}
/>
By default, Tamagui injects the CSS for your configuration on the client-side
into `document.head`, but you probably will want something better for
production. To do this, pass true to `disableInjectCSS` on `TamaguiProvider`, and
then do one of the following three options:
If your framework has server-side layouts, you can just render it inline:
```tsx fileName=app/_layout.tsx
import { config } from './tamagui.config'
export default () => (
<html>
<head>
<style
dangerouslySetInnerHTML={{
__html: config.getCSS(),
}}
/>
</head>
<body>
<Slot />
</body>
</html>
)
```
To optimize a bit more so you share a single CSS file between all pages, you can
use one of our bundler plugins' `outputCSS` setting, like so:
```tsx fileName=vite.config.ts
import { tamaguiPlugin } from '@tamagui/vite-plugin'
export default {
plugins: [
tamaguiPlugin({
config: './src/tamagui.config.ts',
outputCSS: './src/tamagui.generated.css',
}),
],
}
```
And then you'll want to import the resulting `tamagui.generated.css` into your app.
As final option, you can also generate it yourself with the CLI. First create a
`tamagui.build.ts`:
```tsx fileName=tamagui.build.ts
import type { TamaguiBuildOptions } from 'tamagui'
export default {
components: ['tamagui'],
config: './config/tamagui.config.ts',
outputCSS: './tamagui.generated.css',
} satisfies TamaguiBuildOptions
```
And then run:
```bash
npx @tamagui/cli generate
```
See the [CLI Guide](/docs/guides/cli#generate) for more information on the
`generate` command and other CLI tools.
## Tokens
Tokens are inspired by the [Theme UI spec](https://theme-ui.com/theme-spec/).
They are mapped to CSS variables at build time. You can read about them in more
depth on [the tokens page](/docs/core/tokens).
### Font tokens
The font tokens are a bit special and are created with `createFont`:
```tsx
import { isWeb } from 'tamagui'
const interFont = createFont({
family: isWeb ? 'Inter, Helvetica, Arial, sans-serif' : 'Inter',
size: {
1: 12,
2: 14,
3: 15,
// ...
},
lineHeight: {
1: 17,
2: 22,
3: 25,
// ...
},
weight: {
4: '300',
6: '600',
},
letterSpacing: {
4: 0,
8: -1,
},
// because android handles fonts differently, you need to map the weight
// to the actual name of the font in the font-file
// you can get the name with `otfinfo`: otfinfo --family Inter.ttf
face: {
400: { normal: 'Inter', italic: 'Inter-Italic' },
500: { normal: 'InterBold', italic: 'InterBold-Italic' },
},
})
```
<Notice theme="blue">
We use numbered keys as an example, but you can use any strings you'd like. The optional
default styles in `tamagui` make use of number keys 1-10.
</Notice>
This gives you a lot of power over customizing every aspect of your design based
on each font family. In other styling libraries that follow the Theme UI spec,
you generally don't group your size/lineHeight/weight/etc tokens by the family,
which means you are forced to choose a single vertical rhythm no matter the
font.
#### Custom Fonts on Native
If you are using a custom font for native, you need to load your fonts for React
Native to recognize them. Tamagui doesn't really touch this area, instead you'll
use Expo or
[React Native directly](https://stackoverflow.com/questions/35255645/how-to-set-default-font-family-in-react-native),
something like this:
```tsx fileName="App.tsx"
import { useFonts } from 'expo-font'
function App() {
const [loaded] = useFonts({
Inter: require('@tamagui/font-inter/otf/Inter-Medium.otf'),
InterBold: require('@tamagui/font-inter/otf/Inter-Bold.otf'),
})
useEffect(() => {
if (loaded) {
// can hide splash screen here
}
}, [loaded])
if (!loaded) {
return null
}
return <MyApp />
}
```
### Non-font tokens
The rest of the tokens categories besides font are flatter. The `space` and
`size` generally share keys, and that space can generally use negative keys as
well.
```tsx
// passed into createTamagui
const tokens = createTokens({
color: {
white: '#fff',
black: '#000',
},
})
```
You access tokens then by using `$` prefixes in your values. Tamagui knows which
tokens to use based on the style property you use.
```tsx
const App = () => (
<Text fontSize="$lg" lineHeight="$lg" fontFamily="$body" color="$white">
Hello world
</Text>
)
```
One final note: using tokens with themes. Tokens are considered a "fallback" to
themes, so any values you define in your theme will override the token. The next
section will explain this further.
### Configuring tokens
There are a few settings that control how strict your style values are allowed
to be, which are handled by the `settings` option of `createTamagui`. See the
[settings](#settings) below.
### Themes
Themes live one level below tokens. Tokens are your variables, where themes use
those tokens to create consistent, generic properties that you then typically
use in shareable components. Themes should generally only deal with colors.
Tamagui components in general expect a set of theme keys to be defined like the
following, but you can deviate if you create your own design system.
```tsx
const config = createTamagui({
themes: {
light: {
background: '#fff',
backgroundHover: tokens.color.gray2,
// ...
color: tokens.color.gray10,
colorHover: tokens.color.gray9,
colorPress: tokens.color.gray8,
// ...
color1: tokens.color.gray1,
},
dark: {
background: '#000',
// ... matching the properties for light ^
},
},
// ... the rest of your configuration
})
```
Passing tokens to themes will be smart about sharing CSS, but is not required.
You can then access theme values for any style value, either through styled() or
through a Styled Component like View or Text:
```tsx
const P = styled(Text, {
color: '$color12'
})
// or directly
<Text color="$color11" />
```
One of the more powerful features in Tamagui is nesting themes, you just define
them like so:
```tsx
const config = createTamagui({
themes: {
light: {
color1: '#fff',
// ...
},
dark: {
color1: '#000',
// ...
},
light_alert: {
background: '#e6ebbd',
},
dark_alert: {
background: '#3e3f33',
},
},
// ... the rest of your configuration
})
```
Themes work just like CSS variables, they can be changed anywhere in the tree.
Sub-themes can be subsets of parent themes likewise as their values will fall
back to the parent theme. They can also be nested as deeply as you like - so
even `light_alert_subtle` is possible.
For more on themes, see the [themes docs](/docs/core/themes), and more
[advanced theme building library](/docs/guides/theme-builder).
### Media
For more full docs on media queries, see the
[useMedia docs page](/docs/core/use-media).
### Animations
Tamagui supports four animation drivers that can be swapped out per-platform:
- [`@tamagui/animations-css`](/docs/core/animations-css) - CSS transition based
animations (lightest)
- [`@tamagui/animations-react-native`](/docs/core/animations-react-native) -
React Native Animated
- [`@tamagui/animations-reanimated`](/docs/core/animations-reanimated) -
Reanimated for advanced animations
- [`@tamagui/animations-motion`](/docs/core/animations-motion) - Motion with
WAAPI for web
See the [Animations documentation](/docs/core/animations) for a full comparison
and guide on choosing the right driver.
Add `animations` to `createTamagui`:
```tsx
import { createAnimations } from '@tamagui/animations-react-native'
// pass this exported `animations` to your `createTamagui` call:
export const animations = createAnimations({
bouncy: {
damping: 9,
mass: 0.9,
stiffness: 150,
},
lazy: {
damping: 18,
stiffness: 50,
},
})
```
You can use different drivers per-platform - see the
[Platform-Specific Drivers](/docs/core/animation-drivers#platform-specific-drivers)
section.
### Shorthands
Shorthands are defined on `createTamagui`. Here's an example of a partial
shorthands configuration:
```tsx
// the as const ensures types work with the optional `onlyAllowShorthands` option
const shorthands = {
ac: 'alignContent',
ai: 'alignItems',
als: 'alignSelf',
bblr: 'borderBottomLeftRadius',
bbrr: 'borderBottomRightRadius',
bg: 'backgroundColor',
br: 'borderRadius',
btlr: 'borderTopLeftRadius',
btrr: 'borderTopRightRadius',
f: 'flex',
// ...
} as const
export default createTamagui({
shorthands,
})
```
Which will enable usage like:
```tsx
<View br="$myToken" />
```
where `br` expands into `borderRadius`.
### Settings
You can pass a `settings` object to `createTamagui`:
<PropsTable
data={[
{
name: `disableSSR`,
type: `boolean`,
description: `For SSR compatibility on the web, Tamagui will render once with the settings from mediaQueryDefaultActive set for all media queries. Then, it will render again after the initial render using the proper media query values. This is so that the hydrating components will match the server. Setting disableSSR to true will avoid this and instead immediately render using the up to date media state, which is the preferrable behavior for client-side only (SPA) style apps. See the [Server Rendering](/docs/core/server-rendering) docs for more details on SSR, the ClientOnly component, and related hooks.`,
},
{
name: `defaultFont`,
required: false,
type: `string`,
description: `Map it to the regular key of the font given to createTamagui, so "body" or "heading" make sense as values here. This will ensure this font is the fallback for any views that don't define it.`,
},
{
name: 'mediaQueryDefaultActive',
required: false,
type: 'Record<string, boolean>',
description: `For the first render, determines which media queries are true (useful for SSR).`,
},
{
name: 'addThemeClassName',
required: false,
type: "false | 'html' | 'body'",
default: false,
description: `If you want to style your <body> tag to use theme CSS variables on web, you must place the theme className onto the body element or above. This will do so. If disabled, Tamagui will place the className onto the element rendered by the TamaguiProvider`,
},
{
name: 'disableRootThemeClass',
required: false,
type: 'boolean',
description:
'On web, TamaguiProvider will add your top level theme className to the root of your document, the html tag. This lets you do things like style your body tag using Tamagui CSS variables that are generated by themes. If disabled, the theme className will instead by applied to the span inside the TamaguiProvider directly.',
},
{
name: 'selectionStyles',
required: false,
type: '(theme) => ({ backgroundColor: Variable | string; color: Variable | string })',
default: false,
description: `On the web, will generate ::selection styles for text selection.`,
},
{
name: 'shouldAddPrefersColorThemes',
required: false,
type: 'boolean',
default: `true`,
description: `Generates @media queries based on prefers-color-scheme for you if you have light/dark themes, causes larger total CSS sizes.`,
},
{
name: 'onlyAllowShorthands',
required: false,
type: 'boolean',
default: false,
description: `Will remove the type for the long-form versions of any shorthands you define.`,
},
{
name: `allowedStyleValues`,
required: false,
type: 'AllowedStyleValuesSetting',
description: `Set up allowed values on style props, this is only a type-level validation (see below).`,
},
{
name: `autocompleteSpecificTokens`,
required: false,
type: `boolean | 'except-special'`,
description: `Set up if "specific tokens" ($color.name) are added to the types where tokens are allowed (see below).`,
},
{
name: 'fastSchemeChange',
required: false,
type: 'boolean',
default: false,
description: `On iOS, this enables a mode where Tamagui returns color values using DynamicColorIOS significantly speeding up dark/light re-renders. Note: When enabled you must make sure the defaultTheme you set on TamaguiProvider matches the current system color scheme.`,
},
{
name: 'webContainerType',
required: false,
type: 'string',
default: 'inline-size',
description: `Only for web, when using group styles Tamagui uses container queries. When using a group on a containing element, Tamagui defaults to setting "container-type" in CSS to "inline-size".`,
},
]}
/>
#### Type strictness
#### `allowedStyleValues`
- `false (default)`: allows any string (or number for styles that accept
numbers)
- `strict`: only allows tokens for any token-enabled properties
- `strict-web`: same as strict but allows for web-specific tokens like
auto/inherit
- `somewhat-strict`: allow tokens or:
- for `space/size`: string% or numbers
- for `radius`: number
- for `zIndex`: number
- for `color`: named colors or rgba/hsla strings
- `somewhat-strict-web`: same as somewhat-strict but allows for web-specific
tokens
```tsx
type AllowedValueSetting =
| boolean
| 'strict'
| 'somewhat-strict'
| 'strict-web'
| 'somewhat-strict-web'
type AllowedStyleValuesSetting =
| AllowedValueSetting
| {
space?: AllowedValueSetting
size?: AllowedValueSetting
radius?: AllowedValueSetting
zIndex?: AllowedValueSetting
color?: AllowedValueSetting
}
```
#### `autocompleteSpecificTokens`
The VSCode autocomplete puts specific tokens above the regular ones, which leads
to worse DX. If true this setting removes the specific token from types for the
defined categories.
If set to `except-special`, specific tokens will autocomplete only if they don't
normally use one of the special token groups: space, size, radius, zIndex,
color.
### Environment Settings
A few things in Tamagui can be configured via environment variables. Doing it
this way lets us avoid extra code being shipped to the client, as your
bundler can tree shake the unused code easily when using environment variables.
You should be sure to either use the Vite or Webpack Tamagui plugins, or
configure your bundler to define the environment variables for tree shaking. On
native, it is not a concern as the extra size is minimal.
### Tree Shaking Themes
**Production Optimization** - Theme JS can grow to 20KB or more. Since Tamagui can hydrate themes from CSS variables, you can remove the themes JS from your client bundle for better Lighthouse scores.
This works because Tamagui generates CSS variables for all theme values. On the
client, it reads these from the DOM instead of needing the JS object.
<Notice>
This optimization only applies to SSR web apps (Next.js, One, Vite SSR). It doesn't
apply to Metro/Expo or static sites. Note that v5 themes are already more optimized than
v4, so this is mainly relevant if chasing the last few Lighthouse points.
</Notice>
### Setup
```tsx fileName="tamagui.config.ts"
import { defaultConfig, themes } from '@tamagui/config/v5'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
// only load themes on server - client hydrates from CSS
// for non-One Vite apps, use import.meta.env.SSR instead
themes: process.env.VITE_ENVIRONMENT === 'client' ? ({} as typeof themes) : themes,
})
```
<Notice theme="green">
This optimization requires server-side rendering. The CSS must be rendered on the server
for the client to hydrate from it. Make sure you're using `config.getCSS()` or the
bundler plugin's `outputCSS` option.
</Notice>
## core/create-styled-context
---
title: createStyledContext
description: Share variant props across compound components
---
When building a "Compound Component API", you need a way to pass properties down to multiple related components at once.
What is a Compound Component API? It looks like this:
```tsx
export default () => (
<Button size="$large">
<Button.Icon>
<Icon />
</Button.Icon>
<Button.Text>Lorem ipsum</Button.Text>
</Button>
)
```
Note how the `size="$large"` is set on the outer Button frame. We'd expect this size property to pass down to both the Icon and Text so that our frame size always matches the icon and text size. It would be cumbersome and bug-prone to have to always pass the size to every sub-component.
Tamagui solves this with `createStyledContext` which acts much like React `createContext`, except it only works with styled components and only controls their variants (for now, we're exploring if it can do more).
You can set it up as follows:
```tsx
import {
SizeTokens,
View,
Text,
createStyledContext,
styled,
withStaticProperties,
} from '@tamagui/core'
export const ButtonContext = createStyledContext<{
size: SizeTokens
}>({
size: '$medium',
})
export const ButtonFrame = styled(View, {
name: 'Button',
context: ButtonContext,
variants: {
size: {
'...size': (name, { tokens }) => {
return {
height: tokens.size[name],
borderRadius: tokens.radius[name],
gap: tokens.space[name].val * 0.2,
}
},
},
} as const,
defaultVariants: {
size: '$medium',
},
})
export const ButtonText = styled(Text, {
name: 'ButtonText',
context: ButtonContext,
variants: {
size: {
'...fontSize': (name, { font }) => ({
fontSize: font?.size[name],
}),
},
} as const,
})
export const Button = withStaticProperties(ButtonFrame, {
Props: ButtonContext.Provider,
Text: ButtonText,
})
```
A few things to note here:
- ButtonContext should only be typed and given properties that work across both components. Since they both define a `size` variant, this works.
- But note that one defines `...size` while the other defines `...fontSize`. This works in this case only if your design system has consistent naming for token sizes across `size` and `fontSize` (and is why we highly recommend this pattern).
- You can use `<Button.Props size="$large"><Button /></Button.Props>` now to set default props for a Button from above.
- As of today, using the `context` pattern does not work with the optimizing compiler flattening functionality. We recommend not using this for your most common components like Stacks or Text. For Button or anything higher level it is totally fine - it will still extract CSS and remove some logic from the render function. We have mapped out how this can work with flattening eventually and it should not be too much effort.
## core/exports
---
title: 'Other exports'
description: Helpful functions and constants
---
Let's take a quick look through some of the useful other exports in
`@tamagui/core`.
## Constants
[Constants are re-exported from @tamagui/constants](https://github.com/tamagui/tamagui/blob/main/code/core/constants/src/constants.ts):
- `isWeb` - True if targeting the web. Will be true both for SSR and client.
- `isBrowser` - `typeof document !== 'undefined'`
- `isServer` - `isWeb && !isBrowser`.
- `isClient` - `isWeb && isBrowser`.
- `useIsomorphicLayoutEffect` - `isServer ? useEffect : useLayoutEffect`. Helper
for SSR rendering without warnings.
- `isChrome` - client-side Chrome
- `isWebTouchable` - web-only touch-device (client side only)
- `isTouchable` - True for any touch device (client side only).
---
## Helpers
### getConfig
The exported `getConfig` will return your fully parsed Tamagui config. This is
safer than importing it directly as it avoids circular imports. In general you
want to use `useTheme` to access themes, and
`getTokens`/`getToken`/`getTokenValue` for tokens, but for other parts of the
config this can be useful.
### insertFont
```tsx
type insertFont = (name: string, fontIn: GenericFont) => ParsedFont
```
Will add a new font after initial createTamagui call. Where `GenericFont` is the
same as a font passed to createTamagui, and `ParsedFont` is the font with the
subkeys prefixed with `$` and turned into variable objects.
### updateFont
The same as insertFont, but will update an existing font.
### isTamaguiComponent
```tsx
type isTamaguiComponent = (component: any, name?: string) => boolean
```
If no name given, true if a Tamagui component, if name given ensures it's the
specific named Tamagui component.
### isTamaguiElement
```tsx
type isTamaguiElement = (child: any, name?: string) => boolean
```
If no name given, true if a Tamagui ReactElement, if name given ensures it's the
specific named Tamagui component element.
### getTokens
```tsx
;() => TokensParsed
```
Returns the parsed Tamagui config object of all your tokens, can be used at
runtime to get values from tokens. If you are looking to get a single token
value, prefer `getToken` or `getTokenValue`.
### getToken
```tsx
;(name: Token, group?: keyof Tokens) => any
```
Given the specific name of a token or a name + group, will return the value as
either a variable on the web, or raw value on native. So if you define a size
token with key `small` and value of `14`:
```tsx
getToken('$size.small') // returns on web var(--size-small), native 14
getToken('$small', 'size') // returns on web var(--size-small), native 14
```
### getTokenValue
```tsx
;(name: Token, group?: keyof Tokens) => any
```
Similar to getToken, but always returns the raw value rather than the variable
name. If you define a size token with key `small` and value of `14`:
```tsx
getTokenValue('$size.small') // returns 14
getTokenValue('$small', 'size') // returns 14
```
### themeable
```tsx
themeable<Comp extends ReactComponentLike>(component: Comp): Comp
```
A higher order component that accepts `theme` and `themeInverse`, rendering them
onto `Theme` before rendering your component.
### getVariable
```tsx
type getVariable = (value: Variable) => string
```
If given a `Variable` - which comes from a parsed theme or token returned from
`createTamagui`, `useTheme` or `getTokens` (read more
[on the useTheme docs](/docs/core/use-theme)).
Calling `getVariable(useTheme().color)` returns `var(--color)` on the web, and
`#fff` on other platforms.
### setOnLayoutStrategy
This is a low-level utility to control `onLayout` performance. By default,
Tamagui `onLayout` on the web uses the `"async"` strategy (see below), which
gives a good balance of accuracy and performance. For extremely accurate to
native `onLayout`, you can use `"sync"`, but it can cause performance issues in
typical usage.
Call `setOnLayoutStrategy` with a single string argument:
- `"off"` will pause all `onLayout` callbacks from running.
- `"sync"` will run every frame and update every frame if necessary.
- `"async"` will use an async getBoundingClientRect strategy that avoids
repaints/reflows, and also waits 2 frames of debouncing before triggering the
callback.
The `async` strategy generally has great performance on web, and it will also
ensure to call `onLayout` sync on mount the first time, so you avoid jitters.
---
## Hooks
### useProps
Pass in props that include media styles and shorthands, get back a flat object
of props with the current media styles merged and shorthands expanded. This is
an advanced pattern that should be used sparingly, as it will trigger updates on
every media query used in the props object. Helpful for more complex components
that need to pass down props they are given to children (a common example being
the `size` prop, if it's controlling children that also accept size).
```tsx
// if props is:
// { size: '$2', $sm: { size: '$4' } }
// and $sm is active, activeProps will be:
// { size: '$4' }
// if shorthands like m => margin, then m always expands to margin
function MyButton(props) {
const activeProps = useProps(props)
}
```
You can pass an option to disable the shorthands expansion, and another option
to resolve any token strings into their respective theme or token value:
```tsx
function MyButton(props) {
const activeProps = useProps(props)
}
```
useProps takes a second argument, which is the same as useStyle, documented in
the next section.
### useStyle
Pass in props that include media styles and shorthands, get back a flat style
object with the current media styles merged, shorthands expanded, and all
theme/token values resolved (into CSS variables on the web or values on native
by default). This will remove any non-style values and will by default expand
all variants and resolve all values.
```tsx
// if props is:
// { color: '$background', $sm: { color: '$color' } }
// and $sm is active, activeProps will be on web:
// { color: 'var(--color)' }
// and on native will resolve to the actual theme or token value:
// { color: '#fff' }
// this resolve behavior can be controlled, see the docs just below.
// if shorthands like m => margin, then m expands to margin
function MyButton(props) {
const style = useStyle(props)
}
```
It can take a couple options to control how it resolves values and handles
shorthands:
```tsx
function MyButton(props) {
const style = useStyle(props, {
// option to disable the shorthand expansion
disableExpandShorthands: true,
// option to change how values are resolved
// 'auto' (default) resolves to optimized value on all platforms (CSS variable on web, DynamicColor on iOS, not optimized on android)
// 'value' resolves to the raw theme/token string on all platforms (the same as useTheme()[name].val)
resolveValues: 'value',
// pass a Tamagui component to resolve any custom variants:
forComponent: CustomComponent,
})
}
```
### usePropsAndStyle
Similar to the above two hooks, this returns your fully resolved/merged/media
query flattened styles, but instead of returning _just_ props or _just_ style,
it returns both of them split out, as well as returning the result of useTheme()
and useMedia() that it uses internally:
```tsx
function MyButton(props) {
const [props, style, theme, media] = usePropsAndStyle(props, opts)
// see useStyle to for documentation on opts
// we return theme, media as they are used internally already in the hook
}
```
### useWebRef
For platform-specific files (`.tsx` and `.native.tsx`), this hook creates a
properly typed ref for web that also composes with a forwarded ref. When you
need to access `HTMLElement`-specific properties like `selectionStart` on
inputs, this avoids needing `any` casts.
```tsx
import { useWebRef } from 'tamagui' // or '@tamagui/element'
// In your .tsx (web) file:
function MyInput(props, forwardedRef) {
const { ref, composedRef } = useWebRef<HTMLInputElement>(forwardedRef)
// ref.current is typed as HTMLInputElement
// Access web-specific properties safely:
const start = ref.current?.selectionStart
// composedRef forwards to both ref and forwardedRef
return <StyledInput ref={composedRef} />
}
```
### useNativeRef
For platform-specific files (`.native.tsx`), this hook creates a properly typed
ref for React Native that also composes with a forwarded ref. Pairs with
`useWebRef` for cross-platform component development.
```tsx
import { useNativeRef } from 'tamagui' // or '@tamagui/element'
// In your .native.tsx file:
function MyComponent(props, forwardedRef) {
const { ref, composedRef } = useNativeRef(forwardedRef)
// ref.current is typed as View
return <StyledView ref={composedRef} />
}
```
For input components, use `useNativeInputRef` which types to `TextInput`:
```tsx
import { useNativeInputRef } from 'tamagui' // or '@tamagui/element'
function MyInput(props, forwardedRef) {
const { ref, composedRef } = useNativeInputRef(forwardedRef)
// ref.current is typed as TextInput
return <StyledInput ref={composedRef} />
}
```
### getWebElement
A runtime utility that narrows a `TamaguiElement` to an `HTMLElement`. Throws if
the element is not an HTMLElement. Useful for platform-specific code where you
know you're on web.
```tsx
import { getWebElement } from 'tamagui' // or '@tamagui/element'
// Narrows to HTMLElement
const el = getWebElement(ref.current)
// Or narrow to a specific element type
const input = getWebElement<HTMLInputElement>(ref.current)
input.selectionStart // typed correctly
```
### useThemeName
Returns the string name of the current theme.
### useIsTouchDevice
SSR-friendly, only true if native touchable or web touchable device (client
side, not server side).
### useDidFinishSSR
SSR-friendly, returns true if SSR has completed on the client, false before
hydration done. On server it's always false.
### ClientOnly
A component that opts its children out of SSR. When wrapped in `ClientOnly`,
Tamagui avoids double-renders in various places which results in faster
client-side rendering.
```tsx
import { ClientOnly } from 'tamagui' // or '@tamagui/core'
// Children will render only on the client, skipping SSR entirely
<ClientOnly>
<MyClientOnlyComponent />
</ClientOnly>
// Can be conditionally enabled
<ClientOnly enabled={shouldSkipSSR}>
<MyComponent />
</ClientOnly>
```
---
## Type Helpers
### GetProps
Fetches the type of props for a Tamagui component:
```tsx
import { View, GetProps, styled } from '@tamagui/core'
const X = styled(View, {})
type XProps = GetProps<typeof X>
```
### GetRef
Fetches the type of a ref for a Tamagui component, or any React component:
```tsx
import { View, GetRef, styled } from '@tamagui/core'
const X = styled(View, {})
const MyComponent = () => {
const ref = useRef<GetRef<typeof X>>()
return <X ref={ref} />
}
```
## core/font-language
---
title: FontLanguage
description: Using custom fonts for each language
---
The `FontLanguage` component works with the Tamagui design system, allowing you to define custom fonts per-language.
### Rationale
Before `FontLanguage`, you could get custom fonts with some effort. Simply create a `body` font, and a `body_french` font, and switch it out like so on relevant text components: `<Text fontFamily={isFrench ? '$body_french' : '$body'} />`.
This would work, but having to update every Text component on page with a conditional would be terribly verbose and slow.
With `FontLanguage`, you can do:
```tsx
<FontLanguage body={isFrench ? 'french' : 'default'}>
<Text fontFamily="$body">Hello world</Text>
</FontLanguage>
```
Even better, with `styled`:
```tsx
const P = styled(Text, { fontFamily: '$body' })
<FontLanguage body={isFrench ? 'french' : 'default'}>
<P>Hello world</P>
</FontLanguage>
```
On the web, FontLanguage uses CSS Variables to avoid re-renders even when changing languages.
### Usage
In your `tamagui.config.ts`, you can add a language variant for any font by adding a suffix with a custom name for that language. Here's a stripped down config showing how you can add the custom `_cn` suffix for a Chinese language font:
```tsx
import { createFont, createTamagui, createTokens } from 'tamagui'
export default createTamagui({
fonts: {
body: createFont({
family: 'Inter, Helvetica, Arial, sans-serif',
// ...
}),
body_cn: createFont({
family: 'Inter-CN',
// ...
}),
},
})
```
You'll then need to load the `Inter` and `Inter-CN` fonts for the platforms you support. Once you do, you'll get fully typed support for changing any `body` font to `cn` in a component:
```tsx
import { FontLanguage, Text } from 'tamagui' // or '@tamagui/core'
export default (
<FontLanguage body="cn">
<Text fontFamily="$body">你好</Text>
</FontLanguage>
)
```
The name you choose for the suffix is up to you, and Tamagui will treat any font with a `_` separator as a language variant.
To reset your font back to your base `body` font, you can use the reserved key `default`:
```tsx
import { FontLanguage, Text } from 'tamagui' // or '@tamagui/core'
export default (
<FontLanguage body="cn">
<Text fontFamily="$body">你好</Text>
<FontLanguage body="default">
<Text fontFamily="$body">Hello</Text>
</FontLanguage>
</FontLanguage>
)
```
## core/server-rendering
---
title: Server Rendering
description: Advanced SSR, hydration, and client-only rendering in Tamagui
---
Tamagui takes a unique approach to server-side rendering that goes beyond typical SSR solutions, ensuring perfect hydration with zero flickering even when using spring animations.
## How It Works
Unlike other libraries that simply render default values and re-render on the client, Tamagui uses a three-phase approach:
1. **Server**: Renders with proper CSS media queries for everything, including components using spring animation drivers (which normally require hard-coded values)
2. **Client (first render)**: Renders with CSS to match the server output perfectly
3. **Client (after hydration)**: Seamlessly swaps to springs and animations
This approach delivers perfect hydration with zero flickering, even for complex responsive layouts with animations.
## Opting Out
You can opt out of SSR entirely or for specific parts of your app.
### Wholesale: disableSSR
For single-page applications that don't need SSR, disable it entirely in your config:
```tsx
import { createTamagui } from 'tamagui'
export const config = createTamagui({
// ... other config
settings: {
disableSSR: true, // Automatically wraps app with <ClientOnly enabled>
},
})
```
When `disableSSR` is true:
- Media queries immediately use actual values
- No double render occurs
- Your entire app is wrapped with `<ClientOnly enabled>`
### Partial: ClientOnly Component
For fine-grained control, wrap specific parts of your tree:
```tsx
import { ClientOnly } from '@tamagui/use-did-finish-ssr'
function MyComponent() {
return (
<YStack>
{/* Server-rendered for SEO */}
<ProductInfo />
{/* Client-only for complex interactions */}
<ClientOnly enabled>
<InteractiveChart />
</ClientOnly>
</YStack>
)
}
```
## Configuration Component
The `Configuration` component is essentially a shorthand for `ClientOnly`. It accepts [configuration settings](/docs/core/configuration) like `disableSSR`:
```tsx
import { Configuration } from 'tamagui'
function MyApp() {
return (
<Configuration disableSSR>
{/* Everything here renders client-only */}
<ComplexComponent />
</Configuration>
)
}
```
## useMedia and SSR
The `useMedia` hook automatically leverages Tamagui's smart SSR rendering. During server render, it returns appropriate defaults that match CSS media queries, then updates after hydration without flickering.
```tsx
import { useMedia } from 'tamagui'
function ResponsiveComponent() {
const media = useMedia() // Automatically handles SSR correctly
return media.gtSm ? <DesktopView /> : <MobileView />
}
```
## SSR Hooks
### useDidFinishSSR
Returns `true` when hydration is complete:
```tsx
import { useDidFinishSSR } from '@tamagui/use-did-finish-ssr'
function MyComponent() {
const isClient = useDidFinishSSR()
return isClient ? <BrowserFeature /> : <Placeholder />
}
```
**Behavior:**
- Server/before hydration: `false`
- After hydration: `true`
- Inside `<ClientOnly enabled>`: always `true`
### useIsClientOnly
Checks if you're in a client-only context:
```tsx
import { useIsClientOnly } from '@tamagui/use-did-finish-ssr'
function MyComponent() {
const isClientOnly = useIsClientOnly()
if (isClientOnly) {
// Safe to use browser APIs
return <ComponentWithBrowserAPIs />
}
return <SSRSafeComponent />
}
```
### useClientValue
Returns `undefined` during SSR, your value after hydration:
```tsx
import { useClientValue } from '@tamagui/use-did-finish-ssr'
function WindowInfo() {
const width = useClientValue(() => window.innerWidth)
return <Text>{width ? `${width}px` : 'Loading...'}</Text>
}
```
## Best Practices
### When to Use disableSSR
**Use it for:**
- Single-page applications
- Client-only web apps
- Maximum performance without SSR overhead
**Avoid for:**
- SEO-critical content
- Server-rendered frameworks (Next.js SSR/SSG)
- Fast initial page loads
### When to Use ClientOnly
**Use it for:**
- Expensive client-only components (charts, editors)
- Browser API dependencies
- Non-critical UI that can load after initial render
**Example:**
```tsx
function ProductPage({ product }) {
return (
<YStack>
{/* SEO-critical content */}
<ProductHeader product={product} />
{/* Defer complex UI */}
<ClientOnly enabled>
<InteractiveGallery />
</ClientOnly>
</YStack>
)
}
```
### When to Use Hooks
- `useDidFinishSSR`: Conditional rendering based on hydration state
- `useIsClientOnly`: Checking client-only context for browser APIs
- `useClientValue`: Shorthand for client-only values
## Package Reference
```tsx
import {
ClientOnly, // Component for client-only subtrees
useDidFinishSSR, // Hook for hydration state
useIsClientOnly, // Hook for client-only context
useClientValue, // Hook for client-only values
} from '@tamagui/use-did-finish-ssr'
```
## See Also
- [Configuration](/docs/core/configuration) - Learn more about settings
- [useMedia](/docs/core/use-media) - Responsive media queries
- [Themes](/docs/intro/themes) - Theme system and context
## core/styled
---
title: styled()
description: Extend and build custom and optimizable components
---
<Notice theme="green">
If you're looking for a full list of style properties accepted by Tamagui, see the
[Styles page](/docs/intro/styles).
</Notice>
Create a new component by extending an existing one:
```tsx
import { GetProps, View, styled } from '@tamagui/core'
export const RoundedSquare = styled(View, {
borderRadius: 20,
})
// helper to get props for any TamaguiComponent
export type RoundedSquareProps = GetProps<typeof RoundedSquare>
```
Usage:
```tsx
<RoundedSquare x={10} y={10} backgroundColor="red" />
```
You can pass any prop that is supported by the component you are wrapping in styled.
One really important and useful thing to note about Tamagui style properties: the order is important! [Read more here](/docs/intro/styles#order-is-important)
## Variants
Let's add some variants:
```tsx
import { 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: {
'...size': (size, { tokens }) => {
return {
width: tokens.size[size] ?? size,
height: tokens.size[size] ?? size,
}
},
},
} as const,
})
```
<Notice theme="blue">
Please use `as const` for the variants definition until Typescript gains the ability to
infer generics as const.
</Notice>
We can use these like so:
```tsx
<RoundedSquare pin="top" centered size="$lg" />
```
To learn more about how to use them and all the special types, [see the docs on variants](/docs/core/variants).
### Non-working React Native views
You can assume all "utility" views in React Native are not supported: Pressable, TouchableOpacity, and others. They have specific logic for handling events that conflicts with Tamagui. We could support these in the future, but we don't plan on it - you can get all of Pressable functionality for the most part within Tamagui itself, and if you need something outside of it, you can use Pressable directly.
## 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`:
```tsx
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:
```tsx
const Button = styled(View, {
render: 'button',
padding: '$4',
backgroundColor: '$background',
})
const Anchor = styled(Text, {
render: 'a',
color: '$blue10',
})
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:
```tsx
<Stack
render={<a href="/about" target="_blank" />}
padding="$4"
backgroundColor="$background"
>
About Page
</Stack>
```
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`.
<Notice theme="yellow">
JSX element render props are not optimized by the compiler - they will cause a deopt.
</Notice>
### Function
For full control, pass a render function that receives props and component state:
```tsx
import { TamaguiComponentState } from '@tamagui/core'
;<Stack
render={(props, state: TamaguiComponentState) => (
<CustomButton {...props} isHovered={state.hover} isPressed={state.press} />
)}
padding="$4"
hoverStyle={{ backgroundColor: '$blue5' }}
>
Custom Button
</Stack>
```
The state object includes:
- `hover` - true when hovered (web)
- `press` - true when pressed
- `focus` - true when focused
- `disabled` - true when disabled
<Notice theme="yellow">
Function render props are not optimized by the compiler - they will cause a deopt.
</Notice>
### Runtime override
You can also override the render prop at runtime:
```tsx
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>
```
## styleable
Any component created with `styled()` has a new static property on it called `.styleable()`.
If you want a functional component that renders a Tamagui-styled component inside of it to _also_ be able to be `styled()`, you need to wrap it with `styleable`. This is a mouthful, let's see an example:
```tsx
// 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
},
})
```
The above code will generally cause weird issues, because Tamagui can't know that it needs to just forward some props down. Instead, Tamagui tries to "resolve" all the style props from `StyledHigherOrderStyledText` before passing them down to `HigherOrderStyledText`. But that causes problems, because now `HigherOrderStyledText` will merge things differently than you'd expect.
The way to fix this is to add `.styleable()` around your `HigherOrderStyledText`. You'll also want to forward the ref, which is forwarded for you:
```tsx
const StyledText = styled(Text)
// note the styleable wrapper here:
const HigherOrderStyledText = StyledText.styleable((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.
A final note: you must pass all Tamagui style props given to `HigherOrderStyledText` down to a single `StyledText`, at least if you want everything to work fully correctly.
And if you'd like to add new props on top of the existing props, you can pass them in for the first generic type argument of styleable:
```tsx
import { View, ViewProps } from '@tamagui/core'
type ExtraProps = {
someCustomProp: boolean
}
export type CustomProps = ViewProps & ExtraProps
const Custom = View.styleable<ExtraProps>((props) => {
// ...
return null
})
```
---
## accept
If you are wrapping something like an SVG, you may want it to accept theme and token values on certain props, for example `fill`. You can do so using `accept`:
```tsx
const StyledSVG = styled(
SVG,
{},
{
accept: {
fill: 'color',
} as const,
}
)
```
Now your StyledSVG will properly type the `fill` property to accept token and theme values and will pass the resolved colors to the SVG component.
You can also use `accept` to take in Tamagui style objects and output React Native style objects. This is useful for things like the `contentContainerStyle` prop on `ScrollView`, which expects a style object:
```tsx
const MyScrollView = styled(
ScrollView,
{},
{
accept: {
contentContainerStyle: 'style', // or 'textStyle'
} as const,
}
)
```
## core/theme
---
title: Theme
description: Change themes contextually
---
Changing themes in Tamagui is as easy as passing their name to the Theme
component.
## Usage
Change themes anywhere in your app like so:
```tsx
import { Square, Theme } from 'tamagui' // or '@tamagui/core'
export default () => (
<Theme name="dark">
<Button>I'm a dark button</Button>
<Button theme="subtle">I'm using dark_subtle theme</Button>
</Theme>
)
```
## Defining Themes
Themes live one level below tokens. Tokens are your static variables, where
themes are more dynamic like CSS variables.
You can define themes however you want, but for the `tamagui` UI kit, the
components default styling will use a set of pre-defined keys. Those keys are:
- `background`, `backgroundHover`,`backgroundPress`, `backgroundFocus`
- `borderColor`, `borderColorHover`,`borderColorPress`, `borderColorFocus`
- `shadowColor`
These are optional, as you can always set `unstyled` on any Tamagui component to
use your own styles.
Here's a simplified theme definition:
```tsx
import { createTamagui, createTokens } from 'tamagui'
const tokens = createTokens({
color: {
pinkDark: '#610c62',
pinkLight: '#f17efc',
},
// ... see configuration docs for required tokens
})
export default createTamagui({
tokens,
themes: {
dark: {
background: '#000',
color: '#fff',
},
light: {
color: '#000',
background: '#fff',
},
dark_pink: {
background: tokens.color.pinkDark,
color: tokens.color.pinkLight,
},
light_pink: {
background: tokens.color.pinkLight,
color: tokens.color.pinkDark,
},
},
})
```
Passing tokens to themes will reduce CSS, but is not required.
You can then access theme values for any style value, either through styled() or
through a Styled Component like View or Text:
```tsx
const P = styled(Text, {
color: '$color12'
})
// or directly
<Text color="$color11" />
```
Because we defined sub-themes `light_pink` and `dark_pink`, the `Theme`
component will now let us do this:
```tsx
import { Button, Theme } from 'tamagui'
export default () => {
return (
<Theme name="dark">
<Button>I have the theme dark</Button>
<Theme name="pink">
<Button>I have the theme dark_pink</Button>
</Theme>
</Theme>
)
}
```
Notice we just use the name `pink`, easy!
For more on themes see our [advanced theme guide](/docs/guides/theme-builder)
which also introduces the `createThemes` and `createThemeBuilder`, both of which
help generate larger and richer, typed themes more easily.
## core/tokens
---
title: Tokens
description: Accessing and using tokens
---
Tamagui lets you create tokens using `createTokens`, which is then passed to the `createTamagui` function as part of the [configuration](/docs/core/configuration) object.
## Getting Tokens
For example, if you define some tokens:
```tsx
const tokens = createTokens({
size: {
small: 10,
},
})
```
After you pass that into `createTamagui`, you can access your tokens from anywhere using `getTokens`:
```tsx
import { getTokens } from '@tamagui/core'
getTokens().size.small
```
or
```tsx
getTokens().size['$small']
```
If you'd like just an object containing the prefixed (starting with `$`) or non-prefixed values, you can use the `prefixed` option:
```tsx
// only non-$
getTokens({ prefixed: false }).size.small
// only $
getTokens({ prefixed: true })['$size'].small
```
What is returned is of type `Variable`, which is what Tamagui turns all tokens and theme values into internally in order to give them CSS variable names, as well as other things:
```tsx
getTokens().size.small.val // returns 10
getTokens().size.small.variable // returns something like (--size-small), which matches the CSS rule inserted
```
Tamagui has some helpers that make working with variables easier, which are documented in [Exports](/docs/core/exports), namely [`getVariable`](/docs/core/exports#getvariable) which will return the CSS variable on web, but raw value on native, and `getVariableValue` which always returns the raw value.
### Color Tokens as Fallback Values for Themes
Color tokens are available as fallback values when you access a theme. So when you `useTheme()` and then access a value that isn't in the theme, it will check for a `tokens.color` with the matching name.
Think of it this way:
- Tokens are static and are your base values.
- Themes are dynamic, they can change in your React tree, and live above tokens.
If you are confused by Tamagui themes, don't be. You can avoid them altogether, or avoid learning them until later. Instead, you can just build your app using regular style props and leave out themes altogether. Or, simply use a `light` and a `dark` theme if you want light and dark mode in your app, but avoid using any nested themes.
## Using Tokens with Components
When using `styled` or any Tamagui component like `Stack`, you can access tokens directly. Just like with `useTheme`, it will first look for a theme value that matches, and if not it will fall back to a token.
Tokens are automatically applied to certain properties. For example, `size` tokens are applied to width and height. And of course `radius` to borderRadius.
Here's how they all apply:
<PropsTable
title="How tokens apply to attributes"
data={[
{
name: 'Size',
description: 'width, height, minWidth, minHeight, maxWidth, maxHeight',
},
{
name: 'zIndex',
description: 'zIndex',
},
{
name: 'Radius',
description:
'borderRadius, borderTopLeftRadius, borderTopRightRadius, borderBottomLeftRadius, borderBottomRightRadius',
},
{
name: 'Color',
description:
'color, backgroundColor, borderColor, borderBottomColor, borderTopColor, borderLeftColor, borderRightColor',
},
{
name: 'Space',
description: 'All properties not matched by the above.',
},
]}
/>
## Specific Tokens
As of version 1.34, you can also define any custom token values you'd like:
```tsx
const tokens = createTokens({
// ...other tokens
icon: {
small: 16,
medium: 24,
large: 32,
},
})
```
And then access them using the new "specific tokens" syntax:
```tsx
export default () => (
<Stack
// access with the category first:
width="$icon.small"
/>
)
```
This, like all token values, works the same with `styled`:
```tsx
import { styled, View } from '@tamagui/core'
export const MyView = styled(View, {
width: '$icon.small',
})
```
When creating custom tokens, you can use the `px` helper to ensure values get proper pixel units on web while remaining as raw numbers on native:
```tsx
import { createTokens, px } from '@tamagui/core'
const tokens = createTokens({
customSize: {
small: px(100), // → "100px" on web, 100 on native
medium: px(200),
large: px(300),
},
opacity: {
low: 0.25, // → 0.25 (unitless on both platforms)
medium: 0.5,
high: 0.75,
},
})
```
<Notice theme="blue">
The predefined token categories like `size`, `space`, and `radius` automatically add
pixel units where appropriate, so you don't need to use the `px` helper for them.
</Notice>
## core/use-media
---
title: useMedia
description: Respond to different screen sizes
---
Define your media rules in the media object of your `tamagui.config.ts`:
```tsx line=2-6
export default createTamagui({
media: {
xs: { maxWidth: 660 },
gtXs: { minWidth: 660 + 1 },
sm: { maxWidth: 860 },
gtSm: { minWidth: 860 + 1 },
md: { maxWidth: 980 },
gtMd: { minWidth: 980 + 1 },
lg: { maxWidth: 1120 },
gtLg: { minWidth: 1120 + 1 },
short: { maxHeight: 820 },
tall: { minHeight: 820 },
hoverNone: { hover: 'none' },
touch: { pointer: 'coarse' },
},
})
```
<Notice theme="blue">
Choose the naming convention you prefer. We use `sm`, `md`, `lg` and `gtSm` etc. Where
"gt" means "greater than".
</Notice>
The order here is important: items defined further down will override items before. In this example, `xs` is the weakest, `gtXs` will override it, and so on.
Behind the scenes, we convert this object syntax into media query syntax with a simple loop over your object, turning camelCase into hyphen-case, and adding `@media()` around it.
### Usage
Now in any component you may import and use `useMedia` or `$` prefixed media prop styles.
### Inline props
```tsx
import { Button, XStack, useMedia } from 'tamagui' // note: design system can use @tamagui/core
export default () => {
const [x, setX] = useState(0)
return (
<XStack
backgroundColor="red"
$gtSm={{
backgroundColor: 'blue',
}}
$gtMd={{
backgroundColor: x > 0.5 ? 'green' : 'yellow',
}}
>
<Button onPress={() => setX(Math.random())}>Hello</Button>
</XStack>
)
}
```
In this example we are doing mobile-first design, where the base props will be overridden as the viewport gets wider. Notice the ternary logic in `$gtMd`: this is extractable with Tamagui, and will still output simple CSS with no leftover style props in your component's render function.
### Hooks
```tsx
import { Button, XStack, useMedia } from 'tamagui' // note: design system can use @tamagui/core
export default () => {
const media = useMedia()
return (
<XStack
// can be used as a ternary
backgroundColor={media.sm ? 'red' : 'blue'}
// can be used as a spread
{...(media.lg && {
x: 10,
y: 10,
})}
>
<Button>Hello</Button>
</XStack>
)
}
```
As long as **all** of your usages of useMedia are extractable, Tamagui will actually generate your CSS and then fully remove the hook from the output code. You can check this by adding `// debug` to the top of your component.
Of course, you may sometimes use `useMedia` for other purposes than styling. In fact, that's the nice part about Tamagui - if it can't extract, it always falls back to runtime gracefully.
Tamagui runtime `useMedia` usage will track which keys are accessed, and only update if the media query matching that key updates. This granular updating is nice for performance.
#### Limitations
The `useMedia` hook uses proxies so it can track which keys you are accessing and only re-render components that need re-rendering based on those keys. The Tamagui compiler also understands straightforward uses of the hook during compile-time and can remove it entirely when targeting the web, in favor of CSS.
- In order to keep things lightweight and simple, the proxied object returned by `useMedia` is not iterable, and you can't check keys on it using `in` or get them using `Object.keys`.
- It's encouraged to write `const media = useMedia()` and then access your keys like `media.sm` to work best with the compiler. The compiler supports de-structuring, but not re-naming.
## core/use-theme
---
title: useTheme
description: Creating and using theme values
---
Access the current theme in context with `useTheme`. Tamagui themes operate much
the same as
[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)
do, and so
[they can nest and override each other contextually](/docs/intro/themes).
Note that the object that useTheme returns is the current theme, proxied upwards
to every parent theme, finally proxying back to your tokens. This means it
behaves just like CSS variables as well, at runtime.
A short example:
```tsx
import { YStack, useTheme } from 'tamagui'
const App = () => {
const theme = useTheme()
return <YStack backgroundColor={theme.color1.val} />
}
```
The `useTheme` hook returns your Theme turned into a `ThemeParsed`, which means
it turns all values into a `Variable`:
```tsx
{
background: {
val: '#000',
variable: 'var(--background)',
name: 'background',
isVar: true,
},
color: {
val: '#fff',
variable: 'var(--color)',
name: 'color',
isVar: true,
},
}
```
The `useTheme` hook further adds a `.get()` helper function on each Variable for
extra performance. On the web, this will return the `.variable` property and
avoid re-rendering if values change, as it assumes you are using the CSS
variables it generates to update styles. Meanwhile on native it will return
`.val` and re-render always. But - if you enable the
[`fastSchemeChange` setting on createTamagui](/docs/core/configuration#settings)
then `get()` will return a
[DynamicColorIOS](https://reactnative.dev/docs/dynamiccolorios) on iOS and avoid
re-rendering there too - unless you do `get('web')` in which case it will only
optimize for web.
```tsx
import { View, useTheme } from '@tamagui/core'
import { SomeExternalComponent } from 'some-external-component'
const App = () => {
const theme = useTheme()
// on the web this is something like var(--background) and will avoid re-renders
// on native it will be something like #fff and will re-render
const background = theme.background.get()
// if you needed to access it in a way that always returns the raw value
const backgroundValue = theme.background.val
return (
<SomeExternalComponent
style={{
backgroundColor: background,
}}
/>
)
}
```
You can mix and match `useMedia` and `useTheme`, and the compiler understands
most basic usages, even with nested logic or constants within the file or
imports [whitelisted in your build setup](/docs/intro/compiler-install):
```tsx
import { YStack, useMedia, useTheme } from 'tamagui'
const App = () => {
const theme = useTheme()
const media = useMedia()
return (
<YStack
y={media.sm ? 10 : 0}
backgroundColor={media.lg ? theme.red : theme.blue}
{...(media.xl && {
y: theme.space2,
})}
/>
)
}
```
This will compile on the web to:
```tsx
const _cn =
' _alignItems-1oszu61 _boxSizing-deolkf _display-6koalj _flexBasis-1mlwlqe _flexDirection-eqz5dr _flexShrink-1q142lx _transform-_sm_1exagq _transform-_sm0_1wpzndr _backgroundColor-_lg_no4z4g _backgroundColor-_lg0_1qoifqd _transform-_xl_gqa6p0'
import { YStack, useMedia, useTheme } from 'tamagui'
const App = () => {
return <div className={_cn} />
}
```
And the following CSS:
```css
._alignItems-1oszu61 {
-ms-flex-align: stretch;
-webkit-align-items: stretch;
-webkit-box-align: stretch;
align-items: stretch;
}
._boxSizing-deolkf {
box-sizing: border-box;
}
._display-6koalj {
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
}
._flexBasis-1mlwlqe {
-ms-flex-preferred-size: auto;
-webkit-flex-basis: auto;
flex-basis: auto;
}
._flexDirection-eqz5dr {
-ms-flex-direction: column;
-webkit-box-direction: normal;
-webkit-box-orient: vertical;
-webkit-flex-direction: column;
flex-direction: column;
}
._flexShrink-1q142lx {
-ms-flex-negative: 0;
-webkit-flex-shrink: 0;
flex-shrink: 0;
}
@media (max-width: 860px) {
:root:root ._transform-_sm_1exagq {
-webkit-transform: translateY(10px);
transform: translateY(10px);
}
}
@media not all and (max-width: 860px) {
:root:root ._transform-_sm0_1wpzndr {
-webkit-transform: translateY(0px);
transform: translateY(0px);
}
}
@media (min-width: 1120px) {
:root:root:root ._backgroundColor-_lg_no4z4g {
background-color: var(--red);
}
}
@media not all and (min-width: 1120px) {
:root:root:root ._backgroundColor-_lg0_1qoifqd {
background-color: var(--blue);
}
}
@media (min-width: 1280px) {
:root:root:root:root ._transform-_xl_gqa6p0 {
-webkit-transform: translateY(var(--space2));
transform: translateY(var(--space2));
}
}
```
## Using outside of styling
You can `useTheme()` (and `useMedia()`) at runtime. Like useMedia, useTheme will
only re-render when it has to, and often you can skip re-renders altogether by
either passing the values to a Tamagui styled component, or by using the helper
`getVariable`:
### getVariable
If you access `theme.bg.val` in your render function, the component will only
re-render when theme.bg changes.
```tsx
import { useTheme, View } from '@tamagui/core'
export default () => {
const theme = useTheme()
// access the value
console.log(theme.bg.val)
return <View backgroundColor={theme.color1} />
}
```
## Changing the theme at the hook level
This is a more advanced use for building custom hooks or components, but you can
pass in a theme name and a component theme name (as
[discussed here](/docs/intro/themes#component-themes)) to grab the right
subset theme:
```tsx
function MyComponent(props) {
const theme = useTheme(props.theme, 'MyComponent')
}
```
For example, if you have the following themes:
- `dark`
- `dark_green`
- `dark_green_MyComponent`
And this code:
```tsx
<Theme name="dark">
<MyComponent theme="green" />
</Theme>
```
The above `useTheme` hook would return the `dark_green_MyComponent` theme.
## core/variants
---
title: Variants
description: Simple typed prop styles through styled()
---
Variants allow for a nice balance between simplicity and power, with affordances
for both the compiler and the type system.
In a style library you want to be able to succinctly add on conditional values
that expand into groups of styles. Variants do just that. Before explaining more
on why and how they work, an example:
```tsx
import { 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: {
'...size': (size, { tokens }) => {
return {
width: tokens.size[size] ?? size,
height: tokens.size[size] ?? size,
}
},
},
} as const,
})
```
<Notice theme="blue">
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.
</Notice>
We can use this styled component like so:
```tsx
<Circle pin="top" centered size="$lg" />
```
This component uses a few different types of variants. Below we'll expand on
each.
But first, why variants?
## Why Variants?
You have two basic options for sharing styles across components: share style
objects and then combine them directly into your render function, or allow
abstraction in some form.
You can think of `StyleSheet.create` as "a lot of style objects". While this is
nice in that it's simple, it doesn't enforce any rules, which can get you in
trouble with both a compiler and a type system.
You end up basically typing props by hand and then doing somewhat arbitrary
logic to glue it all together inside a functional component. The types may not
map to the actual output, and a compiler will almost certainly get confused or
be unable to optimize with any easily unforeseen abstraction.
Variants force you out of the React render function, which means no hooks and a
much clearer contract of limitations: at most you take in a value, your design
system, and props, and you output a group of styles.
It's nice having this special area just for styling. It keeps your types correct
by definition. And it ensures the optimizing compiler can understand your styled
components and people on your team won't "de-opt" it on accident.
And because Variants work with the styled function, they nest without adding an
extra depth to your render tree. Doing `styled(styled())` results in a single
React component, and can be optimized and flattened by the compiler as well.
Whereas it's quite easy for developers to take an existing functional component
and throw in a new one around it, further de-optimizing a compiler and leading
to a less clear separation of styled components and regular ones.
## 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.
```tsx
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, Number Variants
You can use a pseudo Typescript syntax for other variants:
- `:string` - Accepts a `string`
- `:boolean` - Accepts a `boolean` (less precedence than `true` or `false`)
- `:number` - Accepts a `number`
```tsx
import { View, styled } from 'tamagui' // or '@tamagui/core'
export const ColorfulView = styled(View, {
variants: {
color: {
':string': (color) => {
// color is of type "string"
return {
color,
borderColor: color,
}
},
},
} as const,
})
```
### Spread 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. For example, without spread
variants, if you wanted to have a `pad` property that accepted all the keys from
`tokens.size`, you'd have to write this:
```tsx
// 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 only gets more verbose if you add more sizes. It would
require always updating every component every time you change the tokens.
Spread variants solve this problem. Instead, we can write:
```tsx
// 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: {
'...size': (val, { tokens }) => ({
padding: tokens.size[val]
}),
}
} as const
})
// now you can
<MyButton pad="$lg" />
```
Spread variants save you from having to define hardcoded styles for every key
(`sm`, `md`, `lg`) in your token object. They collect values from any of your
top level token categories. So you can only use `...color`, `...size`,
`...space`, `...font`, `...fontSize`, `...lineHeight`, `...radius`,
`...letterSpace`, or `...zIndex`. They must be prefixed with `...` as that is
how they are typed properly and assembled for runtime.
#### Extra properties passed to functional variants
There's a second argument passed to all variant functions that is a
bag-o-goodies that help you use the current tokens, theme, props, and fonts
easily.
```tsx
const SizableText = styled(Text, {
variants: {
size: {
'...size': (size, { tokens, font }) => {
return {
fontSize: font?.size,
lineHeight: font?.lineHeight,
height: tokens.size[size] ?? size,
}
},
},
} as const,
})
```
Which you can use:
```tsx
<SizableText size="$md">Hello world</SizableText>
```
The Spread variant function will receive two arguments: the first is the value
given to the property (`"$lg"`), and the second is an object with
`{ theme, tokens, props, font, context }`.
<PropsTable
data={[
{
name: 'theme',
type: 'ThemeParsed',
description: `A proxy to your theme that lets you access all theme values using normal keys, or with a "$" prefix.`,
},
{
name: 'tokens',
type: 'TokensParsed',
description: `All your tokens given to createTamagui, ensuring the keys all start with "$".`,
},
{
name: 'props',
type: 'Props',
description: `All props passed to the styled component.`,
},
{
name: 'font',
type: 'Font',
description: `Maybe undefined. A single resolved Font that you passed to you fonts config.`,
},
{
name: 'fontFamily',
type: 'string',
description: `Maybe undefined. The name of the current fontFamily.`,
},
{
name: 'fonts',
type: 'string',
description: `All your fonts passed to createTamagui.`,
},
{
name: 'context',
type: 'StyledContext',
description: `The styledContext values from the parent component, useful for compound components.`,
},
]}
/>
#### Catch-all variants
Much like a dynamic variant, except it lets you use it alongside the other typed
variants you need. Use '...' and it will grab all variants that don't match:
```tsx
import { View, styled } from 'tamagui' // or '@tamagui/core'
export const ColorfulView = styled(View, {
variants: {
colorful: {
true: {
color: 'red',
},
'...': (val: string) => {
// this will catch any other values that don't match
return {
color: val,
}
},
},
} as const,
})
```
#### Dynamic variants
If you need more complex types, or simply prefer a shorter syntax, you can use a
single function instead of using the object syntax for variants:
```tsx
import { View, styled } from 'tamagui' // or '@tamagui/core'
export const MyView = styled(View, {
variants: {
doubleMargin: (val: number) => ({
margin: val * 2,
}),
} as const,
})
```
Tamagui also provides a few types of other variant definition patterns that work
with tokens or types.
### 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:
```tsx
const Square = styled(View, {
variants: {
size: {
'...size': (size, { tokens }) => {
// size === '$lg'
// tokens.size.$lg === 25
return {
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 Pseudos, Media Queries
Variants have the full power of the Tamagui styling system, including pseudo and
media styles:
```tsx
const SizedText = styled(Text, {
variants: {
size: {
md: {
fontSize: '$sm',
$gtMd: {
fontSize: '$md',
},
$gt2xl: {
fontSize: '$lg',
},
},
},
} as const,
})
```
### Variants and Parent Variants
Styled components can access their parent components variants, even in their
variants:
```tsx
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,
})
```
## core/view-and-text
---
title: View & Text
description: Your base components
---
View and Text are functionally equivalent to React Native `View` and `Text`, they just accept the superset of props that Tamagui supports.
### Props
See [the Props docs](/docs/intro/props) for the full list of properties View and Text accept.
### Usage
You can use them directly:
```tsx
import { View, Text } from 'tamagui' // or '@tamagui/core'
export default () => (
<View margin={10}>
<Text color="$color">Hello</Text>
</View>
)
```
We encourage you to use inline styles. Combined with [shorthands](/docs/core/configuration#shorthands) they can lead to really easy styling, and the Tamagui optimizing compiler will take care of optimizing everything for you so that there is little to no extra cost in performance:
```tsx
import { View, Text } from 'tamagui' // or '@tamagui/core'
export default () => (
<View mx="$sm" scale={1.2}>
<Text c="$color">Hello</Text>
</View>
)
```
<Notice theme="blue">
One really important and useful thing to note about Tamagui style properties: the order
is important! [Read more here](/docs/intro/styles#order-is-important)
</Notice>
### With styled()
You can also use them [with styled](/docs/core/styled) to create your own lower level views that are meant to be re-usable:
```tsx
import { 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: {
'...size': (size, { tokens }) => {
return {
width: tokens.size[size] ?? size,
height: tokens.size[size] ?? size,
}
},
},
} as const,
})
```
Inline styles and `styled()` both are optimized by the compiler, so you can author styles using both depending on the use case.
## guides/cli
---
title: Tamagui CLI
description: Command-line tools for building, checking, and managing Tamagui projects
---
<IntroParagraph>
The Tamagui CLI provides a suite of command-line tools for optimizing components,
managing dependencies, adding fonts and icons, and more.
</IntroParagraph>
<Notice theme="gray">
**Just starting out?** Skim this section, the CLI is not necessary to get going.
</Notice>
## Installation
Install the CLI as a development dependency:
```bash
yarn add -D @tamagui/cli
```
## Commands
### build
Pre-compile Tamagui components in-place for production builds. This is useful
for bundlers that don't have a Tamagui plugin yet (like Turbopack) or when you
want a simple setup that works with any bundler.
```bash
# Build all components in a directory (web + native by default)
npx tamagui build ./src
# Build for web only
npx tamagui build --target web ./src
# Build for native only
npx tamagui build --target native ./src
# Build a specific file
npx tamagui build ./src/components/MyComponent.tsx
# Include/exclude patterns
npx tamagui build --include "components/**" --exclude "**/*.test.tsx" ./src
# Output to a separate directory (source files unchanged)
npx tamagui build --output ./dist ./src
# Create platform-specific files next to source files (.web.tsx or .native.tsx)
npx tamagui build --target native --output-around ./src
# Preview changes without writing files
npx tamagui build --dry-run ./src
```
**Flags:**
- `--target <platform>` - Target platform: `web`, `native`, or `both` (default:
`both`)
- `--include <pattern>` - Glob pattern to include files
- `--exclude <pattern>` - Glob pattern to exclude files
- `--output <path>` - Output directory for optimized files (preserves directory
structure). When specified, source files are not modified
- `--output-around` - Create platform-specific files (`.web.tsx` or `.native.tsx`)
next to source files instead of modifying them. Errors if the file already exists
- `--expect-optimizations <number>` - Fail if fewer than this many components
are optimized (useful for CI)
- `--dry-run` - Preview what would be optimized without writing any files
- `--debug` - Enable debug output
- `--verbose` - Enable verbose debug output
**Platform-Specific File Handling:**
The CLI automatically handles platform-specific files:
- Files with `.web.tsx` or `.ios.tsx` extensions are optimized for web only
- Files with `.native.tsx` or `.android.tsx` extensions are optimized for native
only
- Base files (`.tsx`) without platform-specific versions are optimized for all
platforms
- If both `.web.tsx` and `.native.tsx` exist, the base `.tsx` file is skipped
**Configuration:**
Create a `tamagui.build.ts` config file in your project root:
```ts
import type { TamaguiBuildOptions } from 'tamagui'
export default {
config: './tamagui.config.ts',
components: ['tamagui'],
importsWhitelist: ['constants.js', 'colors.js'],
outputCSS: './public/tamagui.generated.css',
} satisfies TamaguiBuildOptions
```
**Integration Examples:**
The CLI can wrap your build command using `--`, which optimizes files beforehand
and automatically restores them after:
```json
{
"scripts": {
"build": "tamagui build --target web ./src -- next build"
}
}
```
The `--` separator tells the CLI to run `next build` after optimization, then
restore your source files automatically. This is the recommended approach as it
keeps your source files unchanged.
Alternatively, run `tamagui build` separately (files will remain modified):
```json
{
"scripts": {
"build": "tamagui build --target web ./src && next build"
}
}
```
**Important:** Without the `--` wrapper, files are modified in-place and not
restored. Only use this approach if you're building in a CI environment where
the files are discarded anyway.
Alternatively, use `--output` to write optimized files to a separate directory:
```json
{
"scripts": {
"build": "tamagui build --target web --output ./dist ./src && next build"
}
}
```
With `--output`, source files are never modified and no restoration is needed.
For more details, see the
[Compiler Installation guide](/docs/intro/compiler-install#cli-based-in-place-compilation).
### check
Check your dependencies for inconsistent versions across your project. This
helps identify version mismatches that can cause issues, especially in
monorepos.
```bash
npx tamagui check
```
**Flags:**
- `--debug` - Enable debug output
- `--verbose` - Enable verbose debug output
**Example output:**
The command will scan your project and report any packages with mismatched
versions, helping you maintain consistency across your dependencies.
### generate
Build your entire Tamagui configuration and output CSS. This is useful for
pre-generating your design system's CSS and validating your configuration.
```bash
npx tamagui generate
```
**Flags:**
- `--debug` - Enable debug output
- `--verbose` - Enable verbose debug output
**What it does:**
1. Loads and validates your Tamagui configuration
2. Generates CSS for all your tokens, themes, and components
3. Outputs to the `.tamagui` directory
4. Also generates an LLM-friendly prompt file at `.tamagui/prompt.md`
### generate-css
Generate the `tamagui.generated.css` file from your configuration. Useful for build
pipelines or when you need to regenerate CSS without running a full build.
```bash
npx tamagui generate-css
```
**Flags:**
- `--output <path>` - Custom output path (default: `tamagui.generated.css`)
- `--debug` - Enable debug output
- `--verbose` - Enable verbose debug output
**Example:**
```bash
npx tamagui generate-css --output ./public/styles/tamagui.generated.css
```
### generate-themes
Pre-build your theme configuration for faster runtime performance. This
generates optimized theme objects from your theme definitions.
```bash
npx tamagui generate-themes <input-path> <output-path>
```
**Example:**
```bash
npx tamagui generate-themes ./themes/input.ts ./themes/generated.ts
```
**Flags:**
- `--debug` - Enable debug output
- `--verbose` - Enable verbose debug output
**Use case:** If you have complex theme generation logic, this command
pre-computes your themes at build time rather than runtime.
### add
<Notice theme="green">
**Tamagui Pro** - This command is available exclusively to Tamagui Pro members.
</Notice>
Add pre-configured fonts and icons from Tamagui's curated collections. This
includes Google Fonts and Iconify icon packs.
```bash
# Add a font
npx tamagui add font [packages-path]
# Add an icon pack
npx tamagui add icon [packages-path]
```
**Flags:**
- `--debug` - Enable debug output
- `--verbose` - Enable verbose debug output
**Interactive selection:**
The command will present an interactive menu where you can search and select
from available fonts or icons:
- **Fonts**: Browse Google Fonts with weight, style, and subset information
- **Icons**: Browse Iconify collections with icon counts and license information
**Default path:** If you don't provide a path, packages are installed to
`./packages` in your current directory.
**Requirements:** This feature requires
[Tamagui Pro](https://tamagui.dev/takeout) membership for access to the font and
icon repositories.
### generate-prompt
Generate an LLM-friendly markdown file from your Tamagui configuration. This
creates comprehensive documentation of your design system for use with AI
assistants.
```bash
npx tamagui generate-prompt
```
**Flags:**
- `--output <path>` - Custom output path (default: `.tamagui/prompt.md`)
- `--debug` - Enable debug output
**What it includes:**
- All your tokens (colors, sizes, space, etc.)
- Theme definitions and variants
- Component configurations
- Font families and configurations
- Media queries and breakpoints
**Use case:** Share this file with AI assistants like Claude or ChatGPT to get
better suggestions that align with your design system.
## Global Flags
All commands support these flags:
- `--help` - Show help for the command
- `--version` - Show CLI version
- `--debug` - Enable debug output
- `--verbose` - Enable verbose debug output (more detailed than `--debug`)
## Examples
### Production Build Pipeline
```json
{
"scripts": {
"build": "tamagui check && tamagui build --target web ./src -- next build"
}
}
```
### CI with Optimization Verification
```json
{
"scripts": {
"build": "tamagui build --target web --expect-optimizations 10 ./src -- next build"
}
}
```
This fails the build if fewer than 10 components are optimized, helping catch
configuration issues in CI.
### Cross-Platform Mobile App
```json
{
"scripts": {
"build:ios": "tamagui build --target native ./src -- eas build --platform ios",
"build:android": "tamagui build --target native ./src -- eas build --platform android",
"build:web": "tamagui build --target web ./src -- vite build"
}
}
```
## Troubleshooting
### Build command fails with "cannot find module"
Make sure you have a `tamagui.build.ts` config file that correctly points to
your configuration:
```ts
export default {
config: './tamagui.config.ts', // Verify this path is correct
components: ['tamagui'],
}
```
### Check command reports false positives
The check command is strict about version consistency. If you have a specific
reason for version mismatches (like testing), you can document them in your
README.
### Add command shows "Repository not found"
The `add` command requires Tamagui Takeout access. Visit
[tamagui.dev/takeout](https://tamagui.dev/takeout) to learn more.
## guides/create-tamagui-app
---
title: Bootstrapping
description: Starter kits for React Native
---
Set up a starter repo to help you learn about and bootstrap your Tamagui app:
### Templates
There are four starter templates:
- Free - Expo, Expo Router, Solito and Next.js (with both `/pages` and `/app`
dir).
- Learn - Client-only web app with Webpack or Vite. Useful to understand how to
set up tamagui.config.ts.
- [Expo Router](/docs/guides/expo) - The Expo Router default starter with
Tamagui added via `@tamagui/metro-plugin`.
- [Takeout](/takeout) - A more robust, paid and supported starter kit.
### Usage
```bash
npm create tamagui@latest
```
Check out
[the source of the templates](https://github.com/tamagui/starter-free).
A big shout out to [Fernando Rojo](https://twitter.com/fernandotherojo) for
creating [Solito](https://solito.dev), a great library for sharing all your
views between Expo and Next.js, and the bootstrap repo we borrowed from.
### Starting out
The starter-free app requires yarn to run.
To run the app:
```bash
cd myapp
yarn
yarn web # Web local dev
yarn native # Expo local dev (only for `starter-free`)
```
## 📦 Included packages (`starter-free`)
- `tamagui` for cross-platform views, themes and animations
- `solito` for cross-platform navigation
- `expo-router` for having same routing concepts between web and native
- Expo SDK 48
- Next.js 13
By integrating `Tamagui`, `expo-router`, `Solito`, and `Next.js` you can enjoy
the power and simplicity of creating universal apps.
## 🗂 Folder layout
- `apps` entry points for each app
- `expo`
- `next`
- `packages` shared packages across apps
- `app` you'll be importing most files from `app/`
- `features` (don't use a `screens` folder. organize by feature.)
- `provider` (all the providers that wrap the app, and some no-ops for Web.)
- `navigation` only for starter-free template, contains the navigation code
for RN
You can add other folders inside of `packages/` if you know what you're doing
and have a good reason to.
## 🏁 Start the app
- Install dependencies: `yarn`
- Next.js local dev: `yarn web`
- Runs `yarn next`
- Expo local dev: `yarn native`
- Runs `expo start`
## 🆕 Add new dependencies
### Pure JS dependencies
If you're installing a JavaScript-only dependency that will be used across
platforms, install it in `packages/app`:
```bash
cd packages/app
yarn add date-fns
cd ../..
yarn
```
### Native dependencies
If you're installing a library with any native code, you must install it in
`apps/expo`:
```bash
cd apps/expo
yarn add react-native-reanimated
cd ../..
yarn
```
You can also install the native library inside of `packages/app` if you want to
get autoimport for that package inside of the `app` folder. However, you need to
be careful and install the _exact_ same version in both packages. If the
versions mismatch at all, you'll potentially get terrible bugs. This is a
classic monorepo issue. I use `lerna-update-wizard` to help with this (you don't
need to use Lerna to use that lib).
## guides/design-systems
---
title: Design Systems
description: Put together your own design system.
---
Tamagui allows you to build out your own set of components that are optimized
with the compiler whenever they are used in your app.
By default, if you just use `styled()` in your app, Tamagui **won't** be able to
optimize those components. This is because the compiler needs to know about
those components at build-time.
Let's break down how to set this up in more detail.
## Step 1: Create a package
Your design system needs to live in its own npm module, which can be private to
just your app. That way you can later direct the compiler to look for that
package.
Design systems can extend from each other. In fact, `tamagui` extends
`@tamagui/core`, which contains simple base-level components.
So, for example, if you'd like to use the Tamagui `XStack`, `YStack`, `Button`
and `Paragraph` in your design system, you would add `tamagui` to your design
system's package.json.
If you want to build more from scratch, then use `@tamagui/core` and only import
either the `View` or `Text` components. For the purpose of this guide, we'll use
`@tamagui/core`.
Add package.json:
```json
{
"name": "@ourapp/components",
"types": "./types/index.d.ts",
"main": "dist/cjs",
"module": "dist/esm",
"module:jsx": "dist/jsx",
"files": ["types", "src", "dist"],
"sideEffects": ["*.css"],
"dependencies": {
"@tamagui/core": "*"
},
"scripts": {
"build": "tamagui-build",
"watch": "tamagui-build --watch"
},
"devDependencies": {
"@tamagui/build": "*"
}
}
```
If using Typescript, a `tsconfig.json` of your choosing:
```json fileName="tsconfig.json"
{
"compilerOptions": {
"types": ["node", "react"],
"lib": ["dom", "esnext"],
"baseUrl": "./",
"outDir": "dist"
},
"include": ["./**/*.ts"],
"exclude": ["node_modules/**/*"]
}
```
There are a few things to note here:
- We're using `@tamagui/build` to build this package, which is a small script
built around `esbuild` and `typescript` that makes sure you output your
components _with JSX preserved_.
- We then set `module:jsx`, which then needs to be added to your webpack
`resolve.mainFields`.
- The Next.js plugin handles this for you automatically.
- `sideEffects` field is important, otherwise webpack will remove the generated
CSS in production.
## Step 2: Create your design system
Check the [configuration](/docs/core/configuration) for more detail on this
step.
You'll be creating a `tamagui.config.ts` at the root of your app. It will
contain a full suite of tokens, themes, and fonts exported onto a single named
`config` export.
## Step 3: Define and export components
Now, create and export your components. You can re-export components from
`tamagui` or `@tamagui/core` as well. Let's create a Circle component:
`Circle.tsx`:
```tsx
import { GetProps, YStack, styled } from 'tamagui' // or '@tamagui/core' if extending just that
export const Circle = styled(YStack, {
alignItems: 'center',
justifyContent: 'center',
borderRadius: 100_000_000,
overflow: 'hidden',
variants: {
size: {
'...size': (size, { tokens }) => {
return {
width: tokens.size[size] ?? size,
height: tokens.size[size] ?? size,
}
},
},
},
})
export type CircleProps = GetProps<typeof Circle>
```
And then export from `index.tsx`:
```tsx
export * from './Circle'
```
## Step 4: Set up your build
Now in your app, add `@ourapp/components` and `tamagui` (since we are extending
it) to your package.json, and update your tamagui build configuration.
### Webpack
In your `webpack.config.js`:
```js
{
loader: 'tamagui-loader',
options: {
config: './tamagui.config.ts',
components: ['@ourapp/components', 'tamagui'],
},
}
```
### Next.js
In your `next.config.js`:
```js
export default withPlugins([
withTamagui({
config: './tamagui.config.ts',
components: ['@ourapp/components', 'tamagui'],
}),
])
```
### React Native
In your `babel.config.js`:
```js
export default {
plugins: [
[
'@tamagui/babel-plugin',
{
exclude: /node_modules/,
config: './tamagui.config.ts',
components: ['@ourapp/components', 'tamagui'],
},
],
],
}
```
### Configuration notes
You only need `tamagui` in your `components` array if you extend it, otherwise
no need. You can also extend your own base level module so long as they export
Tamagui `styled` components.
## Step 5: Test it out
In your app, you should now be able to import and use your Circle component.
Using the debug pragma, you can also verify the extraction is working. Make sure
the build settings `logTimings: true` and `disableExtraction: false` are set so
you can see the compiler at work:
Anywhere in your app:
```tsx
import { Circle } from '@ourapp/components'
export default () => <Circle size="$large" />
```
When it compiles you should see something like:
```bash
tamagui built app.tsx in 16ms (1 optimized, 1 flattened)
```
To get more information on any extraction, use the `// debug` pragma:
```tsx
// debug
// ^ the above pragma will direct Tamagui to output a lot of information on the extraction
import { Circle } from '@ourapp/components'
export default () => <Circle size="$large" />
```
You should see much more log output with details on how it extracted, including
the final CSS and JS.
## guides/developing
---
title: Developing with Tamagui
description: Tips and tricks
---
Tamagui includes many helpful tools for debugging UI.
### Debugging
Tamagui has several ways of giving you more insight into what's happening at
compile-time.
#### Visualizer
You can set up a simple visualizer that shows a heads-up display when you
hold "Option" (or any key you define) after a short delay, revealing
the styled component names and even the file numbers and components they are in
(if you have the compiler plugin installed) as an overlay over your app in
development mode.
```tsx
import { setupDev } from '@tamagui/core'
setupDev({
// can just be true as well for defaulting to key: Alt + delay: 800
visualizer: {
key: 'Alt',
delay: 800,
},
})
```
#### Debug Pragma
To see what's being extracted, why, and every step along the optimization
pipeline, add `// debug` to the top of any file. Adding `// debug-verbose` will
show even more information, including more granular timings.
If you're developing in your design system package that is built with
@tamagui/build, esbuild will strip this banner. You can try using `//! debug`
(esbuild only preserves comments at the top that start with `//!`), but still
occasionally esbuild will insert a helper above the comment, breaking it, so be
sure to check the built file in `dist/jsx`.
#### Debug Prop
Adding `debug` to any Tamagui component outputs detailed information about
what's happening at runtime. Use it like so:
```tsx
import { Button } from 'tamagui'
export default () => <Button debug>Hello world</Button>
```
You'll see props, styles, and a variety of variables relevant to processing
them.
You can do `<Button debug="break" />` to have it break at the beginning of
rendering, or `<Button debug="verbose" />` to have it output more detailed debug
information.
#### DEBUG Environment Variable
If you set `DEBUG=tamagui` before your build command, you get the full
debug output of every file built. This is useful for seeing everything that's
happening across every file and is especially helpful for diagnosing production
issues.
### Runtime introspection
In development mode, Tamagui sets the variable `Tamagui` onto `globalThis` with
a lot of helpful internals, including your entire parsed config from
`tamagui.config.ts`.
Beyond your config, you have:
- **allSelectors**: All the selectors inserted by Tamagui (before runtime).
#### Inspecting Components
Any `styled()` component will have a `staticConfig` property attached to it:
```tsx
const Circle = styled(View, {
borderRadius: 1000,
})
console.log(Circle.staticConfig) // lots of helpful information
```
- `componentName` is taken from the `name` key
- `variants` contains the merged variants including parents.
- `defaultProps` is the extracted props left to use as defaults on the
component.
### Classes generated
Tamagui generates helpful CSS classes. For components created with `styled()`
where a `name` is set like this:
```tsx
const MyButton = styled(View, {
name: 'MyButton',
backgroundColor: 'red',
})
```
Tamagui adds the classname `is_MyButton`. This is a useful escape hatch for
attaching CSS to any extra component. All the default Tamagui components have
their name set.
For components that extend a Text-based component, a further classname is set of
the format `font_[fontFamily]`. So if you do:
```tsx
<Paragraph fontFamily="$body" />
```
The classnames `is_Paragraph` and `font_body` will be output to DOM.
## guides/expo
---
title: Expo Guide
description: How to set up Tamagui with Expo
---
We've created a new template repo for starting an Expo Router app based on
the Expo starter repo.
<Notice theme="blue">
This template requires Yarn 4.4.0 or greater. You can set yarn to the latest version by
running `yarn set version stable`.
</Notice>
```bash
yarn create tamagui@latest --template expo-router
```
There are also [pre-made community Expo starters](/community).
## Install
Use this guide to set up Tamagui with Expo Native and Web.
<Notice theme="blue">
To support dark mode, update your `app.json` to `app.config.ts` and set
`userInterfaceStyle` to `"automatic"`.
</Notice>
### Native
Create a new [Expo](https://docs.expo.dev/get-started/create-a-project/#initialize-a-new-project) project:
```bash
yarn dlx create-expo-app -t expo-template-blank-typescript
```
<Notice theme="blue">
This guide assumes Expo is configured with TypeScript support.
</Notice>
The following steps are optional but useful for many apps. They enable the optimizing compiler, Reanimated, and support for `process.env.XYZ` environment variables.
Add `@tamagui/babel-plugin`:
```bash
yarn add @tamagui/babel-plugin
```
If you have a `tamagui.build.ts` (recommended — see
[compiler install docs](/docs/intro/compiler-install#configuration-with-tamaguibuildts)),
no options are needed:
Update your `babel.config.js` to include the optional `@tamagui/babel-plugin`:
```js fileName="babel.config.js"
module.exports = function (api) {
api.cache(true)
return {
presets: ['babel-preset-expo'],
plugins: [
'@tamagui/babel-plugin',
],
}
}
```
<Notice>
If you're using a monorepo you probably want to use [this Metro
configuration](/docs/guides/metro#native).
</Notice>
### Expo Router / Web
- First, follow the [Metro configuration guide](/docs/guides/metro#web-support) to enable web support.
Add `@tamagui/config` and `tamagui` to your `package.json` and install them. Then create a `tamagui.config.ts`:
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v5'
import { createTamagui } from 'tamagui'
export const tamaguiConfig = createTamagui(defaultConfig)
export default tamaguiConfig
export type Conf = typeof tamaguiConfig
declare module 'tamagui' {
interface TamaguiCustomConfig extends Conf {}
}
```
Then update `app/_layout.tsx`:
```tsx showMore fileName="app/_layout.tsx"
import '../tamagui.generated.css'
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
import { Stack } from 'expo-router'
import { useColorScheme } from 'react-native'
import { TamaguiProvider } from 'tamagui'
import { tamaguiConfig } from '../tamagui.config'
export default function RootLayout() {
const colorScheme = useColorScheme()
return (
// add this
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme!}>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
</ThemeProvider>
</TamaguiProvider>
)
}
```
### Setup Tamagui
From here on out you can follow the [Installation](/docs/intro/installation) and [Configuration](/docs/core/configuration) docs.
## Loading fonts
Install the `expo-font` package:
```bash
npx expo install expo-font
```
Load your fonts so React Native can recognize them. There are several ways to do this:
<Spacer size="$6" />
<InlineTabs id="font" defaultValue="tamagui">
<InlineTabs.List>
<InlineTabs.Tab value="tamagui">Tamagui</InlineTabs.Tab>
<InlineTabs.Tab value="expo">Expo Google Fonts</InlineTabs.Tab>
</InlineTabs.List>
<InlineTabs.Content value="tamagui">
Use the `@tamagui/font-inter` package, a pre-configured version of the Inter font that works with Tamagui:
Import the `useFonts` hook and load the fonts:
```tsx fileName="App.tsx"
import { useFonts } from 'expo-font'
function App() {
const [loaded] = useFonts({
Inter: require('@tamagui/font-inter/otf/Inter-Medium.otf'),
InterBold: require('@tamagui/font-inter/otf/Inter-Bold.otf'),
})
useEffect(() => {
if (loaded) {
// can hide splash screen here
}
}, [loaded])
if (!loaded) {
return null
}
return <MyApp />
}
```
</InlineTabs.Content>
<InlineTabs.Content value="expo">
Use the `@expo-google-fonts` package, a collection of Google Fonts that work with Expo:
<Notice theme="green">
You can find the [full list of
fonts](https://github.com/expo/google-fonts/tree/master/font-packages) and usage
instructions in the [Expo documentation](https://github.com/expo/google-fonts).
</Notice>
Install the font package:
```bash
npx expo install @expo-google-fonts/inter
```
Import the `useFonts` hook and load the fonts:
```tsx fileName="App.tsx"
import { useFonts, Inter_400Regular, Inter_900Black } from '@expo-google-fonts/inter'
function App() {
const [loaded] = useFonts({
Inter_400Regular,
Inter_900Black,
})
useEffect(() => {
if (loaded) {
// can hide splash screen here
}
}, [loaded])
if (!loaded) {
return null
}
return <MyApp />
}
```
</InlineTabs.Content>
</InlineTabs>
<Notice>
For more information on loading fonts in Expo, see the [Expo
documentation](https://docs.expo.dev/develop/user-interface/fonts/).
</Notice>
## First time starting Expo
The first time running your project with Tamagui, be sure to clear the cache:
```bash
npx expo start -c
```
Your `package.json` scripts should look something like this:
```json fileName="package.json"
{
"scripts": {
"start-native": "expo start -c",
"start-web": "expo start -c",
"android": "yarn expo run:android",
"ios": "yarn expo run:ios",
"web": "expo start --web"
}
}
```
## guides/how-to-build-a-button
---
title: How to Build a Button
description: Learn how to create a powerful yet simple API for a button with Tamagui and
compound components
hero: HowToBuildAButton
demoName: BuildAButton
---
<HeroContainer noScroll>
<BuildAButtonDemo />
</HeroContainer>
<IntroParagraph>
Deceptively innocuous, designing a robust yet flexible API even for a Button can be
filled with surprising challenges.
</IntroParagraph>
More than just text in a box, buttons need icons, themes, variants, sizes, often
loading indicators and several interactive states, and always a delicate balance
between sub-components' positions, spacing and style.
All of this ideally delivered in a simple API that still allows complete
customization.
Tamagui allows for building a button with "compound components", a concept
popularized by [Radix](https://www.radix-ui.com/). With the introduction of
`createStyledContext` in version 1.28 this pattern has become especially easy,
so we wrote this guide to show off building more advanced compound component
APIs. While this guide covers a button, this is applicable to many types of UI
components.
So what are compound components? Simply, it's when you have, say, a
`<Button />` that wants children `<Button.Text />` and `<Button.Icon />`.
And why do they exist? Well, we'll explain that and more in this guide.
Keep in mind that this is an advanced use case. Usually you can just grab
Tamagui components off the shelf and use them with inline styles. But sometimes
you need components that want multiple related children.
<Notice>
This guide is more of a primer on designing compound components, teaching advanced
concepts that aren't necessary if you just want to build your app. If you just want a
simple, ready to use Button, you can use the [Tamagui Button](/docs/components/button)
itself directly.
</Notice>
### The Final Result
To the point, here is the final code we'll end up with for our Button, ready to
copy and paste right into your app:
```tsx
import { getSize, getSpace } from '@tamagui/get-token'
import {
GetProps,
SizeTokens,
View,
Text,
createStyledContext,
styled,
useTheme,
withStaticProperties,
} from '@tamagui/web'
import { cloneElement, isValidElement, useContext } from 'react'
export const ButtonContext = createStyledContext({
size: '$md' as SizeTokens,
})
export const ButtonFrame = styled(View, {
name: 'Button',
context: ButtonContext,
backgroundColor: '$background',
alignItems: 'center',
flexDirection: 'row',
hoverStyle: {
backgroundColor: '$backgroundHover',
},
pressStyle: {
backgroundColor: '$backgroundPress',
},
variants: {
size: {
'...size': (name, { tokens }) => {
return {
height: tokens.size[name],
borderRadius: tokens.radius[name],
// note the getSpace and getSize helpers will let you shift down/up token sizes
// whereas with gap we just multiply by 0.2
// this is a stylistic choice, and depends on your design system values
gap: tokens.space[name].val * 0.2,
paddingHorizontal: getSpace(name, {
shift: -1,
}),
}
},
},
} as const,
defaultVariants: {
size: '$md',
},
})
type ButtonProps = GetProps<typeof ButtonFrame>
export const ButtonText = styled(Text, {
name: 'ButtonText',
context: ButtonContext,
color: '$color',
userSelect: 'none',
variants: {
size: {
'...fontSize': (name, { font }) => ({
fontSize: font?.size[name],
}),
},
} as const,
})
const ButtonIcon = (props: { children: any }) => {
const { size } = useContext(ButtonContext.context)
const smaller = getSize(size, {
shift: -2,
})
const theme = useTheme()
return isValidElement(props.children)
? cloneElement(props.children, {
size: smaller.val * 0.5,
color: theme.color.get(),
})
: null
}
export const Button = withStaticProperties(ButtonFrame, {
Props: ButtonContext.Provider,
Text: ButtonText,
Icon: ButtonIcon,
})
```
Now you can use your button like so:
```tsx
export default () => (
<Button>
<Button.Icon>
<Moon />
</Button.Icon>
<Button.Text>hi</Button.Text>
</Button>
)
// multiple icons:
export default () => (
<Button>
<Button.Icon>
<Moon />
</Button.Icon>
<Button.Text>hi</Button.Text>
<Button.Icon>
<Moon />
</Button.Icon>
</Button>
)
```
### Styling Text Color
In v2, `color` is not a direct prop on the Button frame. Instead, style your
text through `Button.Text`. For interactive color changes on hover or press, use
Tamagui's `group` prop to coordinate styles between parent and child:
```tsx
export default () => (
<Button group="btn" bg="pink">
<Button.Text color="blue" $group-btn-hover={{ color: 'red' }}>
Hello world
</Button.Text>
</Button>
)
```
By setting `group="btn"` on the Button frame, any child can reference that group
for interactive styles using `$group-btn-hover`, `$group-btn-press`, and so on.
This is more flexible and explicit than passing `color` directly, as it lets you
independently control text, icon, and frame styles for every interaction state.
This may only be about a hundred lines of code, but there's a lot to take in. And
behind the scenes, there's a lot going on to make this possible.
### From the Start
This guide will build up to this complete example from the ground up, which
should help explain both why many patterns exist, and also how they work.
We start with the assumption that we have a design system set up with tokens
that use keys `sm`, `md`, and `lg` and a few themes:
```tsx
import { createTamagui, createTokens } from '@tamagui/core'
export default createTamagui({
tokens: createTokens({
size: {
sm: 38,
md: 46,
lg: 60,
},
space: {
sm: 15,
md: 20,
lg: 25,
},
radius: {
sm: 4,
md: 8,
lg: 12,
},
// ... the rest of your tokens
}),
themes: {
light: {
background: '#fff',
color: '#000',
},
// define a Button sub-theme, see the Themes docs for more
light_Button: {
background: '#ccc',
backgroundPress: '#bbb', // darker background on press
backgroundHover: '#ddd', // lighter background on hover
color: '#222',
},
},
// ... the rest of your tamagui.config.ts
})
```
If you'd like to see a more complete work-up of a Tamagui config, check out
[the Simple Web starter repo source code](https://github.com/tamagui/tamagui/blob/main/code/starters/simple-web/src/tamagui.config.ts)
or go ahead and create that starter using `npm create tamagui@latest`.
With that in mind, we can create the outer frame of the button fairly simply,
like so:
```tsx
import { View, styled } from '@tamagui/core'
const ButtonFrame = styled(View, {
// the name indicates to use the sub-theme `Button`
// since we defined light_Button, if our theme is light, this component
// will always use the values from our `light_Button` theme
name: 'Button',
alignItems: 'center',
flexDirection: 'row',
// our $ prefixed values look for theme first, then fallback to tokens
backgroundColor: '$background', // #ccc
hoverStyle: {
backgroundColor: '$backgroundHover', // #ddd
},
pressStyle: {
backgroundColor: '$backgroundPress', // #bbb
},
// these all use tokens
// note that size tokens are used only for these properties:
// width, height, minWidth, minHeight, maxWidth and maxHeight
height: '$md', // 46
// meanwhile our radius token is used here
borderRadius: '$md', // 8
// and space tokens are used for all others
paddingHorizontal: '$sm', // 25
})
```
This gets us a simple rounded rectangle that uses the `md` tokens from your
design system and the `background` styles from your theme.
We set the `name` property to `"Button"`, which tells Tamagui to check for a
sub-theme `Button` that extends the current one. So since we have a theme
`light` and we have a sub-theme `light_Button`, Tamagui finds the theme
`light_Button` and apply it to this component, getting the `background` value
from that theme (assuming we have our base theme set to light).
Finally we set the `hoverStyle` and `pressStyle` so our button frame looks nice
when hovered or pressed.
Next we'll want a Text component to go inside:
```tsx
import { Text, styled } from '@tamagui/core'
export const ButtonText = styled(Text, {
name: 'ButtonText',
color: '$color',
fontFamily: '$body',
fontSize: '$md',
lineHeight: '$md',
userSelect: 'none',
})
```
This is pretty similar to the frame, just with its own name and with a `color`
set rather than a `background`. Since we didn't define a `light_ButtonText`
theme, this will fall back to the color defined on the light theme. Finally,
button text usually isn't selectable so we set `userSelect` to none.
We can use our simple button. It's nicely themed, but only renders at one size:
```tsx
export default () => (
<Button>
<ButtonText>Hello world</ButtonText>
</Button>
)
```
So we'll make it sizeable. First, let's look at how we'd solve this without the
new `createStyledContext` helper, so we can understand why it exists.
We can add some variants:
```tsx
const ButtonFrame = styled(View, {
name: 'Button',
backgroundColor: '$background',
alignItems: 'center',
flexDirection: 'row',
variants: {
size: {
sm: {
height: '$sm',
borderRadius: '$sm',
gap: 4,
},
md: {
height: '$md',
borderRadius: '$md',
gap: 6,
},
lg: {
height: '$lg',
borderRadius: '$lg',
gap: 8,
},
},
} as const,
})
```
Still, this is somewhat repetitive and prone to typos. To avoid duplication while
also working even if we add new sizes to our design system in the future, we can
use the handy Tamagui [Spread Variants](/docs/core/variants#spread-variants):
```tsx
import { View, styled } from '@tamagui/core'
const ButtonFrame = styled(View, {
name: 'Button',
backgroundColor: '$background',
alignItems: 'center',
flexDirection: 'row',
variants: {
size: {
'...size': (name, { tokens }) => {
return {
height: tokens.size[name],
borderRadius: tokens.radius[name],
gap: tokens.space[name].val * 0.2,
}
},
},
} as const,
})
export const ButtonText = styled(Text, {
name: 'ButtonText',
color: '$color',
userSelect: 'none',
variants: {
size: {
'...fontSize': (name, { font }) => ({
fontSize: font?.size[name],
}),
},
} as const,
})
```
<Notice>
The `...size` spread variant `name` defaults to implicit any. Update `tsconfig.json`'s
`"noImplicitAny": false` to avoid TypeScript errors.
</Notice>
So now we can pass in our new `size` property:
```tsx
<ButtonFrame size="$md">
<ButtonText size="$md" />
</ButtonFrame>
```
Let's clean up these two components so they are more clearly meant to be
used together (and are easier to import for users):
```tsx
export const Button = ButtonFrame as typeof ButtonFrame & {
Text: typeof ButtonText
}
Button.Text = Text
```
What is this whole `typeof ButtonFrame` thing? It's just TypeScript being
awkward. Since Tamagui uses this pattern internally for many components, we've
made a small helper:
```tsx
import { withStaticProperties } from '@tamagui/core'
export const Button = withStaticProperties(ButtonFrame, {
Text: ButtonText,
})
```
Which is functionally the same as the above. So, now our users can do:
```tsx
import { Button } from './OurButton'
export default () => (
<Button size="$md">
<Button.Text size="$md">Hello world</Button.Text>
</Button>
)
```
### Improving the API
Great. We've gotten our custom Button exported. But there's something quite
unfortunate about our API as it stands, and that is that we have to always pass
`size` to both components. This is brittle and ugly.
One typical way to solve this would be to abstract both of these components into
our own React component:
```tsx
const Button = ({
size,
children,
textProps,
...props
}: ViewProps & {
textProps?: TextProps
}) => (
<ButtonFrame size={size} {...props}>
<ButtonText size={size} {...textProps}>
{children}
</ButtonText>
</ButtonFrame>
)
```
But suddenly we have issues. What if a user wants to show an icon? We need to
add `icon` and `iconProps`. And what if they want to change the order? You may
reach for something like `direction="reverse"`. But then what if they want a
loading indicator after, but an icon before. Or maybe they want to show big text
above, and smaller text below.
The problem with abstracting your Button into a single component is that the
internals are surprisingly flexible, and ideally they want to each be styled
independently.
Abstracting the tree structure that's output is our problem. The compound
component API gives us full control - we can customize every piece, even using
the existing `styled` function to re-style each piece, before re-exporting it
again:
```tsx
import { withStaticProperties } from '@tamagui/core'
import { Button } from './OurButton'
const CustomButtonFrame = styled(Button, {
// override some styles
})
const CustomButtonText = styled(Button.Text, {
// override some styles
})
export const CustomButton = withStaticProperties(CustomButtonFrame, {
Text: CustomButtonText,
})
export default () => (
<CustomButton>
<CustomButton.Text>Hello world</CustomButton.Text>
<CustomButton.Text size="$sm">(Smaller text)</CustomButton.Text>
</CustomButton>
)
```
Not to mention by staying with the `styled` world, the Tamagui optimizing
compiler extracts CSS and optimizes our Button components. Note that components
using `createStyledContext` won't be flattened, but CSS extraction and style
optimization still apply - see the [createStyledContext docs](/docs/core/create-styled-context)
for details.
So we want our compound component API both for ergonomics and for performance,
but we need some way to thread our `size` property down from the parent frame to
the inner sub-components.
### Solving size
In React this is typically what context is used for.
Here's how we'd solve for size using context in plain-old React, while still
keeping the compound API:
```tsx
import { createContext } from 'react'
import { SizeTokens, GetProps, withStaticProperties } from '@tamagui/core'
import { Button } from './OurButton'
const SizeContext = createContext<SizeTokens>('$md')
const ButtonFrame = Button.styleable(
({ size = '$md', ...props }: GetProps<typeof OGB.ButtonFrame>) => {
return (
<SizeContext.Provider value={size}>
<Button size={size} {...props} />
</SizeContext.Provider>
)
}
)
const ButtonText = Button.Text.styleable((props: GetProps<typeof OGB.ButtonText>) => {
const size = useContext(SizeContext)
return <Button.Text size={size} {...props} />
})
export const NewButton = withStaticProperties(ButtonFrame, {
Text: ButtonText,
})
```
What the hell is `ButtonText.styleable` and `ButtonFrame.styleable`?
While it's explained in [the styled docs](/docs/core/styled#styleable),
the short of it is: if you want a functional component that returns a styled
component _to then be able to be styled again_, you need `styleable`.
That's because merging things like themes, animations, variants, media queries,
and pseudo queries is quite complex, and if Tamagui doesn't know that there is a
functional component in between some styled components it can lead to unexpected
merging of styles.
More importantly, the above code is once again verbose, duplicative, and
brittle. For example sharing more than just `size` across these components would
require a pretty significant refactor of the context with a lot of logic
memoizing the context value properly. And, once again, by moving into functional
components, your base level views now won't be as optimized by the compiler.
### The solution, createStyledContext
Finally we get to use `createStyledContext`. We just change from `createContext`
to:
```tsx
import { createStyledContext } from '@tamagui/core'
export const ButtonContext = createStyledContext({
size: '$md' as SizeTokens,
})
```
This returns a slightly modified `React.Context` just like
`createContext` does, so you can use it with `useContext` later on. We
still export a `.context` object with the original type just in case there's any
issue.
And now, bringing back the full example, we add an extra property to each
`styled` component, `context`:
```tsx
import { View, styled, createStyledContext } from '@tamagui/core'
export const ButtonContext = createStyledContext({
size: '$md' as SizeTokens,
})
const ButtonFrame = styled(View, {
name: 'Button',
context: ButtonContext,
backgroundColor: '$background',
alignItems: 'center',
flexDirection: 'row',
variants: {
size: {
'...size': (name, { tokens }) => {
return {
height: tokens.size[name],
borderRadius: tokens.radius[name],
gap: tokens.space[name].val * 0.2,
}
},
},
} as const,
})
const ButtonText = styled(Text, {
name: 'ButtonText',
context: ButtonContext,
color: '$color',
userSelect: 'none',
variants: {
size: {
'...fontSize': (name, { font }) => ({
fontSize: font?.size[name],
}),
},
} as const,
})
export const Button = withStaticProperties(ButtonFrame, {
Text: ButtonText,
Props: ButtonContext.Provider,
})
```
...and we've finally arrived at our destination! We can now do the following:
```tsx
import { Button } from './OurButton'
export default () => (
<Button size="$md">
<Button.Text>Hello world</Button.Text>
</Button>
)
```
This time since `Button` knows the `size` property is defined in the `context`
it will automatically pass size down from Button to Text, just like our
hand-rolled version did.
But - we don't have to write terribly verbose and brittle code, we get nice
types and memoization automatically, and the optimizing compiler is happy.
Notice we also exported a new property on Button, `Button.Props`.
Since `createStyledContext` returns a regular `React.Context` value, this works
the same as any other React provider. We can now control the variant from
anywhere above in the React tree:
```tsx
import { Button } from './OurButton'
export default () => (
<Button.Props size="$md">
<Button>
<Button.Text>Hello world</Button.Text>
</Button>
</Button.Props>
)
```
The only difference from `createContext` is that the `createStyledContext`
Provider just takes props directly rather than inside a `value` object (and of
course the memoization comes for free).
### Adding an Icon
Are we done?
Not quite. One final common piece of a Button is having an icon that sizes and
colors properly as well. But an icon usually comes from a third-party
library, an SVG, or perhaps an icon font.
Let's add a new component, `Button.Icon` and make this work.
Instead of going through `styled`, this component will be just a plain
functional component. It will still work nicely with the Tamagui themes and
sizes, showing how you can use them together with external React components.
```tsx
import { getTokens, useTheme } from '@tamagui/core'
import * as React from 'react'
const ButtonIcon = (props: { children: React.ReactNode }) => {
const { size } = React.useContext(ButtonContext)
const tokens = getTokens()
const smallerSize = tokens.size[size].val * 0.5
const theme = useTheme()
return React.cloneElement(props.children, {
width: smallerSize,
height: smallerSize,
color: theme.color.get(),
})
}
// add it to your Button:
export const Button = withStaticProperties(ButtonFrame, {
Text: ButtonText,
Icon: ButtonIcon,
Props: ButtonContext.Provider,
})
```
Now we can use it like this, assuming your Icon accepts `width`, `height`, and
`color`:
```tsx
import { MyIcon } from 'some-icon-library'
export default () => (
<Button size="$lg">
<Button.Icon>
<MyIcon />
</Button.Icon>
<Button.Text>Hello world</Button.Text>
</Button>
)
```
Of course you can make your `Button.Icon` work a bit differently if you'd like,
say changing out `children` + `cloneElement` for something like
`<Button.Icon icon={MyIcon} />`. It's up to you.
### Conclusion
We hope this has been helpful in explaining a variety of Tamagui features and
some of the benefits and ideas behind compound components.
There's certainly further you can go in building out your Button, but we think
that in under 150 lines of code you're getting a nearly ideal API, fully typed
sizing and themes, and a great balance of customization to optimization. Note
that the compiler will still extract CSS and optimize these components, though
flattening won't apply when using `createStyledContext`.
If you are working on a smaller app, say one that shows only a few buttons at a
time or with a small set of button variations, then abstracting your Button into
a single functional Button component is totally fine.
But we'd encourage you to give the compound component API a try and see
how you like it.
With the new Tamagui APIs, we look forward to never having to carefully add a
forwardRef, re-jig the types, and then manually thread context just to share
some props between parent and child components, especially when even after all
of that we'd still somehow accidentally break memoization or scoped values!
## guides/how-to-upgrade
---
title: Upgrading from v1 to v2
description: A comprehensive guide for migrating your Tamagui project from v1 to v2
---
Tamagui v2 aligns with modern web standards, improves performance, and simplifies APIs. The migration is moderate -- mostly removing deprecated APIs, renaming props, and updating your config. This guide walks through every change.
## Prerequisites
Tamagui 2 requires:
- **React 19+**
- **React Native 0.81+** (with New Architecture support)
- **TypeScript 5+**
Upgrade these first before proceeding.
## 1. Update Dependencies
Bump all `@tamagui/*` and `tamagui` packages to `^2.0.0`:
```bash
# if using npm/yarn/pnpm, update all tamagui packages
npx tamagui check
```
### Removed packages
- `@tamagui/animations-moti` -- use `@tamagui/animations-reanimated` (same API)
- `@tamagui/image-next` -- use `Image` from `tamagui` directly
### Add resolutions
In monorepos, you may want to add resolutions to avoid duplicate copies of core packages (critical for context/provider to work):
```json
"resolutions": {
"@tamagui/core": "^2.0.0",
"@tamagui/web": "^2.0.0",
"tamagui": "^2.0.0"
}
```
## 2. Config Migration (v4 to v5)
You don't have to migrate to Config v5, and it changes some default style behaviors that may
cause some issues. It's easier to migrate to Tamagui 2 first, then Config v5 after if you do choose to.
Config v5 is mostly a superset of v5, but some media query names changed and some themes and colors are different.
### Update imports
```tsx
// before
import { defaultConfig } from '@tamagui/config/v4'
// after
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css' // animations are now separate
```
Animations are no longer bundled with the config. Choose your driver:
- `@tamagui/config/v5-css` -- CSS transitions (smallest bundle, web-only)
- `@tamagui/config/v5-rn` -- React Native Animated API
- `@tamagui/config/v5-reanimated` -- Reanimated (best native performance)
- `@tamagui/config/v5-motion` -- Motion (Web Animations API, experimental)
### Add animations to config
```tsx
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css'
import { createTamagui } from 'tamagui'
export const config = createTamagui({
...defaultConfig,
animations,
})
export type Conf = typeof config
declare module 'tamagui' {
interface TamaguiCustomConfig extends Conf {}
}
```
### Settings changes
All root-level `createTamagui` settings have moved into the `settings` object:
```tsx
// before (v1)
createTamagui({
defaultFont: 'body',
disableRootThemeClass: true,
// ...
})
// after (v2)
createTamagui({
...defaultConfig,
settings: {
...defaultConfig.settings,
// your overrides
},
})
```
**Removed settings:**
- `maxDarkLightNesting` -- removed entirely
- `cssStyleSeparator` -- removed entirely
- `themeClassNameOnRoot` -- handled by `addThemeClassName`
- `disableRootThemeClass` -- now part of `settings`
### Flex and position defaults
v5 changes two important defaults:
- `flexBasis` changes from `auto` to `0` (React Native standard)
- `position` changes from `relative` to `static` (browser default)
If your layout relies on the old behavior, restore it:
```tsx
settings: {
...defaultConfig.settings,
styleCompat: 'legacy', // restores flexBasis: auto
defaultPosition: 'relative', // restores position: relative
}
```
Otherwise, you may need to add `position="relative"` explicitly on containers that have absolutely-positioned children, and add `flexBasis="auto"` where needed.
### Media query renames
- `$2xl` → `$xxl`
- `$2xs` → `$xxs`
- `$max2Xl` → `$max-xxl`
- `$maxXl` → `$max-xl`
- `$maxLg` → `$max-lg`
- `$maxMd` → `$max-md`
- `$maxSm` → `$max-sm`
Max queries changed from camelCase to kebab-case. New height-based queries are also available (`$height-sm`, `$height-md`, etc.) and a `$pointerTouch` query.
Breakpoint values now match Tailwind CSS: 640, 768, 1024, 1280, 1536.
### Colors and themes
Colors have been updated to Radix Colors v3 with slightly different values. Legacy colors are available at `@tamagui/colors/legacy`.
v5 adds new color themes: `orange`, `pink`, `purple`, `teal`, `gray`, `neutral`.
```tsx
// before (v1) - manual theme-builder
import { createThemes, defaultComponentThemes } from '@tamagui/theme-builder'
const themes = createThemes({ ... })
// after (v2) - simplified
import { createV5Theme, defaultChildrenThemes } from '@tamagui/themes/v5'
const themes = createV5Theme({
childrenThemes: {
...defaultChildrenThemes,
// add custom color themes
cyan: { light: cyan, dark: cyanDark },
},
})
```
Component themes are off by default in v5. Use `defaultProps` in your config instead:
```tsx
createTamagui({
...defaultConfig,
defaultProps: {
Button: { theme: 'accent' },
},
})
```
### Theme tree-shaking (SSR optimization)
On the client, you can skip loading theme JS and hydrate from CSS variables instead:
```tsx
themes: process.env.VITE_ENVIRONMENT === 'client'
? ({} as typeof themes)
: themes,
```
<Notice theme="blue">
Once your v5 config is set up, run `npx tamagui generate-prompt` and commit the output
to your repo. This helps AI assistants understand your Tamagui config and can speed up
the rest of the migration.
</Notice>
## 3. Prop Renames
These are the most common find-and-replace changes.
### `animation` to `transition`
```tsx
// before
<View animation="bouncy" />
<Sheet.Overlay animation="lazy" />
// after
<View transition="bouncy" />
<Sheet.Overlay transition="lazy" />
```
The TypeScript type also changed: `AnimationProp` to `TransitionProp`.
### `tag` to `render`
```tsx
// before
<View tag="nav" />
<View tag="button" />
// after
<View render="nav" />
<View render="button" />
```
For `tag="a"`, consider using the `Anchor` component instead.
### `Stack` to `View`
```tsx
// before
import { Stack, type StackProps } from 'tamagui'
// after
import { View, type ViewProps } from 'tamagui'
```
### `themeInverse` / `<Theme inverse>` to `theme="accent"`
```tsx
// before
<Button themeInverse>Primary</Button>
<Theme inverse><Card>...</Card></Theme>
// after
<Button theme="accent">Primary</Button>
<Theme name="accent"><Card>...</Card></Theme>
```
### `space` / `spaceDirection` to `gap`
```tsx
// before
<YStack space="$4" spaceDirection="both">
// after
<YStack gap="$4">
```
### `onHoverIn` / `onHoverOut`
```tsx
// before
<View onHoverIn={handler} onHoverOut={handler} />
// after
<View onPointerEnter={handler} onPointerLeave={handler} />
```
### `ellipse` to `numberOfLines`
```tsx
// before
<Text ellipse>Long text...</Text>
// after
<Text numberOfLines={1}>Long text...</Text>
```
## 4. Shadow Migration
Replace React Native shadow props with CSS `boxShadow`:
```tsx
// before
<View
shadowColor="$shadow3"
shadowRadius={20}
shadowOffset={{ height: 10, width: 0 }}
/>
// after
<View boxShadow="0 10px 20px $shadow3" />
```
The format is `x y blur color`. Multiple shadows are comma-separated. Spread and inset are also supported.
## 5. Accessibility Props
All React Native accessibility props are replaced with web-standard ARIA equivalents:
- `accessibilityLabel` → `aria-label`
- `accessibilityRole` → `role`
- `accessibilityHint` → `aria-describedby`
- `accessibilityState={{ disabled }}` → `aria-disabled`
- `accessibilityState={{ selected }}` → `aria-selected`
- `accessibilityState={{ checked }}` → `aria-checked`
- `accessibilityState={{ busy }}` → `aria-busy`
- `accessibilityState={{ expanded }}` → `aria-expanded`
- `accessibilityValue` → `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext`
- `accessibilityElementsHidden` → `aria-hidden`
- `accessibilityViewIsModal` → `aria-modal`
- `accessibilityLiveRegion` → `aria-live`
- `accessible` → `tabIndex={0}`
- `focusable` → `tabIndex`
- `nativeID` → `id`
## 6. Component API Changes
### Input
Input now uses web-standard HTML attributes as the primary API:
```tsx
// before
<Input
keyboardType="email-address"
secureTextEntry
returnKeyType="send"
textContentType="emailAddress"
onChangeText={(text) => setText(text)}
onKeyPress={(e) => {
if (e.nativeEvent.key === 'Enter') submit()
}}
editable={false}
/>
// after
<Input
inputMode="email"
type="password"
enterKeyHint="send"
autoComplete="email"
onChange={(e) => setText(e.target?.value ?? e.nativeEvent?.text ?? '')}
onKeyDown={(e) => {
if (e.key === 'Enter') submit()
}}
readOnly
/>
```
The old React Native props still work but are deprecated.
### Image
Image now prefers web-standard `src` for URLs and native `require()` results.
React Native's `source` remains supported during migration, but is deprecated:
```tsx
// before
<Image
source={{ uri: 'https://example.com/photo.jpg', width: 200, height: 200 }}
resizeMode="cover"
/>
// after
<Image
src="https://example.com/photo.jpg"
width={200}
height={200}
objectFit="cover"
/>
```
The `width` and `height` props control layout. Percentage dimensions also work
when the parent has defined dimensions.
### Button
- Text style props (`fontFamily`, `fontSize`, etc.) removed from the direct API -- style text through child components
- Now defaults to `type="button"` to prevent accidental form submissions
- `useButton` hook is deprecated
### ListItem
- Text style props removed -- use `ListItem.Text` and `ListItem.Subtitle` child components
- Internal spacing props (`spaceFlex`, `scaleSpace`) removed
### Tabs
- `activationMode` now defaults to `'manual'` (was `'automatic'`) -- users must click or press Enter to change tabs, matching web standards
- `Tabs.Trigger` deprecated in favor of `Tabs.Tab`
### Group
- `Group.Item` wrapper is now required (no more auto-cloning direct children)
- Removed props: `space`, `separator`, `scrollable`, `showScrollIndicator`, `disablePassBorderRadius`, `forceUseItem`
- Add `<Separator />` manually between items when needed
### Sheet / Popover.Sheet
`Popover.Sheet` sub-components are replaced with standalone `Sheet`:
```tsx
// before
<Adapt when="maxMd" platform="touch">
<Popover.Sheet modal dismissOnSnapToBottom>
<Popover.Sheet.Frame p="$4">
<Adapt.Contents />
</Popover.Sheet.Frame>
<Popover.Sheet.Overlay animation="quick" />
</Popover.Sheet>
</Adapt>
// after
<Adapt when="max-md" platform="touch">
<Sheet modal dismissOnSnapToBottom>
<Sheet.Frame p="$4">
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay transition="quick" />
</Sheet>
</Adapt>
```
### Select
`SelectLabel` is now SizableText-based instead of ListItem-based.
## 7. Removed APIs
- `<Spacer />` removed from core -- import from `@tamagui/spacer`
- `composeEventHandlers` -- compose manually: `(val) => { a(val); b?.(val) }`
- `useTheme(props)` -- use `<Theme>` component instead
- `ThemeableStack` -- use `View`
- `backgrounded` prop -- use `bg="$background"`
- `selectable` prop -- use `select="text"`
- `animatePresence` prop (inline) -- wrap with `<AnimatePresence>` component
- `scrollbarWidth` prop -- use CSS
- `isWindowDefined` -- use `isBrowser` from `@tamagui/constants`
- `useTheme` from `@tamagui/next-theme` -- renamed to `useThemeSetting`
- `@tamagui/react-native-use-responder-events` -- use pointer events (`onPointerDown`, etc.)
## 8. Native Setup
v2 requires explicit setup imports for native features. Add these at the top of your app entry before any Tamagui imports. Note these are optional and mostly new, so only if you were using native gradient, or toast before do you need to do this.
```tsx
// portals (Sheet, Dialog, Popover, Select, Toast)
import '@tamagui/native/setup-teleport'
// LinearGradient
import '@tamagui/native/setup-expo-linear-gradient'
// Toast (burnt)
import '@tamagui/native/setup-burnt'
// Menu (zeego)
import '@tamagui/native/setup-zeego'
// for smoother Sheet on native:
import '@tamagui/native/setup-gesture-handler'
```
### Expo Router entry point
With Expo Router, these setup imports must run **before** `expo-router/entry`. Create a custom entry point:
```js
// index.js (at project root)
import '@tamagui/native/setup-zeego'
// add other setup imports here as needed
import 'expo-router/entry'
```
Then update your `package.json`:
```json
{
"main": "index.js"
}
```
This ensures native modules like zeego are configured before Expo Router initializes your app.
## 9. Build Config
Not much has changed, but there's some nice improvements you can make:
### Vite
```tsx
import { tamaguiAliases, tamaguiPlugin } from '@tamagui/vite-plugin'
export default {
plugins: [tamaguiPlugin()],
resolve: {
alias: [
...tamaguiAliases({
rnwLite: true, // use lightweight react-native-web
svg: true,
}),
],
},
}
```
Create a `tamagui.build.ts` at your project root:
```tsx
import type { TamaguiBuildOptions } from 'tamagui'
export default {
components: ['tamagui'],
config: './src/tamagui.config.ts',
outputCSS: './src/tamagui.generated.css',
} satisfies TamaguiBuildOptions
```
### Metro (Expo / React Native)
No special Metro configuration is needed for Tamagui v2. A standard Expo Metro config works out of the box:
```js
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const config = getDefaultConfig(__dirname)
module.exports = config
```
### Next.js (Turbopack)
Add `resolveExtensions` and a `react-native-safe-area-context` shim:
```js
// next.config.js
module.exports = {
turbopack: {
resolveAlias: {
'react-native': 'react-native-web',
'react-native-svg': '@tamagui/react-native-svg',
'react-native-safe-area-context': './shims/react-native-safe-area-context.js',
},
resolveExtensions: [
'.web.tsx',
'.web.ts',
'.web.js',
'.web.jsx',
'.tsx',
'.ts',
'.js',
'.jsx',
'.json',
],
},
}
```
### CSS
We recommend generating Tamagui CSS to `tamagui.generated.css`, just so it's easier to configure linters and such:
```tsx
// before
import './tamagui.css'
// after
import './tamagui.generated.css'
```
Generate it with: `npx tamagui generate-css --output ./src/tamagui.generated.css`
## 10. Quick Reference: Find-and-Replace Patterns
These are the most common replacements you can do across your codebase. Run carefully and review each change:
```
animation= -> transition=
AnimationProp -> TransitionProp
AnimationKeys -> TransitionKeys
tag= -> render=
themeInverse -> theme="accent"
<Theme inverse -> <Theme name="accent"
<Stack -> <View
</Stack> -> </View>
StackProps -> ViewProps
onHoverIn= -> onPointerEnter=
onHoverOut= -> onPointerLeave=
$2xl= -> $xxl=
$2xs= -> $xxs=
maxMD -> max-md
maxLG -> max-lg
maxSM -> max-sm
maxXL -> max-xl
max2Xl -> max-xxl
```
## 11. New Features Worth Knowing
While not required for migration, these v2 features can improve your app:
- **`boxShadow`** -- full CSS box-shadow support with tokens
- **`backgroundImage`** -- CSS gradients with token support
- **`filter`**, **`mixBlendMode`** -- graphical effects
- **`scope` prop** on Dialog, Popover, Sheet, Tooltip -- mount once at root for performance
- **`activeStyle` / `activeTheme`** on Switch, Checkbox, ToggleGroup, Tabs
- **Multiple animation drivers** via `animatedBy` prop
- **`transition` delay and enter/exit control** -- `transition={{ enter: 'lazy', exit: 'quick' }}`
- **Menu and ContextMenu** -- new components with native platform rendering
- **Headless components** -- `@tamagui/switch-headless`, `@tamagui/checkbox-headless`, etc.
- **CLI commands** -- `tamagui build`, `tamagui generate`, `tamagui generate-prompt`, `tamagui check`
- **~32% smaller core bundle** (37KB to 25KB gzipped)
## Migration Checklist
- [ ] Upgrade React 19+, React Native 0.81+, TypeScript 5+
- [ ] Bump all `@tamagui/*` packages to `^2.0.0`
- [ ] Add package resolutions for `@tamagui/core`, `@tamagui/web`, `tamagui`
- [ ] Update config from `@tamagui/config/v4` to `@tamagui/config/v5`
- [ ] Import animations separately (`@tamagui/config/v5-css` or other driver)
- [ ] Add `declare module 'tamagui'` type augmentation
- [ ] Move root-level settings into `settings` object
- [ ] Rename `animation` prop to `transition` everywhere
- [ ] Rename `tag` prop to `render`
- [ ] Replace `Stack` with `View`, `StackProps` with `ViewProps`
- [ ] Replace `themeInverse` / `<Theme inverse>` with `theme="accent"`
- [ ] Replace `space` / `spaceDirection` with `gap`
- [ ] Replace `onHoverIn` / `onHoverOut` with pointer events
- [ ] Migrate shadows to `boxShadow`
- [ ] Update media query names (`$2xl` to `$xxl`, max queries to kebab-case)
- [ ] Update accessibility props to ARIA equivalents
- [ ] Update Input props to web-standard API
- [ ] Update Image from `source` to `src`
- [ ] Wrap Group children in `Group.Item`
- [ ] Update Tabs (`activationMode` default changed, `Trigger` to `Tab`)
- [ ] Replace `Popover.Sheet.*` with standalone `Sheet.*`
- [ ] Update Toast from `ToastProvider` wrapper to `Toaster` sibling
- [ ] Import `Spacer` from `@tamagui/spacer` if used
- [ ] Replace `@tamagui/animations-moti` with `@tamagui/animations-reanimated`
- [ ] Add native setup imports (portals, LinearGradient, Toast, Menu)
- [ ] Review flex/position defaults (add `position="relative"` where needed)
- [ ] Update build config (Metro package exports, Vite aliases, CSS generation)
- [ ] Update snapshot tests (shadow colors now use `color-mix()`)
- [ ] Run `npx tamagui check` to verify dependency consistency
## guides/metro
---
title: Metro Guide
description: How to set up Tamagui with Metro
---
## Basic Setup
The default Expo Metro configuration works out of the box:
```ts fileName="metro.config.js"
const { getDefaultConfig } = require('expo/metro-config')
module.exports = getDefaultConfig(__dirname)
```
## With Metro Plugin (Recommended)
For better dev experience, use `@tamagui/metro-plugin` which loads your Tamagui
config and watches for changes:
```bash
yarn add @tamagui/metro-plugin
```
If you have a `tamagui.build.ts` (recommended — see
[compiler install docs](/docs/intro/compiler-install#configuration-with-tamaguibuildts)),
no options are needed:
```tsx fileName="metro.config.js"
const { getDefaultConfig } = require('expo/metro-config')
const { withTamagui } = require('@tamagui/metro-plugin')
const config = getDefaultConfig(__dirname)
// reads from tamagui.build.ts automatically
module.exports = withTamagui(config)
```
Or pass options inline:
```tsx fileName="metro.config.js"
const { getDefaultConfig } = require('expo/metro-config')
const { withTamagui } = require('@tamagui/metro-plugin')
const config = getDefaultConfig(__dirname)
module.exports = withTamagui(config, {
components: ['tamagui'],
config: './tamagui.config.ts',
})
```
## Optimized Production Builds
For maximum optimization, use the `tamagui build` command which pre-compiles
your components:
```bash
# Optimize and build, then restore source files
tamagui build --target web ./app -- expo export --platform web
```
The `--` syntax runs the command after optimization, then automatically restores
your source files. Add this to your `package.json`:
```json
{
"scripts": {
"build:web": "expo export --platform web",
"build:web:optimized": "tamagui build --target web ./app -- expo export --platform web"
}
}
```
## Debugging
To debug the compiler output, add `// debug` at the top of a file:
```tsx
// debug
import { YStack } from 'tamagui'
export function MyComponent() {
return <YStack flex={1} bg="$background" />
}
```
This will print detailed compiler output showing what optimizations are
happening:
```
[✅] flattened YStack div
```
Use `// debug-verbose` for even more detailed output.
## guides/next-js
---
title: Next.js Guide
description: How to set up Tamagui with Next.js
---
<Notice theme="green">
Running `npm create tamagui@latest` lets you choose the `starter-free` starter which is
a nicely configured Next.js app where you can take or leave whatever you want.
</Notice>
Create a new [Next.js](https://nextjs.org/docs/getting-started/installation)
project:
```bash
npx create-next-app@latest
```
We recommend starting with our default config which gives you media queries and
other nice things:
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v5'
import { createTamagui } from 'tamagui' // or '@tamagui/core'
const appConfig = createTamagui(defaultConfig)
export type AppConfig = typeof appConfig
declare module 'tamagui' {
// or '@tamagui/core'
// overrides TamaguiCustomConfig so your custom types
// work everywhere you import `tamagui`
interface TamaguiCustomConfig extends AppConfig {}
}
export default appConfig
```
## Setup
Next.js uses Turbopack by default. In dev mode, Tamagui works without any setup.
For production builds, use the Tamagui CLI to optimize your build.
Install the CLI:
```bash
yarn add -D @tamagui/cli
```
Create `tamagui.build.ts`:
```ts fileName="tamagui.build.ts"
import type { TamaguiBuildOptions } from '@tamagui/core'
export default {
components: ['@tamagui/core'], // or ['tamagui']
config: './tamagui.config.ts',
outputCSS: './public/tamagui.generated.css',
} satisfies TamaguiBuildOptions
```
Your `next.config.ts` needs some configuration:
```ts fileName="next.config.ts"
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// some Tamagui packages may need transpiling
transpilePackages: ['@tamagui/lucide-icons-2'],
experimental: {
turbo: {
resolveAlias: {
'react-native': 'react-native-web',
'react-native-svg': '@tamagui/react-native-svg',
},
},
},
}
export default nextConfig
```
<Notice theme="blue">
The `transpilePackages` array may need additional packages depending on which Tamagui
packages you use. If you see module resolution errors, try adding the problematic
package to this array.
</Notice>
### Build Scripts
The CLI can wrap your build command, optimizing files beforehand and restoring
them after:
```json fileName="package.json"
{
"scripts": {
"dev": "next dev --turbopack",
"build": "tamagui build --target web ./src -- next build"
}
}
```
The `--` separator tells the CLI to run `next build` after optimization, then
restore your source files automatically.
You can also target specific files or use `--include`/`--exclude` patterns:
```bash
# Target specific files
tamagui build --target web ./src/components/Button.tsx ./src/components/Card.tsx -- next build
# Use glob patterns
tamagui build --target web --include "src/components/**/*.tsx" --exclude "src/components/**/*.test.tsx" ./src -- next build
```
### CSS Setup
The CLI generates theme CSS to `outputCSS`. Commit this file to git and import
it in your layout:
```tsx fileName="app/layout.tsx"
import '../public/tamagui.generated.css'
import { TamaguiProvider } from '@tamagui/core'
import config from '../tamagui.config'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<TamaguiProvider config={config}>{children}</TamaguiProvider>
</body>
</html>
)
}
```
<Notice theme="blue">
With React 19, Tamagui automatically injects runtime styles via style tags. The
`outputCSS` file handles themes and tokens that are generated at build time.
</Notice>
Run `npx tamagui build` once to generate the initial CSS file, then commit it.
### CI Verification
Use `--expect-optimizations` to fail builds if the compiler optimizes fewer than
the expected minimum number of components:
```json
{
"build": "tamagui build --target web --expect-optimizations 5 ./src -- next build"
}
```
This will fail the build if fewer than 5 components are optimized, helping catch
configuration issues in CI.
## Themes
We've created a package called `@tamagui/next-theme` that properly supports SSR
light/dark themes while respecting user system preferences. It assumes your
themes are named `light` and `dark`, but you can override this. This is
pre-configured in the create-tamagui starter.
```bash
yarn add @tamagui/next-theme
```
Here's how to set up your `NextTamaguiProvider.tsx`:
```tsx fileName="NextTamaguiProvider.tsx"
'use client'
import { ReactNode } from 'react'
import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme'
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from '../tamagui.config'
export const NextTamaguiProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useRootTheme()
return (
<NextThemeProvider
skipNextHead
// change default theme (system) here:
// defaultTheme="light"
onChangeTheme={(next) => {
setTheme(next as any)
}}
>
<TamaguiProvider config={tamaguiConfig} disableRootThemeClass defaultTheme={theme}>
{children}
</TamaguiProvider>
</NextThemeProvider>
)
}
```
Then update your `app/layout.tsx`:
```tsx fileName="app/layout.tsx"
import '../public/tamagui.generated.css'
import { Metadata } from 'next'
import { NextTamaguiProvider } from './NextTamaguiProvider'
export const metadata: Metadata = {
title: 'Your page title',
description: 'Your page description',
icons: '/favicon.ico',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<NextTamaguiProvider>{children}</NextTamaguiProvider>
</body>
</html>
)
}
```
## NextThemeProvider
`NextThemeProvider` lets you set the theme for your app and provides a hook to
access the current theme and toggle between themes.
```bash
yarn add @tamagui/next-theme
```
<PropsTable
data={[
{
name: 'skipNextHead',
required: false,
type: 'boolean',
description: `Required in app router. The internal usage of next/head is not supported in the app directory, so you need to add it.`,
},
{
name: 'enableSystem',
required: false,
type: 'boolean',
description: `Whether to switch between dark and light themes based on prefers-color-scheme.`,
},
{
name: 'defaultTheme',
required: false,
type: 'string',
description:
'If enableSystem is `false`, the default theme is light. Default theme name (for v0.0.12 and lower the default was light).',
},
{
name: 'forcedTheme',
required: false,
type: 'string',
description: 'Forced theme name for the current page.',
},
{
name: 'onChangeTheme',
required: false,
type: '(name: string) => void',
description:
'Used to change the current theme. The function receives the theme name as a parameter.',
},
{
name: 'systemTheme',
required: false,
type: 'string',
description: 'System theme name for the current page.',
},
{
name: 'enableColorScheme',
required: false,
type: 'boolean',
description: `Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons.`,
},
{
name: 'disableTransitionOnChange',
required: false,
type: 'boolean',
description: `Disable all CSS transitions when switching themes.`,
},
{
name: 'storageKey',
required: false,
type: 'string',
description: `Key used to store theme setting in localStorage.`,
},
{
name: 'themes',
required: false,
type: 'string[]',
description: `List of all available theme names.`,
},
{
name: 'value',
required: false,
type: 'ValueObject',
description: `Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value.`,
},
]}
/>
### Theme toggle
If you need to access the current theme, say for a toggle button, you will then
use the `useThemeSetting` hook. We'll release an update in the future that makes
this automatically work better with Tamagui's built-in `useThemeSetting`.
```tsx fileName="SwitchThemeButton.tsx"
import { useState } from 'react'
import { Button, useIsomorphicLayoutEffect } from 'tamagui'
import { useThemeSetting, useRootTheme } from '@tamagui/next-theme'
export const SwitchThemeButton = () => {
const themeSetting = useThemeSetting()
const [theme] = useRootTheme()
const [clientTheme, setClientTheme] = useState<string | undefined>('light')
useIsomorphicLayoutEffect(() => {
setClientTheme(themeSetting.forcedTheme || themeSetting.current || theme)
}, [themeSetting.current, themeSetting.resolvedTheme])
return <Button onPress={themeSetting.toggle}>Change theme: {clientTheme}</Button>
}
```
---
## For Older Versions
The following sections cover setup for older Next.js versions using Webpack
instead of Turbopack.
### Webpack Plugin
If you aren't using Turbopack, you may want the optional `@tamagui/next-plugin`,
which smooths out a few settings. See the
[compiler install docs](/docs/intro/compiler-install) for more options.
Add `@tamagui/next-plugin` to your project:
```bash
yarn add @tamagui/next-plugin
```
## Pages Router
### next.config.js
Set up the optional Tamagui plugin in `next.config.js`:
```tsx showMore fileName="next.config.js"
const { withTamagui } = require('@tamagui/next-plugin')
module.exports = function (name, { defaultConfig }) {
let config = {
...defaultConfig,
// ...your configuration
}
const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
})
return {
...config,
...tamaguiPlugin(config),
}
}
```
### pages/\_document.tsx
If you're using React Native Web components, gather the `react-native-web`
styles in `_document.tsx`:
```tsx fileName="_document.tsx"
import NextDocument, {
DocumentContext,
Head,
Html,
Main,
NextScript,
} from 'next/document'
import { StyleSheet } from 'react-native'
export default class Document extends NextDocument {
static async getInitialProps({ renderPage }: DocumentContext) {
const page = await renderPage()
// @ts-ignore RN doesn't have this type
const rnwStyle = StyleSheet.getSheet()
return {
...page,
styles: (
<style
id={rnwStyle.id}
dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }}
/>
),
}
}
render() {
return (
<Html lang="en">
<Head>
<meta id="theme-color" name="theme-color" />
<meta name="color-scheme" content="light dark" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
```
<Notice theme="blue">
Tamagui automatically injects styles at runtime. You can optionally generate a static
CSS file - see the [Static CSS Output](#static-css-output) section.
</Notice>
### pages/\_app.tsx
Add `TamaguiProvider`:
```tsx fileName="_app.tsx"
import { NextThemeProvider } from '@tamagui/next-theme'
import { AppProps } from 'next/app'
import Head from 'next/head'
import React, { useMemo } from 'react'
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from '../tamagui.config'
export default function App({ Component, pageProps }: AppProps) {
// memo to avoid re-render on dark/light change
const contents = useMemo(() => {
return <Component {...pageProps} />
}, [pageProps])
return (
<>
<Head>
<title>Your page title</title>
<meta name="description" content="Your page description" />
<link rel="icon" href="/favicon.ico" />
</Head>
<NextThemeProvider>
<TamaguiProvider config={tamaguiConfig} disableInjectCSS disableRootThemeClass>
{contents}
</TamaguiProvider>
</NextThemeProvider>
</>
)
}
```
<Notice theme="blue">
Use `disableInjectCSS` for SSR apps to prevent duplicate style injection. Only omit it
for client-only apps without server rendering.
</Notice>
### Themes (Pages Router)
We've created a package called `@tamagui/next-theme` that properly supports SSR
light/dark themes while respecting user system preferences. It assumes your
themes are named `light` and `dark`, but you can override this. This is
pre-configured in the create-tamagui starter.
```bash
yarn add @tamagui/next-theme
```
Here's how to set up your `_app.tsx`:
```tsx fileName="_app.tsx"
import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme'
import { AppProps } from 'next/app'
import Head from 'next/head'
import React, { useMemo } from 'react'
import { TamaguiProvider, createTamagui } from 'tamagui'
// you usually export this from a tamagui.config.ts file:
import { defaultConfig } from '@tamagui/config/v5'
const tamaguiConfig = createTamagui(defaultConfig)
// make TypeScript type everything based on your config
type Conf = typeof tamaguiConfig
declare module '@tamagui/core' {
interface TamaguiCustomConfig extends Conf {}
}
export default function App({ Component, pageProps }: AppProps) {
const [theme, setTheme] = useRootTheme()
// memo to avoid re-render on dark/light change
const contents = useMemo(() => {
return <Component {...pageProps} />
}, [pageProps])
return (
<>
<Head>
<title>Your page title</title>
<meta name="description" content="Your page description" />
<link rel="icon" href="/favicon.ico" />
</Head>
<NextThemeProvider
// change default theme (system) here:
// defaultTheme="light"
onChangeTheme={setTheme as any}
>
<TamaguiProvider
config={tamaguiConfig}
disableInjectCSS
disableRootThemeClass
defaultTheme={theme}
>
{contents}
</TamaguiProvider>
</NextThemeProvider>
</>
)
}
```
### Static CSS Output (Pages Router)
You can generate a static CSS file for your themes and tokens. There are two
ways to do this:
#### Option 1: Using the CLI
The simplest approach is to use the Tamagui CLI to generate the CSS file:
```bash
npx tamagui generate
```
This outputs CSS to `.tamagui/tamagui.generated.css`. Copy it to your public folder or
configure `outputCSS` in your `tamagui.build.ts`:
```ts fileName="tamagui.build.ts"
import type { TamaguiBuildOptions } from '@tamagui/core'
export default {
components: ['tamagui'],
config: './tamagui.config.ts',
outputCSS: './public/tamagui.generated.css',
} satisfies TamaguiBuildOptions
```
Then import it in your `_app.tsx`:
```tsx fileName="_app.tsx"
import '../public/tamagui.generated.css'
```
#### Option 2: Using the Next.js Plugin
You can also have the plugin generate CSS during your Next.js build:
```tsx fileName="next.config.js"
const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/tamagui.generated.css' : null,
// faster dev mode, keeps debugging helpers:
disableExtraction: process.env.NODE_ENV === 'development',
})
```
Then import it in your `_app.tsx`:
```tsx fileName="_app.tsx"
import '../public/tamagui.generated.css'
```
<Notice theme="green">
With `outputCSS`, you don't need `getCSS()` in your `_document.tsx` - all styles are
handled by the static CSS file and runtime style injection.
</Notice>
## App Router (Webpack)
Tamagui includes Server Components support for the Next.js app directory with
[`use client`](https://nextjs.org/docs/app/building-your-application/rendering/client-components#using-client-components-in-nextjs)
support.
Note that `"use client"` components do render on the server, and since Tamagui
extracts to CSS statically and uses inline `<style />` tags for non-static
styling, you get excellent performance out of the box.
### next.config.js
The Tamagui plugin is optional but helps with compatibility with the rest of the
React Native ecosystem. It requires CommonJS for now because the optimizing
compiler uses various resolving features that haven't been ported to ESM yet.
Rename your `next.config.mjs` to `next.config.js` before adding it:
```tsx fileName="next.config.js"
const { withTamagui } = require('@tamagui/next-plugin')
module.exports = function (name, { defaultConfig }) {
let config = {
...defaultConfig,
// ...your configuration
}
const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
appDir: true,
})
return {
...config,
...tamaguiPlugin(config),
}
}
```
<Notice theme="blue">
You need to pass the `appDir` boolean to `@tamagui/next-plugin`.
</Notice>
### app/layout.tsx
Create a new component to add `TamaguiProvider`:
<Notice theme="blue">
The internal usage of `next/head` is not supported in the app directory, so you need to add the `skipNextHead` prop to your `<NextThemeProvider>`.
</Notice>
```tsx fileName="NextTamaguiProvider.tsx"
'use client'
import { ReactNode } from 'react'
import { StyleSheet } from 'react-native'
import { useServerInsertedHTML } from 'next/navigation'
import { NextThemeProvider } from '@tamagui/next-theme'
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from '../tamagui.config'
export const NextTamaguiProvider = ({ children }: { children: ReactNode }) => {
// only if using react-native-web components like ScrollView:
useServerInsertedHTML(() => {
// @ts-ignore
const rnwStyle = StyleSheet.getSheet()
return (
<>
<style
dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }}
id={rnwStyle.id}
/>
</>
)
})
return (
<NextThemeProvider skipNextHead>
<TamaguiProvider config={tamaguiConfig} disableRootThemeClass>
{children}
</TamaguiProvider>
</NextThemeProvider>
)
}
```
<Notice theme="green">
The `getNewCSS` helper in Tamagui will keep track of the last call and only return new
styles generated since the last usage.
</Notice>
Then add it to your `app/layout.tsx`:
```tsx fileName="layout.tsx"
import { Metadata } from 'next'
import { NextTamaguiProvider } from './NextTamaguiProvider'
export const metadata: Metadata = {
title: 'Your page title',
description: 'Your page description',
icons: '/favicon.ico',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<NextTamaguiProvider>{children}</NextTamaguiProvider>
</body>
</html>
)
}
```
<Notice theme="green">
You can use `suppressHydrationWarning` to avoid the warning about mismatched content
during hydration in dev mode.
</Notice>
### app/page.tsx
Now you're ready to start adding components to `app/page.tsx`:
```tsx fileName="page.tsx"
'use client'
import { Button } from 'tamagui'
export default function Home() {
return <Button>Hello world!</Button>
}
```
### Themes (App Router Webpack)
We've created a package called `@tamagui/next-theme` that properly supports SSR
light/dark themes while respecting user system preferences. It assumes your
themes are named `light` and `dark`, but you can override this. This is
pre-configured in the create-tamagui starter.
```bash
yarn add @tamagui/next-theme
```
Here's how to set up your `NextTamaguiProvider.tsx`:
```tsx fileName="NextTamaguiProvider.tsx"
'use client'
import { ReactNode } from 'react'
import { StyleSheet } from 'react-native'
import { useServerInsertedHTML } from 'next/navigation'
import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme'
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from '../tamagui.config'
export const NextTamaguiProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useRootTheme()
// only needed if using react-native-web components:
useServerInsertedHTML(() => {
// @ts-ignore
const rnwStyle = StyleSheet.getSheet()
return (
<style
dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }}
id={rnwStyle.id}
/>
)
})
return (
<NextThemeProvider
skipNextHead
// change default theme (system) here:
// defaultTheme="light"
onChangeTheme={(next) => {
setTheme(next as any)
}}
>
<TamaguiProvider config={tamaguiConfig} disableRootThemeClass defaultTheme={theme}>
{children}
</TamaguiProvider>
</NextThemeProvider>
)
}
```
### Static CSS Output (App Router Webpack)
You can generate a static CSS file for your themes and tokens. Use either the
CLI or the Next.js plugin:
```tsx fileName="next.config.js"
const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/tamagui.generated.css' : null,
// faster dev mode, keeps debugging helpers:
disableExtraction: process.env.NODE_ENV === 'development',
})
```
Then link the generated CSS file in your `app/layout.tsx`:
```tsx fileName="app/layout.tsx"
import '../public/tamagui.generated.css'
```
<Notice theme="green">
With React 19, Tamagui automatically injects runtime styles via style tags on the
server. The `outputCSS` file handles themes and tokens generated at build time, so you
don't need any `getCSS()` calls in your provider.
</Notice>
## guides/one
---
title: One Guide
description: How to set up Tamagui with One
---
[One](https://onestack.dev) is a React framework for web, iOS, and Android built on Vite. It offers seamless cross-platform development and integrates well with Tamagui.
## Install
First, create a new One project:
```bash
npx one
```
Add Tamagui and its dependencies:
```bash
yarn add tamagui @tamagui/config
```
## Configuration
Create a `tamagui.config.ts` file in your project root:
```tsx fileName="tamagui.config.ts"
import { defaultConfig } from '@tamagui/config/v5'
import { createTamagui } from 'tamagui'
const tamaguiConfig = createTamagui(defaultConfig)
export default tamaguiConfig
// this is important!
export type Conf = typeof tamaguiConfig
declare module 'tamagui' {
interface TamaguiCustomConfig extends Conf {}
}
```
Update your `vite.config.ts`:
```tsx fileName="vite.config.ts"
import { one } from 'one/vite'
import { tamaguiPlugin } from '@tamagui/vite-plugin'
import type { UserConfig } from 'vite'
export default {
plugins: [
one({
web: {
defaultRenderMode: 'ssg',
},
}),
tamaguiPlugin({
config: './tamagui.config.ts',
components: ['tamagui'],
}),
],
// Vite 6 style configuration
ssr: {
noExternal: true,
},
optimizeDeps: {
include: ['@tamagui/core', '@tamagui/config'],
},
build: {
cssTarget: 'safari15',
},
} satisfies UserConfig
```
## Setup Tamagui Provider
Update your root layout file (typically `app/_layout.tsx`):
```tsx fileName="app/_layout.tsx"
import { TamaguiProvider } from 'tamagui'
import { Slot } from 'one'
import config from '../tamagui.config'
export default function Layout() {
return (
<TamaguiProvider config={config}>
<Slot />
</TamaguiProvider>
)
}
```
## Usage
Now you can use Tamagui components in your One app:
```tsx fileName="app/index.tsx"
import { Button, Text, YStack } from 'tamagui'
export default function Home() {
return (
<YStack f={1} jc="center" ai="center" p="$4" gap="$4">
<Text fontSize="$6">Welcome to Tamagui with One!</Text>
<Button>Click me</Button>
</YStack>
)
}
```
## Themes
To support light and dark modes, use One's built-in color scheme support along with Tamagui's theming system. Update your root layout:
```tsx fileName="app/_layout.tsx"
import { TamaguiProvider, Theme } from 'tamagui'
import { Slot } from 'one'
import { useColorScheme } from 'react-native'
import config from '../tamagui.config'
export default function Layout() {
const colorScheme = useColorScheme()
return (
<TamaguiProvider config={config} defaultTheme={colorScheme}>
<Theme name={colorScheme}>
<Slot />
</Theme>
</TamaguiProvider>
)
}
```
## Performance
For better performance, use the `outputCSS` option in the Tamagui Vite plugin:
```tsx fileName="vite.config.ts"
tamaguiPlugin({
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/tamagui.generated.css' : null,
})
```
Then import the CSS in your root layout:
```tsx fileName="app/_layout.tsx"
import '../public/tamagui.generated.css'
// ... rest of your imports and component
```
## Next Steps
You now have Tamagui set up with [One](https://onestack.dev). Start building your cross-platform app using Tamagui's styling system and One's seamless development experience.
## guides/theme-builder
---
title: Creating Themes with Tamagui
description: Learn how to create a suite of themes for a Tamagui app
demoName: ThemeBuilder
---
<HeroContainer noScroll>
<ThemeBuilderDemo />
</HeroContainer>
<IntroParagraph>
Tamagui themes start simple, but can do some pretty powerful things. To make them easier
to generate, we've built a few helpers. You can always just skip themes, or add a single
basic theme if you prefer. This guide is for users wanting to generate a more complex
suite of themes.
</IntroParagraph>
We have three ways to generate themes, from simplest to most powerful:
| Helper | Best for |
| ------------------------ | -------------------------------------------------------- |
| **`createV5Theme`** | Quick start with sensible defaults. Minimal config. |
| **`createThemes`** | Custom palettes and structure while keeping conventions. |
| **`createThemeBuilder`** | Full control over every aspect of theme generation. |
We've also released [Theme](/theme), a free visual tool to create themes.
---
## createV5Theme
The simplest way to get a complete theme suite. Call it with no arguments for production-ready defaults, or pass options to customize.
```tsx
import { createV5Theme } from '@tamagui/themes/v5'
// zero-config - includes light, dark, accent, and color themes
export const themes = createV5Theme()
```
### Customizing
```tsx
import { createV5Theme, defaultChildrenThemes } from '@tamagui/themes/v5'
import { orange, orangeDark } from '@tamagui/colors'
export const themes = createV5Theme({
// override base palettes
lightPalette: ['#fff', '#f8f8f8', ...],
darkPalette: ['#000', '#111', ...],
// add/override color themes
childrenThemes: {
...defaultChildrenThemes,
orange: { light: orange, dark: orangeDark },
},
// disable component themes
componentThemes: false,
})
```
### Options
- `lightPalette` / `darkPalette` - Override base 12-color palettes
- `childrenThemes` - Color themes (blue, red, etc). Accepts Radix color objects directly
- `grandChildrenThemes` - Third-level themes (defaults to `{ accent: { template: 'inverse' } }`)
- `componentThemes` - Component theme mappings, or `false` to disable
---
## createThemes
More control over structure while still getting automatic palette interpolation and component themes.
### Quick Start
```tsx
import { createThemes } from '@tamagui/theme-builder'
export const themes = createThemes({
base: {
palette: {
light: ['#fff', '#000'],
dark: ['#000', '#fff'],
},
},
})
```
### Full Example
```tsx
import { createThemes, defaultComponentThemes } from '@tamagui/theme-builder'
import * as Colors from '@tamagui/colors'
export const themes = createThemes({
componentThemes: defaultComponentThemes,
base: {
palette: {
light: ['#fff', '#f2f2f2', '#e0e0e0', '#999', '#666', '#333', '#000'],
dark: ['#000', '#111', '#222', '#666', '#999', '#ccc', '#fff'],
},
extra: {
light: { ...Colors.blue, shadowColor: 'rgba(0,0,0,0.1)' },
dark: { ...Colors.blueDark, shadowColor: 'rgba(0,0,0,0.4)' },
},
},
accent: {
palette: {
light: ['#000', '#333', '#666', '#999', '#ccc', '#eee', '#fff'],
dark: ['#fff', '#eee', '#ccc', '#999', '#666', '#333', '#000'],
},
},
childrenThemes: {
blue: {
palette: {
light: Object.values(Colors.blue),
dark: Object.values(Colors.blueDark),
},
},
red: {
palette: { light: Object.values(Colors.red), dark: Object.values(Colors.redDark) },
},
},
grandChildrenThemes: {
accent: { template: 'inverse' },
},
})
```
### Structure
`createThemes` generates this hierarchy:
```
light / dark # base themes
├── light_accent / dark_accent # accent themes
├── light_blue / dark_blue # child themes
│ └── light_blue_accent # grandchild themes
└── light_Button / dark_Button # component themes
```
### Configuration
#### `base` (required)
```tsx
base: {
palette: { light: string[], dark: string[] },
// or single array (auto-reversed for dark):
palette: string[],
template: 'base' | 'surface1' | 'surface2' | 'surface3' | 'inverse',
extra: { light: {...}, dark: {...} }, // non-inherited values
}
```
#### `accent`, `childrenThemes`, `grandChildrenThemes`
```tsx
accent: { palette: { light: [...], dark: [...] } }
childrenThemes: {
blue: { palette: { light: [...], dark: [...] }, template: 'base' },
}
grandChildrenThemes: {
accent: { template: 'inverse' }, // template-only inherits parent palette
}
```
#### `templates`
Override default templates. Maps property names to palette indices:
```tsx
templates: {
base: { background: 6, color: -1, borderColor: 9 },
surface1: { background: 7, color: -1, borderColor: 10 },
}
```
Defaults: `base`, `surface1`, `surface2`, `surface3`, `alt1`, `alt2`, `inverse`
#### `componentThemes`
```tsx
componentThemes: {
Button: { template: 'surface3' },
Card: { template: 'surface1' },
}
// or disable:
componentThemes: false
```
#### `getTheme`
Customize any generated theme:
```tsx
getTheme: ({ name, theme, scheme, level, palette }) => ({
...theme,
shadowColor: scheme === 'dark' ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.1)',
})
```
Parameters: `name`, `theme`, `scheme` ('light'|'dark'), `level` (1=base, 2=children, 3=grandchildren), `parentName`, `parentNames`, `palette`, `template`
### Generated Theme Shape
```tsx
{
background, backgroundHover, backgroundPress, backgroundFocus,
color, colorHover, colorPress, colorFocus,
borderColor, borderColorHover, borderColorPress, borderColorFocus,
placeholderColor, outlineColor,
color1...color12, // full palette scale
background0...background08, // transparent variants
accent1...accent12, // if accent defined
}
```
---
## createThemeBuilder
The low-level API that powers `createThemes`. Use this when you need complete control over palette structure, template definitions, and theme hierarchy.
```tsx
import { createThemeBuilder } from '@tamagui/theme-builder'
const themesBuilder = createThemeBuilder()
.addPalettes({
dark: ['#000', '#111', '#222', '#999', '#ccc', '#eee', '#fff'],
light: ['#fff', '#eee', '#ccc', '#999', '#222', '#111', '#000'],
})
.addTemplates({
base: { background: 0, color: -0 },
subtle: { background: 1, color: -1 },
})
.addThemes({
light: { template: 'base', palette: 'light' },
dark: { template: 'base', palette: 'dark' },
})
.addChildThemes({
subtle: { template: 'subtle' },
})
export const themes = themesBuilder.build()
```
### Build-time Generation
Optionally generate themes at build time to reduce bundle size:
```tsx
// next.config.js
withTamagui({
themeBuilder: {
input: './themes-input.tsx',
output: './themes.tsx',
},
})
```
Or use the CLI: `npx @tamagui/cli generate-themes ./src/themes-in.ts ./src/themes-out.ts`
---
## Concepts
### Palettes
A palette is a gradient of colors from background to foreground:
<Blog.ThemeBuilder.ExamplePalette showLabels />
```tsx
const dark_blue = [
'hsl(212, 35.0%, 9.2%)', // background
'hsl(216, 50.0%, 11.8%)',
// ...
'hsl(206, 98.0%, 95.8%)', // foreground
]
```
### Templates
Templates map property names to palette indices:
```tsx
const template = { background: 0, color: 12 }
// Negative indices count from end: -1 = last, -2 = second-to-last
```
### Sub-themes
Underscore in theme names defines nesting: `dark_subtle` is a sub-theme of `dark`.
```tsx
<Theme name="dark">
<Box /> {/* uses dark theme */}
<Theme name="subtle">
<Box /> {/* uses dark_subtle theme */}
</Theme>
</Theme>
```
### Component Themes
Named components automatically pick up matching sub-themes:
```tsx
const Button = styled(View, { name: 'Button', ... })
// If dark_Button theme exists, <Button /> uses it automatically
```
### getTheme Callback
Both `createThemes` and `createThemeBuilder` support `getTheme` for customization:
```tsx
.getTheme(({ theme, scheme, level }) => ({
...theme,
customBorder: scheme === 'dark' ? '#333' : '#ddd',
}))
```
### nonInheritedValues
Add values that don't cascade to child themes:
```tsx
.addThemes({
light: {
template: 'base',
palette: 'light',
nonInheritedValues: {
blue1: '#e0f2fe',
shadowColor: 'rgba(0,0,0,0.1)',
},
},
})
```
### Inverse Themes
In v2, the `<Theme inverse />` prop and `themeInverse` prop were removed. To
create inverse-like themes, follow the pattern used by the v5 config's `accent`
theme: swap your light and dark palettes.
```tsx
// with createThemes - swap palettes for inverse effect
accent: {
palette: {
light: darkPalette, // use dark colors in light mode
dark: lightPalette, // use light colors in dark mode
},
},
```
This gives you an "inverted" theme where light mode shows dark colors and vice
versa - commonly used for accent buttons or cards that should stand out. This
approach is also SSR-safe.
Use it in your components:
```tsx
<Theme name="accent">
<Button>Inverted colors</Button>
</Theme>
// or use activeTheme on supported components
<Switch activeTheme="accent" />
<Checkbox activeTheme="accent" />
```
## guides/vite
---
title: Vite Guide
description: How to set up Tamagui with Vite
---
Tamagui now has two plugins for Vite: one that sets up everything you need to get going, and a second that adds CSS compilation. Both are included in the `@tamagui/vite-plugin` package.
## Install
<Notice theme="yellow">
`@tamagui/vite-plugin` is ESM-only. Your project must have `"type": "module"` in its
`package.json` (or use `.mjs`/`.mts` config files). CJS (`require()`) is not supported.
</Notice>
<Notice theme="green">
For a full-featured example, you can create a new app using `npm create tamagui@latest`
and select the 'Simple Web' option which includes a Vite setup.
</Notice>
Create a new [Vite](https://vitejs.dev/guide/#scaffolding-your-first-vite-project) project:
```bash
npm create vite@latest
```
Add `@tamagui/vite-plugin`:
```bash
yarn add @tamagui/vite-plugin
```
### Configuration
Update your `vite.config.ts`. If you have a `tamagui.build.ts` (recommended —
see [compiler install docs](/docs/intro/compiler-install#configuration-with-tamaguibuildts)),
no options are needed:
```tsx fileName="vite.config.ts"
import { tamaguiPlugin } from '@tamagui/vite-plugin'
export default {
plugins: [
// reads from tamagui.build.ts automatically
tamaguiPlugin(),
].filter(Boolean),
}
```
Or pass options inline:
```tsx fileName="vite.config.ts"
import { tamaguiPlugin } from '@tamagui/vite-plugin'
export default {
plugins: [
tamaguiPlugin({
config: 'src/tamagui.config.ts',
components: ['tamagui'],
disableExtraction: true,
}),
].filter(Boolean),
}
```
Or use a minimal manual setup for Vite that just adds compatibility for react-native-web and React Native extensions:
```tsx showMore
config.define = {
__DEV__: `${process.env.NODE_ENV === 'development' ? true : false}`,
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}
config.resolve.alias['react-native'] = 'react-native-web'
// set up web extensions
config.optimizeDeps.esbuildOptions = {
...config.optimizeDeps.esbuildOptions,
resolveExtensions: [
'.web.js',
'.web.jsx',
'.web.ts',
'.web.tsx',
'.mjs',
'.js',
'.mts',
'.ts',
'.jsx',
'.tsx',
'.json',
],
loader: {
'.js': 'jsx',
},
}
```
### Custom Aliases with tamaguiAliases
For advanced use cases where you need more control over alias ordering in your Vite config, you can use the `tamaguiAliases` helper function:
```tsx fileName="vite.config.ts"
import { tamaguiAliases } from '@tamagui/vite-plugin'
export default {
resolve: {
alias: [
// your custom aliases first
{ find: '@app', replacement: '/src' },
// then tamagui aliases
...tamaguiAliases({
// use @tamagui/react-native-web-lite for smaller bundle
rnwLite: true,
// or 'without-animated' for even smaller bundle (no Animated API)
// rnwLite: 'without-animated',
// alias react-native-svg to @tamagui/react-native-svg
svg: true,
}),
],
},
}
```
This is useful when you need to ensure specific alias resolution order or when using a custom Vite setup without the full `tamaguiPlugin`.
## guides/webpack
---
title: Webpack Guide
description: How to set up Tamagui with Webpack
---
First, install [Webpack and webpack-cli](https://webpack.js.org/guides/installation/):
```bash
yarn add -D webpack webpack-cli
```
Then install the Tamagui plugin:
```bash
yarn add -D tamagui-loader
```
### Configuration
Add the plugin to your `webpack.config.js`. If you have a `tamagui.build.ts`
(recommended — see
[compiler install docs](/docs/intro/compiler-install#configuration-with-tamaguibuildts)),
no options are needed:
```tsx fileName="webpack.config.js"
const { TamaguiPlugin } = require('tamagui-loader')
// reads from tamagui.build.ts automatically
config.plugins.push(new TamaguiPlugin())
```
Or pass options inline:
```tsx fileName="webpack.config.js"
const { TamaguiPlugin } = require('tamagui-loader')
config.plugins.push(
new TamaguiPlugin({
config: './src/tamagui.config.ts',
components: ['tamagui'],
})
)
```
Or use a minimal manual setup:
```tsx showMore fileName="webpack.config.js"
// some stuff for react-native
config.plugins.push(
new webpack.DefinePlugin({
process: {
env: {
__DEV__: process.env.NODE_ENV === 'development' ? 'true' : 'false',
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
},
})
)
config.resolve.alias['react-native$'] = 'react-native-web'
// set up web extensions
compiler.options.resolve.extensions = [
'.web.tsx',
'.web.ts',
'.web.js',
'.ts',
'.tsx',
'.js',
]
```
## Usage
To run the server locally, install [webpack-dev-server](https://github.com/webpack/webpack-dev-server):
```bash
yarn add -D webpack-dev-server
```
Start the server with:
```bash
yarn run webpack serve
```
## intro/benchmarks
---
title: Benchmarks
description: Performance tests and comparisons
---
Tamagui compares well to the fastest libraries at runtime, and the compiler
further optimizes your styled components, flattening them down and hoisting
objects out of your rendering loop (and extracting CSS on the web).
With the compiler on Tamagui will output near optimal React code despite giving
many nice shorthand style props. It has a higher impact on web, but with initial
support for flattening more dynamic styled components now landed, Tamagui does
flatten a very high % of your code on all platforms. We look forward to updating
our benchmarks as we expect we've improved in a number of areas.
It's important to note that benchmarks never show a complete picture. Our plan is to
build out a better benchmarking setup to update this page.
## React Native
In [this benchmark](https://github.com/tamagui/benchmarks) Tamagui is within 10%
of the speed of vanilla React Native, even when using nearly all of Tamagui's
features. We render list of 28 items with a few sections, text and images.
Average of 5 runs on a Apple M2 Air:
<BenchmarkChartNative />
Running Tamagui without the compiler averages `125ms` or ~12% slower. Note that
the compiler is much more effective on the web as it turns many more props into
CSS and both bundle size and render performance are more important.
## Web
We forked and ran a few more
[benchmarks](https://github.com/tamagui/tamagui/tree/24ac758bbe5d6ff2f67f25071df4286e0594f578/starters/benchmark)
for the web.
**Legend:** RNW = react-native-web, SC = styled-components
### Simple component
Timing rendering a simple custom component.
<BenchmarkChart
data={[
{ name: 'Tamagui', value: 0.018 },
{ name: 'RNW', value: 0.057 },
{ name: 'Dripsy', value: 0.042 },
{ name: 'Stitches', value: 0.023 },
{ name: 'Emotion', value: 0.041 },
]}
/>
### Updating variants
Changing variants is fast at runtime, and even faster when compiled:
<BenchmarkChart
data={[
{ name: 'Tamagui', value: 0.02 },
{ name: 'RNW', value: 0.063 },
{ name: 'Dripsy', value: 0.108 },
{ name: 'Stitches', value: 0.037 },
{ name: 'Emotion', value: 0.069 },
{ name: 'SC', value: 0.081 },
]}
/>
> Since styled-components and Emotion don't offer a first-class variant API,
> this was done via prop interpolation.
### Updating inline styles
Tamagui has a big advantage for inline styles, it's the only library to compile
them and flatten the tree.
<BenchmarkChart
data={[
{ name: 'Tamagui', value: 0.025 },
{ name: 'RNW', value: 0.06 },
{ name: 'Dripsy', value: 0.266 },
{ name: 'Stitches', value: 0.027 },
{ name: 'Emotion', value: 0.047 },
]}
/>
---
### Fully dynamic styles
These benchmarks don't benefit from the compiler. The React Native API surface
is much more feature-rich than DOM.
<BenchmarkChart
data={[
{ name: 'Tamagui', value: 31.0 },
{ name: 'Dripsy', value: 57.5 },
{ name: 'Stitches', value: 14.5 },
{ name: 'Emotion', value: 49.01 },
]}
/>
> Note: This test was taken from the
> [styled-components benchmarks](https://github.com/styled-components/styled-components/tree/main/packages/benchmarks).
### Mounting deep tree
In this test, we mount a tree with many nested nodes.
<BenchmarkChart
data={[
{ name: 'Tamagui', value: 18.61 },
{ name: 'Dripsy', value: 44.43 },
{ name: 'Stitches', value: 8.32 },
{ name: 'Emotion', value: 42.49 },
{ name: 'SC', value: 51.4 },
]}
/>
- SC - Styled Components
> Note: This test was taken from the
> [styled-components benchmarks](https://github.com/styled-components/styled-components/tree/main/packages/benchmarks).
## intro/colors
---
title: Colors
description: Color palettes and customization options
---
<Notice theme="blue">
This is the Tamagui documentation site's [own color
palette](https://github.com/tamagui/tamagui/blob/main/code/core/themes/src/tokens.tsx).
You can customize your own using
[@tamagui/create-theme](https://github.com/tamagui/tamagui/blob/738011d99e1dec6002a916edf424b75bb6cc0336/packages/themes/src/themes.tsx).
See [@tamagui/colors](https://github.com/tamagui/tamagui/tree/main/code/core/colors/src)
for Radix colors, or BYO color palette.
</Notice>
<ColorsDemo />
## intro/compiler-install
---
title: Tamagui Compiler
description: Adding the compiler to your apps
---
<IntroParagraph>
The Tamagui Compiler significantly improves performance of both web and native
applications through partial analysis and view flattening.
</IntroParagraph>
See the [Benchmarks](/docs/intro/benchmarks) or a more
[in-depth background](/docs/intro/why-a-compiler). Note that Tamagui features
work at compile-time and runtime, so installing the compiler is optional, and in
fact we recommend only setting it up once you're ready for production.
The compiler uses Babel to analyze JSX and `styled` functions, then attempts to
statically analyze and optimize them down to platform-native primitives.
The end result is less abstraction - like a `div` on web, or plain React Native
`View` on native:
<br />
<br />
<TamaguiExamplesCode />
<Notice theme="blue">
The compiler generates built versions of your components and config into a `.tamagui`
directory. You'll want to add that directory to your `.gitignore`.
</Notice>
## Configuration with tamagui.build.ts
We recommend creating a `tamagui.build.ts` file in your project root as the
single source of truth for your compiler configuration. All bundler plugins and
the CLI automatically read from this file, so you only need to define your
options once.
This file lets the Tamagui CLI read your config and perform operations like
generating CSS, pre-compiling components, and verifying optimizations — while
also sharing that same configuration with whichever bundler plugin you use.
Without it, you'd need to duplicate options across your metro, babel, vite,
or webpack configs.
```ts fileName="tamagui.build.ts"
import type { TamaguiBuildOptions } from 'tamagui'
export default {
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: './public/tamagui.generated.css',
// optional:
importsWhitelist: ['constants.js', 'colors.js'],
disableExtraction: process.env.NODE_ENV === 'development',
} satisfies TamaguiBuildOptions
```
With this file in place, your bundler plugins can be configured with no options
at all — they'll pick up everything from `tamagui.build.ts`:
```tsx
// vite: tamaguiPlugin()
// webpack: new TamaguiPlugin()
// metro: withTamagui(config)
// all read from tamagui.build.ts automatically
```
You can still pass options directly to a plugin, and they'll be merged with
(and override) the options from `tamagui.build.ts`.
## Install
There are plugins for a variety of bundlers, or you can use the `@tamagui/cli`
to compile in-place:
### Webpack
```bash
yarn add tamagui-loader
```
We have a full example of a plain Webpack or Vite setup in the simple starter
accessible through `npm create tamagui@latest`, which shows a complete
configuration with more detail.
Add `tamagui-loader` and set up your `webpack.config.js`.
You can set it up more manually like so:
```js
const { shouldExclude } = require('tamagui-loader')
const tamaguiOptions = {
config: './tamagui.config.ts',
components: ['tamagui'],
importsWhitelist: ['constants.js', 'colors.js'],
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
// optional advanced optimization of styled() definitions within your app itself, not just ones in your components option
// default is false
enableDynamicEvaluation: false,
}
module.exports = {
resolve: {
alias: {
// Resolve react-native to react-native-web
'react-native$': require.resolve('react-native-web'),
// optional, for lighter svg icons on web
'react-native-svg': require.resolve('@tamagui/react-native-svg'),
},
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
// you'll likely want to adjust this helper function,
// but it serves as a decent start that you can copy/paste from
exclude: (path) => shouldExclude(path, __dirname, tamaguiOptions),
use: [
// optionally thread-loader for significantly faster compile!
'thread-loader',
// works nicely alongside esbuild
{
loader: 'esbuild-loader',
},
{
loader: 'tamagui-loader',
options: tamaguiOptions,
},
],
},
],
},
}
```
Or you can use the TamaguiPlugin which automates some of this setup for you.
If you have a `tamagui.build.ts`, you can pass no options:
```tsx
const { TamaguiPlugin } = require('tamagui-loader')
module.exports = {
plugins: [
// reads from tamagui.build.ts automatically
new TamaguiPlugin(),
],
}
```
Or pass options inline to override:
```tsx
const { TamaguiPlugin } = require('tamagui-loader')
module.exports = {
plugins: [
new TamaguiPlugin({
config: './tamagui.config.ts',
components: ['tamagui'],
importsWhitelist: ['constants.js', 'colors.js'],
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
}),
],
}
```
Some notes on the options:
- _importsWhitelist_: Tamagui takes a conservative approach to partial
evaluation, this field whitelists (matching against both .ts and .js) files to
allow files that import them to read and use their values during compilation.
Typically colors and constants files.
- _disableExtraction_: Useful for faster developer iteration as your design
system hot reloads more reliably.
### Vite
See the [Vite guide](/docs/guides/vite) for more complete setup.
<Notice theme="yellow">
`@tamagui/vite-plugin` is ESM-only. Your project must have `"type": "module"` in its `package.json` (or use `.mjs`/`.mts` config files).
</Notice>
Add `@tamagui/vite-plugin` and update your `vite.config.ts`. If you have a
`tamagui.build.ts`, no options are needed:
```tsx
import { tamaguiPlugin } from '@tamagui/vite-plugin'
export default defineConfig({
plugins: [
// reads from tamagui.build.ts automatically
tamaguiPlugin(),
],
})
```
Or pass options inline:
```tsx
import { tamaguiPlugin } from '@tamagui/vite-plugin'
export default defineConfig({
plugins: [
tamaguiPlugin({
config: 'src/tamagui.config.ts',
components: ['tamagui'],
disableExtraction: true,
}),
],
})
```
### Next.js
See the [guide](/docs/guides/next-js) for more complete setup.
Next.js with Turbopack (the default) works best with the CLI approach — create a
`tamagui.build.ts` and use `tamagui build` to optimize production builds. No
bundler plugin needed.
For older Webpack-based Next.js setups, add `@tamagui/next-plugin` and configure
your `next.config.js`:
```js
const { withTamagui } = require('@tamagui/next-plugin')
module.exports = function (name, { defaultConfig }) {
const tamaguiPlugin = withTamagui({
// reads from tamagui.build.ts automatically, or pass inline:
config: './tamagui.config.ts',
components: ['tamagui'],
disableExtraction: process.env.NODE_ENV === 'development',
excludeReactNativeWebExports: ['Switch', 'ProgressBar', 'Picker'],
})
return {
...defaultConfig,
...tamaguiPlugin(defaultConfig),
}
}
```
Note: If running into issues, the environment variable `IGNORE_TS_CONFIG_PATHS`
to "true" can fix issues with Tamagui being resolved incorrectly.
See the [Next.js Guide](/docs/guides/next-js) for more details on setting up
your app.
### Babel / Metro
Note that the `@tamagui/babel-plugin` is completely optional, and on native
Tamagui doesn't optimize as much as on web, so leaving it out is actually
recommended to start. If later on you feel the need for a bit more speed, you
can try adding it.
```bash
yarn add @tamagui/babel-plugin
```
Add to your `babel.config.js`. With a `tamagui.build.ts`, you can pass no
options:
```js
module.exports = {
plugins: [
// reads from tamagui.build.ts automatically
'@tamagui/babel-plugin',
],
}
```
Or pass options inline:
```js
module.exports = {
plugins: [
[
'@tamagui/babel-plugin',
{
components: ['tamagui'],
config: './tamagui.config.ts',
importsWhitelist: ['constants.js', 'colors.js'],
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
},
],
],
}
```
### Expo
[Check out the Expo guide](/docs/guides/expo) for more information on setting up
Expo. It's as simple as adding the babel plugin.
### CLI-Based In-Place Compilation
For bundlers that don't have a Tamagui plugin yet (like Turbopack), or if you
prefer a simple setup, you can use `@tamagui/cli` to pre-compile your components
in-place before your build step.
This approach is meant for **production builds only** and should run in your
deployment pipeline, not during development. It rewrites files in place which
will mess up your working directory, but makes it highly compatible with any
bundler or tool. The downside is you don't get the helpful development
compatibility parts of the plugins, plus dev-mode debugging and `data-`
attributes.
For complete CLI documentation including all available commands, see the
[CLI Guide](/docs/guides/cli).
#### Setup
1. Install:
```bash
yarn add -D @tamagui/cli
```
2. Create a `tamagui.build.ts` if you haven't already (see
[above](#configuration-with-tamaguibuildts)).
3. Add a build script to your `package.json`:
```json
{
"scripts": {
"build": "tamagui build ./src -- next build"
}
}
```
#### Usage
```bash
# Build all components in a directory (web + native by default)
npx tamagui build ./src
# Build for web only
npx tamagui build --target web ./src
# Build for native only
npx tamagui build --target native ./src
# Build a specific file
npx tamagui build ./src/components/MyComponent.tsx
# Include/exclude patterns
npx tamagui build --include "components/**" --exclude "**/*.test.tsx" ./src
# Output to a separate directory (source files unchanged)
npx tamagui build --output ./dist ./src
# Create platform-specific files next to source files (.web.tsx or .native.tsx)
npx tamagui build --target native --output-around ./src
# Preview changes without writing files
npx tamagui build --dry-run ./src
# Verify minimum optimizations (useful in CI)
npx tamagui build --target web --expect-optimizations 10 ./src
```
#### CI Verification with --expect-optimizations
The `--expect-optimizations` flag ensures your build is actually optimizing components. This is useful in CI to catch configuration issues:
```json
{
"scripts": {
"build": "tamagui build --target web --expect-optimizations 10 ./src -- next build"
}
}
```
If the compiler produces fewer than the expected number of optimizations, the build will fail with an error message showing the actual count. This helps catch:
- Misconfigured `components` array
- Wrong source paths
- Configuration files not being found
#### Platform-Specific File Handling
The CLI automatically handles platform-specific files (`.web.tsx`,
`.native.tsx`, `.ios.tsx`, `.android.tsx`):
- Files with `.web.tsx` extensions are optimized for web only
- Files with `.native.tsx`, `.ios.tsx`, or `.android.tsx` extensions are
optimized for native only
- Base files (`.tsx`) without platform-specific versions are optimized for all
platforms
- If both `.web.tsx` and `.native.tsx` exist, the base `.tsx` file is skipped
#### Package.json Exports Support
The CLI supports `package.json` exports for path-specific imports. For example:
```json
{
"exports": {
".": "./src/index.tsx",
"./components/Button": "./src/Button.tsx"
}
}
```
Both import styles work:
```tsx
import { Button } from '@my/ui'
import { Button } from '@my/ui/components/Button'
```
#### Integration Examples
This works with **any build tool** - just run `tamagui build` before your build
command. Here are some examples:
**Next.js with Turbopack** (Turbopack doesn't support plugins yet):
```json
{
"scripts": {
"dev": "next dev --turbopack",
"build": "tamagui build --target web ./src -- next build"
}
}
```
**Vite, Remix, or any other bundler:**
```json
{
"scripts": {
"build": "tamagui build --target web ./src -- vite build"
}
}
```
**React Native / Expo:**
```json
{
"scripts": {
"build:ios": "tamagui build --target native ./src -- eas build --platform ios",
"build:android": "tamagui build --target native ./src -- eas build --platform android"
}
}
```
**Using --output (no file restoration needed):**
If you prefer to output optimized files to a separate directory instead of modifying
source files in-place, use `--output`:
```json
{
"scripts": {
"build": "tamagui build --target web --output ./dist ./src && next build"
}
}
```
With `--output`, your source files are never modified. The optimized files are
written to the output directory with their directory structure preserved.
**Using --output-around (platform-specific files):**
The `--output-around` flag creates optimized platform-specific files (`.web.tsx` or
`.native.tsx`) next to your source files instead of modifying them. Bundlers
automatically pick up these files via platform-specific resolution:
```json
{
"scripts": {
"prebuild:native": "tamagui build --target native --output-around ./src",
"prebuild:web": "tamagui build --target web --output-around ./src"
}
}
```
This transforms `Button.tsx` → creates `Button.native.tsx` or `Button.web.tsx`
alongside it. Metro/Expo uses `.native.tsx` on native, and web bundlers use `.web.tsx`.
The Tamagui CLI optimizes your components in-place (or to an output directory),
then your bundler processes the already-optimized files.
**Learn more:** See the [CLI Guide](/docs/guides/cli) for documentation on all
CLI commands including `check`, `generate`, `add`, and more.
## Props
All compiler plugins accept the same options:
<PropsTable
data={[
{
name: 'config',
required: false,
type: 'string',
typeSimple: 'enum',
default: "'./tamagui.config.ts'",
description:
'Relative path to your tamagui.config.ts file which should export default the result from createTamagui. Can be set in tamagui.build.ts instead.',
},
{
name: 'components',
required: false,
type: 'string[]',
typeSimple: 'enum',
default: "['tamagui']",
description: `Array of npm modules containing Tamagui components which you'll be using in your app. For example: if you are using the base Tamagui components. This directs the compiler to load and optimize.`,
},
{
name: 'importsWhitelist',
required: false,
type: 'string[]',
typeSimple: 'enum',
description: `Array of whitelisted file paths (always end in .js) which the compiler may try and import and parse at build-time. It is normalized to ".js" ending for all file extensions (js, jsx, tsx, ts). This usually should be set to something like ['constants.js', 'colors.js'] for example, where you have a couple mostly static files of constants that are used as default values for styles.`,
},
{
name: 'logTimings',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'true',
description:
'Tamagui outputs information for each file it compiles on how long it took to run, how many components it optimized, and how many it flattened. Set to false to disable these logs.',
},
{
name: 'disable',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description: 'Disable everything - debug and extraction.',
},
{
name: 'disableExtraction',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description:
'Disable extraction to CSS completely, instead fully relying on runtime. Setting this to true speed up development as generally your app will hot reload the Tamagui configuration itself.',
},
{
name: 'disableDebugAttr',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description:
'If enabled along with disableExtraction, all parsing will turn off. Normally turning off disableExtraction will keep the helpful debug attributes in DOM',
},
{
name: 'disableFlattening',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description: 'Turns off tree-flattening.',
},
{
name: 'enableDynamicEvaluation',
required: false,
type: 'boolean',
typeSimple: 'enum',
default: 'false',
description:
'(Experimental) Enables further extracting of any styled component, even if not in your components. See below for more information.',
},
{
name: 'outputCSS',
required: false,
type: 'string',
typeSimple: 'enum',
description:
'Path to output extracted CSS. When set, the compiler automatically sets TAMAGUI_DID_OUTPUT_CSS which tree-shakes all runtime CSS generation code from your bundle (~6KB gzipped savings).',
},
]}
/>
### Dynamic Evaluation
By default the Tamagui compiler only optimizes `styled` expressions found in the
modules defined by your `components` config. This means if you do an inline
`styled()` inside your actual app directory, it will default to runtime style
insertion.
This is typically Good Enough™️. As long as you define most of your common
components there, you'll get a very high hit rate of compiled styles being used
and runtime generation being skipped, as atomic styles with your design system
tokens will be mostly pre-generated.
Tamagui has _experimental_ support for loading any component, even if it occurs
somewhere outside your configured components modules. This is called "dynamic
loading", for now. You can enable it with the setting `enableDynamicEvaluation`
as seen above in the props table.
The way it works is, when the compiler detects a styled() expression outside one
of the defined component directories, it will run the following:
1. First, read the file and use a custom `babel` transform to _force_ all
top-level variables to be exported.
2. Then, run `esbuild` and bundle the entire file to a temporary file in the
same directory, something like `.tamagui-dynamic-eval-ComponentName.js`
3. Now, read the file in and load all new definitions found.
4. Finally, continue with optimization, using the newly optimized component.
You may see why this is experimental. It's very convenient as a developer, but
has a variety of edge cases that can be confusing or breaking, and we want to
avoid installation woes. Though it does continue on error and work generally, it
outputs warnings in Webpack currently due to our plugin not properly indicating
to Webpack about the new files (a fixable bug), which causes big yellow warning
output and a cache de-opt.
We're leaving this feature under the environment variable while it matures. Let
us know if you find it useful.
### Disabling the compiler
You can disable the compiler optimizations for an entire file with a comment at
the top of your file:
```tsx
// tamagui-ignore
```
You can disable the compiler optimization for a single component with the
boolean property `disableOptimization`:
```tsx
import { View } from '@tamagui/core'
export default () => <View disableOptimization />
```
### Web-only apps
This only applies to web-only apps. Native and universal apps already have
`react-native` installed. For web-only apps, you still need `react-native`
types in the parent project or workspace root for prop typing and autocomplete.
You can either install `react-native` directly:
```bash
yarn add react-native
```
Or, if you only want the types:
```bash
yarn add react-native@0.0.0 @types/react-native
```
Tamagui's web runtime doesn't require `react-native` at runtime, but current
prop typing and autocomplete rely on the `react-native` types being present.
## intro/errors
---
title: Errors
description: Common error messages and how to resolve them
---
### Error 001
Haven't called createTamagui yet.
This often happens due to having duplicate Tamagui sub-dependencies.
Tamagui needs every `@tamagui/*` dependency to be on the exact same version, we
include an upgrade script with the starter kits that you can call with "yarn
upgrade:tamagui" to help with this.
You may want to clear your node_modules as well and run a fresh install after
upgrading.
### Error 002
Using global config fallback. This may indicate duplicate tamagui instances
(e.g., from Vite SSR bundling). This is handled automatically, but may cause
issues due to duplicate tamagui modules if ignored.
### Warning 001
#### Ignorable modules
This error means the compiler can't bundle some dependencies into what it reads
at compile-time in order to optimize. It's not necessarily an error and usually
won't de-opt.
To see a full stack trace set:
```bash
DEBUG=tamagui
```
To disable the warning you can set:
```bash
TAMAGUI_IGNORE_BUNDLE_ERRORS=some_module_name,some_other_module_name
# Or to disable them all (not recommended):
TAMAGUI_IGNORE_BUNDLE_ERRORS=true
```
### Warning 002
You're rendering a Tamagui <Adapt /> component without nesting it inside a
parent that is able to adapt.
## intro/installation
---
title: Installation
description: Get Tamagui set up, step by step
---
<IntroParagraph>
Tamagui is easy to set up as we've invested into "0-config" installs for all bundlers.
That said, it's easy to get lost if you start configuring too many things too early.
</IntroParagraph>
Use `npm create tamagui@latest` to pick one of our starter templates.
## Requirements
- **React Native 0.81+** with New Architecture enabled (for native apps)
- **React 19+**
- **TypeScript 5+** (required)
Tamagui 2 takes advantage of new style features in React Native 0.81+ including
`boxShadow`, `filter`, and more. On web, there are no version restrictions.
## Installation
To install from scratch:
```bash
yarn add @tamagui/core
```
Core is just the style library. If you plan to use our full UI kit, you can
avoid installing `@tamagui/core` and instead:
```bash
yarn add tamagui
```
The `tamagui` package is a superset of core, so anywhere the docs reference
`@tamagui/core`, you can replace it with `tamagui`.
<Notice>
This applies to web-only apps. If you're building a native or universal app,
you already have `react-native` installed. For web-only apps, you still need
`react-native` types in the parent project for proper prop typing and
autocomplete. You can either install `react-native` directly:
```bash
yarn add react-native
```
Or, if you only want the types:
```bash
yarn add react-native@0.0.0 @types/react-native
```
Tamagui's web runtime is self-contained. In a monorepo, install either option
in the parent app or workspace root rather than a leaf package.
</Notice>
Next, you'll want to set up your [configuration](/docs/core/configuration) and
provide it with `TamaguiProvider`:
```tsx fileName="App.tsx"
import { TamaguiProvider, View } from '@tamagui/core'
import config from './tamagui.config' // your configuration
export default function App() {
return (
<TamaguiProvider config={config} defaultTheme="light">
<View width={200} height={200} backgroundColor="$background" />
</TamaguiProvider>
)
}
```
We have a recommended preset configuration called `@tamagui/config`:
```bash
yarn add @tamagui/config
```
You can use it like so:
```tsx fileName="App.tsx"
import { TamaguiProvider, createTamagui } from '@tamagui/core'
import { defaultConfig } from '@tamagui/config/v5'
// you usually export this from a tamagui.config.ts file
const config = createTamagui(defaultConfig)
type Conf = typeof config
// make imports typed
declare module '@tamagui/core' {
interface TamaguiCustomConfig extends Conf {}
}
export default () => {
return <TamaguiProvider config={config} defaultTheme="light">{/* your app here */}</TamaguiProvider>
}
```
<Notice>
Set your root theme using the `defaultTheme` prop on `TamaguiProvider` rather than wrapping your app with `<Theme>`. This ensures the [`fastSchemeChange` setting](/docs/core/configuration#settings) works on native, and allows light/dark changes on web to use faster media query-based updates during SSR.
</Notice>
And that's it!
<Preview>
<DemoButton />
</Preview>
```tsx class=preview line=5
import { Button } from 'tamagui'
export default function Demo() {
return <Button theme="blue">Hello world</Button>
}
```
From here, we recommend
[spending some time understanding configuration](/docs/core/configuration).
100% of Tamagui features work at both runtime and compile-time, which makes it
both easy to use and fast. Because it works fully at runtime, and because we've
invested a lot into building it such that it doesn't need any special bundler
configuration to work on either native or web, you can begin using Tamagui with
just the above setup.
---
## Next Steps
Once you have Tamagui installed, you'll want to explore:
- **[CLI](/docs/guides/cli)** - Command-line tools for building, optimizing, and
managing your Tamagui projects
- **[Bundler Setup](/docs/guides/next-js)** - Integration guides for Next.js,
Expo, Vite, Webpack, Metro, and more
- **[Design Systems](/docs/guides/design-systems)** - Build your own optimized
component library with Tamagui
Later on, you can [set up the compiler](/docs/intro/compiler-install) to gain
more performance and some nice development helpers.
---
## Bundler Guides
Tamagui generally doesn't require any special bundler setup as we've worked hard
to make it "just work" without configuration in a wide variety of environments.
That said, the broader React Native and React Native Web ecosystem is filled
with packages that do need configuration. Tamagui provides a variety of bundler
plugins that help with that:
<Spacer />
<Grid gap="$4">
<LogoCard
title="Next.js"
img="/logos/next-js.svg"
link="/docs/guides/next-js"
subtitle="Full-featured React framework with great developer experience."
colorOffset={2}
/>
<LogoCard
title="Expo"
img="/logos/expo.svg"
link="/docs/guides/expo"
subtitle="Platform for creating universal native apps with JavaScript and React."
colorOffset={6}
/>
<LogoCard
title="Vite"
img="/logos/vite.svg"
link="/docs/guides/vite"
subtitle="Fast and modern development server and build tool."
colorOffset={1}
/>
<LogoCard
title="Webpack"
img="/logos/webpack.svg"
link="/docs/guides/webpack"
subtitle="Powerful module bundler for modern JavaScript applications."
colorOffset={0}
/>
<LogoCard
title="Metro"
img="/logos/metro.svg"
link="/docs/guides/metro"
subtitle="Fast, scalable, and serverless JavaScript bundler for React Native."
colorOffset={3}
/>
<LogoCard
title="One"
img="/logos/one.svg"
link="/docs/guides/one"
subtitle="Universal React framework with native support."
colorOffset={4}
/>
</Grid>
## intro/introduction
---
title: Introduction
---
<DocsIntro />
<GetStarted />
#### Highlights
<Highlights
disableLinks
disableTitle
large
features={[
`Core only has one dependency - React - but supports the full React Native View and Text API, a superset of the React Native Style API, styled(), powerful hooks, and the typed design system helpers in ~28Kb on web.`,
`A smart, partial-evaluating compiler gives 0-runtime performance with the ergonomics of writing your code however you want - even inline, logic-filled code is optimized.`,
`Every feature works at runtime and compile-time, so none of the usual limits of 0-runtime libraries, while optionally getting the same great performance.`,
`useTheme and useMedia hooks with signal-like granularity and dirty tracking.`,
`Unstyled and styled versions of all components.`,
]}
/>
---
## Community
Join us on:
- [Discord](https://discord.gg/4qh6tdcVDa)
- [X](https://x.com/tamagui_js)
- [GitHub](https://github.com/tamagui/tamagui)
---
## Credits
A big thanks to:
- [Stitches](https://stitches.dev) for the variants pattern.
- [JSXStyle](https://github.com/jsxstyle/jsxstyle) for providing the original
version of the compiler.
- [Modulz](https://github.com/modulz) for Radix of which we've adopted many APIs, and for the initial structure for this website.
- [Framer Motion](https://github.com/framer/motion) for the AnimatePresence
implementation.
## intro/props
---
title: Tamagui Props
description: All the base props
---
Tamagui supports a superset of the React Native props. Start with:
- [View](https://reactnative.dev/docs/view-style-props), or
- [Text](https://reactnative.dev/docs/text-style-props)
From there, we add [style props](/docs/intro/styles) directly onto the same
object.
Finally, there are a few non-style props Tamagui adds:
## Event Props
All Tamagui View-based components have Pressable-like functionality built in. You don't need to wrap in `Pressable` or `TouchableOpacity` - add event handlers directly:
```tsx
<View
onPress={() => console.log('pressed')}
onHoverIn={() => console.log('hovered')}
pressStyle={{ opacity: 0.8 }}
hoverStyle={{ backgroundColor: '$backgroundHover' }}
/>
```
<PropsTable
data={[
{
name: 'onPress',
required: false,
type: '(e: GestureResponderEvent) => void',
description: (
<span>Called when a press is released. Works on both web and native.</span>
),
},
{
name: 'onPressIn',
required: false,
type: '(e: GestureResponderEvent) => void',
description: <span>Called immediately when a press starts.</span>,
},
{
name: 'onPressOut',
required: false,
type: '(e: GestureResponderEvent) => void',
description: <span>Called when a press is released or cancelled.</span>,
},
{
name: 'onLongPress',
required: false,
type: '(e: GestureResponderEvent) => void',
description: (
<span>
Called when a press is held for longer than <code>delayLongPress</code> (default
500ms).
</span>
),
},
{
name: 'onHoverIn',
required: false,
type: '(e: MouseEvent) => void',
description: (
<span>
<strong>Web Only</strong>: Called when the mouse enters the element. Maps to{' '}
<code>onMouseEnter</code>.
</span>
),
},
{
name: 'onHoverOut',
required: false,
type: '(e: MouseEvent) => void',
description: (
<span>
<strong>Web Only</strong>: Called when the mouse leaves the element. Maps to{' '}
<code>onMouseLeave</code>.
</span>
),
},
{
name: 'onFocus',
required: false,
type: '(e: FocusEvent) => void',
description: <span>Called when the element receives focus.</span>,
},
{
name: 'onBlur',
required: false,
type: '(e: FocusEvent) => void',
description: <span>Called when the element loses focus.</span>,
},
{
name: 'delayPressIn',
required: false,
type: 'number',
description: (
<span>
Duration in ms to wait before calling <code>onPressIn</code>.
</span>
),
},
{
name: 'delayPressOut',
required: false,
type: 'number',
description: (
<span>
Duration in ms to wait before calling <code>onPressOut</code> after press is
released.
</span>
),
},
{
name: 'delayLongPress',
required: false,
type: 'number',
description: (
<span>
Duration in ms to wait before calling <code>onLongPress</code>. Default is
500ms.
</span>
),
},
{
name: 'minPressDuration',
required: false,
type: 'number',
description: (
<span>
Minimum duration in ms that a press must be held before <code>onPressOut</code>{' '}
is called.
</span>
),
},
{
name: 'cancelable',
required: false,
type: 'boolean',
description: (
<span>
When true, allows the press to be cancelled by scrolling or other gestures.
</span>
),
},
{
name: 'disabled',
required: false,
type: 'boolean',
description: <span>Disables all press and hover interactions on the element.</span>,
},
{
name: 'focusable',
required: false,
type: 'boolean',
description: (
<span>
Whether the element can receive focus. On web, controls <code>tabIndex</code>.
</span>
),
},
{
name: 'hitSlop',
required: false,
type: `number | Insets`,
description: (
<span>
Extends the pressable area beyond the element bounds. Accepts a number (applies
to all sides) or an object with <code>top</code>, <code>bottom</code>,{' '}
<code>left</code>, <code>right</code>.
</span>
),
},
]}
/>
Use these events alongside style states like `pressStyle`, `hoverStyle`, and `focusStyle` to create interactive components. See the [Style API](/docs/intro/styles) for styling on interaction states.
## Other Props
<PropsTable
data={[
{
name: 'render',
required: false,
type: `string | JSX.Element | ((props, state) => JSX.Element)`,
description: (
<span>
Control which element or component is rendered. Accepts an HTML tag name (e.g., <code>"button"</code>, <code>"a"</code>),
a JSX element, or a render function receiving props and component state. String render props are compiler-optimized.
See <a href="/docs/core/styled#render">styled() render documentation</a> for details.
</span>
),
},
{
name: 'transition',
required: false,
type: 'string',
description: (
<span>
Apply a transition animation as defined in your createTamagui configuration. See the{' '}
<a href="/docs/core/animations">Animations documentation</a> for more details on configuring animation drivers.
</span>
),
},
{
name: 'animateOnly',
required: false,
type: 'string[]',
description: (
<span>
A list of properties to animate. Note that flat transforms can only be controlled via `transform`. Learn more in the{' '}
<a href="/docs/core/animations#granular-animations">Granular animations</a> section.
</span>
),
},
{
name: 'theme',
required: false,
type: 'string',
description: (
<span>
Apply a theme or sub-theme to this component and the components below it.
</span>
),
},
{
name: 'themeInverse',
required: false,
type: 'boolean',
description: (
<span>
Invert light or dark theme for the sub-tree.
</span>
),
},
{
name: 'themeShallow',
required: false,
type: 'boolean',
description: (
<span>
Used in combination with the theme prop, it prevents the theme from passing to children.
</span>
),
},
{
name: 'forceStyle',
required: false,
type: `'hover' | 'press' | 'focus' | 'focusVisible' | 'focusWithin'`,
description: (
<span>
Forces the pseudo style state to be on.
</span>
),
},
{
name: 'group',
required: false,
type: `boolean | string`,
description: (
<span>
Marks this component as a group for use in styling children based on parents named group.
</span>
),
},
{
name: 'componentName',
required: false,
type: 'string',
description: (
<span>
Equivalent to "name" property on styled() for automatically applying a theme.
</span>
),
},
{
name: 'className',
required: false,
type: 'string',
description: (
<span>
<strong>Web Only</strong>: An escape hatch to set className. This works fully with the compiler, which will concat your defined className with its generated ones.
</span>
),
},
{
name: 'disableClassName',
required: false,
type: 'boolean',
description: (
<span>
<strong>Web Only</strong>: Disables className output of styles, instead using only inline styles.
</span>
),
},
{
name: 'tag',
required: false,
type: 'string',
description: (
<span>
<strong>Web Only</strong>: Renders the final element as the given tag. Note: React Native Web doesn't support tag and as such if using any animation driver except CSS the tag prop will stop working. We'd recommend using "role" for most cases.
</span>
),
},
{
name: 'space',
required: false,
deprecated: true,
type: "boolean | string | TamaguiConfig['space']",
description: (
<span>
Use gap instead -
Spacing is built into Tamagui and can accept a number or Token.space value. This will
filter out any nullish child elements and insert a spacer components between the remaining
elements.
</span>
),
},
{
name: 'spaceDirection',
required: false,
deprecated: true,
default: 'both',
type: `'horizontal' | 'vertical' | 'both'`,
description: (
<span>
By default the space inserted is a square, but if you set it to horizontal it will be 0px tall, vertical will be 0px wide.
</span>
),
},
{
name: 'debug',
required: false,
type: `boolean | 'verbose' | 'break'`,
description: (
<span>
When set Tamagui will output a variety of helpful information on how it parsed and
applied styling. Verbose will output more information, and break will hit a debugger at
the start of render in development mode.
</span>
),
},
{
name: 'untilMeasured',
required: false,
type: `'hide' | 'show'`,
description: (
<span>
Works only alongside group, when children of the group are using container based sizing on native you can hide them until parent is measured.
</span>
),
},
{
name: 'disableOptimization',
required: false,
type: 'boolean',
description: (
<span>
Disable all compiler optimization.
</span>
),
},
{
name: 'tabIndex',
required: false,
type: `string | number`,
description: (
<span>
Used for controlling the order of focus with keyboard or assistive device navigation.
</span>
),
},
{
name: 'role',
required: false,
type: 'Role',
description: (
<span>
Indicates to accessibility services to treat UI component like a specific role.
</span>
),
},
{
name: 'asChild',
required: false,
type: `boolean | 'except-style' | 'except-style-web' | 'web'`,
description: (
<span>
When true, Tamagui expects a single child element. Instead of rendering its own element, it will pass all props to that child, merging together any event handling props. When "except-style", the same behavior except Tamagui won't pass styles down from the parent, only non-style props.
</span>
),
},
{
name: 'passThrough',
required: false,
type: 'boolean',
description: (
<span>
Advanced use only. React will "re-parent" components when it's internal tree structure changes.
When making UI that adapts to another look, for better performance and avoiding losing user state (like focus, or uncontrolled text input), you can use passThrough rather than conditionally rendering a wrapping component.
</span>
),
},
]} />
## Event Handlers
Tamagui supports all React Native events on all platforms. On web, we also support all standard DOM event handlers:
### Cross-Platform Events
- `onPress`, `onLongPress`, `onPressIn`, `onPressOut` - Touch/press events that work on both native and web
**Pointer Events** - Modern events that unify mouse, touch, and pen input. On native, these are mapped to touch events with a normalized event shape including `clientX/Y`, `pointerId`, and `pointerType`:
- `onPointerDown`, `onPointerUp`, `onPointerMove`
- `onPointerEnter`, `onPointerLeave`, `onPointerCancel`
On native, `e.target.setPointerCapture(pointerId)` and `releasePointerCapture(pointerId)` are supported for drag scenarios where you need to receive move events outside the element bounds.
### Web-Only Events
The following events are available on web and automatically ignored on native:
**Mouse Events**
- `onClick`, `onDoubleClick`, `onContextMenu`
- `onMouseEnter`, `onMouseLeave`, `onMouseMove`, `onMouseOver`, `onMouseOut`
- `onMouseDown`, `onMouseUp`
- `onWheel`
**Keyboard Events**
- `onKeyDown`, `onKeyUp`, `onKeyPress`
**Drag and Drop**
- `onDrag`, `onDragStart`, `onDragEnd`
- `onDragEnter`, `onDragLeave`, `onDragOver`
- `onDrop`
**Input Events**
- `onChange`, `onInput`, `onBeforeInput`
**Scroll Events**
- `onScroll`
**Clipboard Events**
- `onCopy`, `onCut`, `onPaste`
**Focus Events**
- `onFocus`, `onBlur`
All event handlers receive the standard React synthetic event as their argument.
## intro/static
---
title: Static Optimization
description: How @tamagui/static works to speed up code
---
For details on how the Tamagui compiler optimizes your code through static analysis, see:
- [The Frontend Trilemma](/docs/intro/why-a-compiler) - Background on why a compiler helps
- [Compiler Installation](/docs/intro/compiler-install) - How to set up the compiler
- [Benchmarks](/docs/intro/benchmarks) - Performance comparisons
## intro/styles
---
title: Styling
description: 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](/docs/core/view-and-text), or through the
[`styled()` function](/docs/core/styled) as the second argument.
Here's how that looks in practice:
```tsx
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):
- [React Native View style props](https://reactnative.dev/docs/view-style-props)
- [React Native Text style props](https://reactnative.dev/docs/text-style-props)
The full Tamagui typed style props can be simplified to something like this,
except the values can also accept one of your design tokens:
```tsx
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
```tsx
<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](/docs/core/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.
```tsx
// 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:
```tsx
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:
```tsx
// 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:
```tsx
// 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`, `borderLeftWidth`
- `borderStyle` → `borderStyle`
- `borderColor` → `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`:
```tsx
// 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.
```tsx
<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`:
```tsx
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:
```tsx
<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:
```tsx
createTamagui({
media: {
sm: { maxWidth: 800 },
},
})
```
Then you can use it like:
```tsx
<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:
```tsx
<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:
```tsx
<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:
```tsx
<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:
```tsx
<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:
```tsx
<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:
```tsx
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:
```tsx
<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:
```tsx
<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`:
```tsx
<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:
```tsx
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:
```tsx
// 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.
## intro/themes
---
title: Themes
description: Create themes and sub-themes
---
Themes map neatly to CSS variables: they are objects whose values you want to
contextually change at any point in your React tree. They are used either as the
first lookup for `$` prefixed style values, or with the `useTheme` hook
directly. Tamagui allows nesting themes - both the definition and at runtime. At
runtime Tamagui resolves theme values upwards, ultimately all the way back to
tokens.
For the recommended theme setup, see the [Config v5 docs](/docs/core/config-v5).
Once you've learned the basics here, be sure to
[check out the ThemeBuilder guide](/docs/guides/theme-builder) for generating
more interesting theme suites.
If you want to style `bento` or `tamagui` components: see
[Styling `tamagui` UI components](#styling-tamagui-components).
<Notice theme="green">
If you want a copy-paste theme generation setup, [try this
gist](https://gist.github.com/natew/3be503cc5990a1142d17ffadf86a134f) for a
well-structured example.
</Notice>
You define a theme like this:
```tsx
const dark = {
background: '#000',
color: '#fff',
// define any key to any string or number value
}
```
If you use tokens, you can share values from tokens down to themes. Tokens act
as fallback values for themes, like global CSS variables vs scoped ones:
```tsx
const tokens = createTokens({
color: {
black: '#000',
white: '#fff',
},
})
// theme:
const dark = {
background: tokens.color.black,
color: tokens.color.white,
}
```
### Sub-themes
One of the unique powers of Tamagui is theme nesting. Define a theme with a
name in the form of `parentName_subName` and Tamagui will let you nest themes,
with both `parentName` and `subName` being valid theme names.
You can do this as many times as you'd like. Here's an example of having three
levels:
- `dark_green_subtle`
- `light_green_subtle`
```tsx
<Theme name="dark">
<Theme name="green">
<Button theme="subtle">Hello world</Button>
</Theme>
</Theme>
```
You can also access a specific sub-theme more specifically:
```tsx
<Theme name="dark">
<Button theme="green_subtle">Hello world</Button>
</Theme>
```
In general you want your themes to all be the same shape - the same named keys
and typed values - but sub-themes can be sub-sets of parent themes. The
`useTheme` hook and style system will resolve missing keys upwards to parent
themes, and ultimately to tokens.
#### Component themes
Every Tamagui `styled()` component looks for its own specific theme if you pass
it the `name` property. For example:
```tsx
import { View, styled } from 'tamagui' // or '@tamagui/core'
const Circle = styled(View, {
name: 'Circle',
backgroundColor: '$background',
})
```
The `name` attribute will be removed from the defaultProps and used internally
by Tamagui to check for a sub-theme that ends with `_Circle`.
Now you can create the default theme for all Circle components at any level of
nesting:
```tsx
const dark_Circle = {
background: 'darkred',
color: 'white',
}
const light_Circle = {
background: 'lightred',
color: 'black',
}
```
<Notice theme="blue">Component themes must have the first letter capitalized.</Notice>
- `dark_Circle`
- `dark_green_Circle`
- `dark_green_subtle_Circle`
This is an incredibly powerful and unique feature that allows authors of UI
components control over design, while still letting users customize them
completely.
---
### Styling Tamagui UI components
Tamagui comes in two parts: a core library and a full UI kit. The core library
(`@tamagui/core`) is flexible and doesn't have many rules. But the full UI kit
(`tamagui`) has some standard ways of doing things. This helps make everything
work well together.
In the `tamagui` UI kit, all components use these main theme keys:
- `background`: for background colors
- `color`: for text colors
- `borderColor`: for border colors
- `shadowColor`: for shadow colors
- `placeholderColor`: for placeholder text colors (doesn't change when you
interact with it)
It also uses special versions of these for when you hover, press, or focus on
something. For example, `backgroundHover` or `colorPress`.
These keys help standardize how you style components, and make for easy
re-theming. They are also optional, if you find the theme system too complex for
your use case, you can always just use plain old style props
...plus all the pseudo variants for each, eg, `backgroundHover`,
`backgroundPress`, and `backgroundFocus`.
This means that you can easily re-theme `tamagui`'s UI kit and your own
components together in both light and dark mode.
A minimal theme might look like this:
```tsx
const dark = {
// Standard keys for all components
background: '#000',
backgroundHover: '#111',
backgroundPress: '#222',
backgroundFocus: '#333',
backgroundStrong: '#444',
backgroundTransparent: 'rgba(0, 0, 0, 0.5)',
color: '#fff',
colorHover: '#eee',
colorPress: '#ddd',
colorFocus: '#ccc',
colorTransparent: 'rgba(255, 255, 255, 0.5)',
borderColor: '#555',
borderColorHover: '#666',
borderColorFocus: '#777',
borderColorPress: '#888',
placeholderColor: '#999',
outlineColor: '#aaa',
// Custom tokens like "brand"
brandBackground: '#000', // You can add your own tokens like "brand"
brandColor: '#fff', // and use them in your components
}
const light = {
// Standard keys for all components
background: '#fff',
backgroundHover: '#f5f5f5',
backgroundPress: '#e0e0e0',
backgroundFocus: '#d5d5d5',
backgroundStrong: '#ccc',
backgroundTransparent: 'rgba(255, 255, 255, 0.5)',
color: '#000',
colorHover: '#111',
colorPress: '#222',
colorFocus: '#333',
colorTransparent: 'rgba(0, 0, 0, 0.5)',
borderColor: '#444',
borderColorHover: '#555',
borderColorFocus: '#666',
borderColorPress: '#777',
placeholderColor: '#888',
outlineColor: '#999',
// Custom tokens like "brand"
brandBackground: '#000', // You can add your own tokens like "brand"
brandColor: '#fff', // and use them in your components
}
```
You can of course do all of this yourself in your own design system with
`styled`:
If you are building a component with more than one sub-components, you can
follow this pattern:
```tsx
import { GetProps, View, Text, styled } from 'tamagui' // or '@tamagui/core'
const ButtonFrame = styled(View, {
name: 'Button',
backgroundColor: '$background',
})
const ButtonText = styled(Text, {
name: 'ButtonText',
color: '$color',
})
type ButtonProps = GetProps<typeof ButtonFrame>
// styleable() wraps a functional component so it can be further styled with styled()
// see /docs/core/styled#styleable for full documentation
export const Button = ButtonFrame.styleable<ButtonProps>(
({ children, ...props }, ref) => {
return (
<ButtonFrame ref={ref} {...props}>
<ButtonText>{children}</ButtonText>
</ButtonFrame>
)
}
)
```
And now you can add two themes: `dark_Button` and `dark_ButtonText`, and
override their default styles.
## Full Example
Let's start with an example of inline styling with a subset of the
configuration:
```tsx
import { TamaguiProvider, createTokens, createTamagui, View, Theme } from 'tamagui'
const tokens = createTokens({
color: {
darkRed: '#550000',
lightRed: '#ff0000',
},
// ... see configuration docs for required tokens
})
const config = createTamagui({
tokens,
themes: {
dark: {
red: tokens.color.darkRed,
},
light: {
red: tokens.color.lightRed,
},
},
})
export const App = () => (
<TamaguiProvider config={config} defaultTheme="light">
<View backgroundColor="$red" />
<Theme name="dark">
<View backgroundColor="$red" />
</Theme>
</TamaguiProvider>
)
```
In this example we've set up darkRed and lightRed variables and a dark and light
theme that use those variables. Tamagui will handle defining:
```css
:root {
--colors-dark-red: #550000;
--colors-light-red: #ff0000;
}
.tui_dark {
--red: var(--colors-dark-red);
}
.tui_light {
--red: var(--colors-light-red);
}
```
Which will automatically apply at runtime, or can be gathered for use in SSR
using `Tamagui.getCSS()`.
Finally, the compiler on web will extract your views roughly as so:
```tsx
export const App = () => (
<Provider defaultTheme="light">
<div className="baCo-2nesi3" />
<Theme name="dark">
<div className="baCo-2nesi3" />
</Theme>
</Provider>
)
// CSS output:
// .color-2nesi3 { background-color: var(--red); }
```
## Ensuring valid types
Here's what we've landed on which helps ensure everything is typed properly.
Keep themes in a separate `themes.ts` file, and structure it like this:
```tsx
import { tokens } from './tokens'
const light = {
background: '#fff',
backgroundHover: tokens.color.gray3,
backgroundPress: tokens.color.gray4,
backgroundFocus: tokens.color.gray5,
borderColor: tokens.color.gray4,
borderColorHover: tokens.color.gray6,
color: tokens.color.gray12,
colorHover: tokens.color.gray11,
colorPress: tokens.color.gray10,
colorFocus: tokens.color.gray6,
shadowColor: tokens.color.grayA5,
shadowColorHover: tokens.color.grayA6,
}
// note: we set up a single consistent base type to validate the rest:
type BaseTheme = typeof light
// the rest of the themes use BaseTheme
const dark: BaseTheme = {
background: '#000',
backgroundHover: tokens.color.gray2Dark,
backgroundPress: tokens.color.gray3Dark,
backgroundFocus: tokens.color.gray4Dark,
borderColor: tokens.color.gray3Dark,
borderColorHover: tokens.color.gray4Dark,
color: '#ddd',
colorHover: tokens.color.gray11Dark,
colorPress: tokens.color.gray10Dark,
colorFocus: tokens.color.gray6Dark,
shadowColor: tokens.color.grayA6,
shadowColorHover: tokens.color.grayA7,
}
const dark_translucent: BaseTheme = {
...dark,
background: 'rgba(0,0,0,0.7)',
backgroundHover: 'rgba(0,0,0,0.5)',
backgroundPress: 'rgba(0,0,0,0.25)',
backgroundFocus: 'rgba(0,0,0,0.1)',
}
const light_translucent: BaseTheme = {
...light,
background: 'rgba(255,255,255,0.85)',
backgroundHover: 'rgba(250,250,250,0.85)',
backgroundPress: 'rgba(240,240,240,0.85)',
backgroundFocus: 'rgba(240,240,240,0.7)',
}
export const allThemes = {
dark,
light,
dark_translucent,
light_translucent,
} satisfies { [key: string]: BaseTheme }
// note: `satisfies` was introduced with TypeScript 4.9
```
## Dynamic Themes
Sometimes you want to defer loading themes, or change existing theme values at
runtime. Tamagui exports three helpers for this in the package `@tamagui/theme`
which exports `addTheme`, `updateTheme`, and `replaceTheme`.
### addTheme
<HeroContainer>
<AddThemeDemo />
</HeroContainer>
```tsx hero template=AddTheme
```
### updateTheme
<HeroContainer>
<UpdateThemeDemo />
</HeroContainer>
```tsx hero template=UpdateTheme
```
### replaceTheme
<HeroContainer>
<ReplaceThemeDemo />
</HeroContainer>
```tsx hero template=ReplaceTheme
```
### Notes
- Dynamic themes only work on the client side and will be ignored on the server
side.
- The difference between `updateTheme` and `replaceTheme` is that `replaceTheme`
will replace the entire theme, while `updateTheme` will only update the values
that are passed in.
### Advanced Optimization
You can configure Tamagui to not send any themes JS to the client side, so long
as your are serving the resulting css file from the `getCSS` call on initial
load of your app (SSR).
To enable this you need to have your bundler tree shake away the themes object
you'd typically pass to `createTamagui` for the client bundle. Note this is a
somewhat advanced optimization and not necessary to do right away.
```tsx
import { themes as themesIn } from './your-themes-file'
// We leave this value empty for production client side bundles to save on bundle size.
// The `@tamagui/next-plugin` sets TAMAGUI_IS_SERVER automatically.
// If you pass an empty themes object Tamagui will try to hydrate by scanning CSS in browser environments.
// It typically takes low single-digit ms to scan and can save significantly on JS size.
const themes =
process.env.TAMAGUI_IS_SERVER || process.env.NODE_ENV !== 'production'
? themesIn
: ({} as typeof themesIn)
export const config = createTamagui({
themes,
// ...
})
```
## intro/tokens
---
title: Tokens
description: Understanding the default Tamagui token scale
---
<Notice theme="blue">
This only documents the default `@tamagui/config` tokens that power this site, you are
free to create your own token system with more refined logical underpinning.
</Notice>
### Understanding the size scale
Most design systems use exponential scaling (2, 4, 8, 16, etc.). A lot of people ask why Tamagui doesn't.
Tamagui adopts a hybrid of more gradual increases below 10, before going exponential after 10. This hybrid approach is motivated by allowing designs to have a much more usable range of components.
#### Aligning all the keys
We align every token group and font group to use the same 1-16 keys so that a size 2 Button will look good with a size 2 spacing, a 2 Icon, and size 2 Text inside of it.
#### Quantum scale ($0.25, $0.5, $0.75)
The natural scale starts at 1, but Tamagui has three exponentially decreasing tokens below 1 for all token groups, with 0.25 often rounding to 1.
#### Natural scale ($1-$10)
For sizes 1 to 10, Tamagui increases more gradually. This yields a nice variety of sizes, with $2 being a great tiny UI, $4 is "average", $5 and $6 give you two very usable emphasized sizes, and 7 and above are great for more occasional CTA/hero type things.
#### Super-natural scale
Tamagui switches to an exponential scale from sizes 11 to 16 so you still get some nice chunky sizes.
<TokensDemo />
## intro/why-a-compiler
---
title: The Frontend Trilemma
description: Why Tamagui uses an optimizing compiler
---
<IntroParagraph>
If you're developing a cross-platform app, you've committed to the **frontend
development trilemma**, a choose-two-of-three best shown as a diagram:
</IntroParagraph>
<YStack w="100%" mih={500} als="center" $gtMd={{ my: '$4' }}>
<Image
title="The Frontend Trilemma: Choose two of: A native-feeling app, Deploying to native and web, and share code between the two apps."
src="/trilemma.svg"
size="hero"
height={270 * 2}
width={320 * 2}
resizeMode="contain"
/>
</YStack>
React Native recommends writing things twice generally for the best UX as is
made clear in the `<title>` of the homepage:
"[learn once, write anywhere](https://reactnative.dev/)". This is opposed to the
sort of holy grail mantra of "write once, runs everywhere". Doing this results
in native-feeling and performing apps, while still saving quite a bit of
development time thanks to sharing everything besides your views: utils, state
and data management, hooks, etc.
The center of this above Venn diagram would be a sort of platonic ideal:
<strong>"write once, runs _well_ everywhere"</strong>. We're pretty far from it as of now,
but it's not _technically_ impossible. We can imagine a few ways to get closer:
- A sort of Rails-for-React - unified routing, patterns, code gen, "all the
batteries".
- A UI kit that adapts to each platform's primitives confidently with flexible
APIs.
- A way to author styles that output to platform primitives without overhead:
eg, CSS with media queries, pseudos, and variables on the web.
This document goes over how we can achieve the last one. The first one is doable
(and Expo is [working on as much](https://expo.github.io/router/docs/)), and the
second one [Tamagui UI](/docs/components/stacks) is working towards slowly.
**The idea is to make another "bump" towards properly native-experience apps
with shared code, much like how React Native Web made one:**
<YStack w="100%" mih={220} my="$4" als="center" $gtMd={{ scale: 1.5, my: 100 }}>
<Image
title="How possible it is to share more code: a lot more with React Native, and again with React Native Web"
src="/code-sharing2.svg"
size="hero"
height={220}
width={380}
resizeMode="contain"
/>
</YStack>
This can be done especially on the web by reducing JS bundle size by a large %
and greatly increasing render performance with reduced tree depth, logic,
objects, and hooks. The Tamagui Compiler does this using partial evaluation,
static extraction and hoisting, code elimination, and tree-flattening.
You can
[skip to the technical details without the backstory](#how-tamagui-helps) from
here if you'd like.
<Table heading="Choose your journey">
<TableCol>
<TableCell head></TableCell>
<TableCell>1</TableCell>
<TableCell>2</TableCell>
<TableCell>3</TableCell>
</TableCol>
<TableCol>
<TableCell head>Strategy</TableCell>
<TableCell>Universal</TableCell>
<TableCell>Lean</TableCell>
<TableCell>Big-Budget</TableCell>
</TableCol>
<TableCol>
<TableCell head>Native + Web</TableCell>
<TableCell>✅</TableCell>
<TableCell>❌</TableCell>
<TableCell>✅</TableCell>
</TableCol>
<TableCol>
<TableCell head>Code Sharing</TableCell>
<TableCell>\> 70%</TableCell>
<TableCell>-</TableCell>
<TableCell>\< 30%</TableCell>
</TableCol>
<TableCol>
<TableCell head>Feels native</TableCell>
<TableCell>❌</TableCell>
<TableCell>✅</TableCell>
<TableCell>✅</TableCell>
</TableCol>
<TableCol>
<TableCell head>Ship Fast</TableCell>
<TableCell>✅</TableCell>
<TableCell>✅</TableCell>
<TableCell>❌</TableCell>
</TableCol>
</Table>
Universal apps (those you "write once" that "run everywhere") make sense today
for many cases: side-projecting, SEO-insensitive or enterprise-only apps, people
who want to ship fast, experiment more, are pre-product-market fit, or generally
have apps with simpler UI. Twitter and Tinder are two larger examples of this.
But today, at best, we use hooks for media queries and themes, which basically
touch every component. This causes whole-tree re-renders and more expensive
main-thread time in critical areas on the web. Combine that with the CSS-in-JS
approach of React Native Web greatly increasing bundle size, and even
medium-complexity pages will drop from 100% Lighthouse to half or worse (our
homepage, a good complex example due to showing off many features that are
well-optimized, goes from 95 or so down to 80ish with the compiler off).
With all your media queries, interactive styles, themes, animations, and dynamic
styles in JS, it's hard to make ambitious apps that don't feel janky.
### How Tamagui Helps
`@tamagui/static` is an optimizing compiler for React Native with four main
features:
<YStack py="$4">
<Features
size="$5"
items={[
`Extracts all types of styling syntax into atomic CSS.`,
`Removes a high % of inline styles with partial evaluation and hoisting.`,
`Reduces tree depth, flattening expensive styled components into div or View.`,
`Evaluates useMedia and useTheme hooks, turning logical expressions into media queries and CSS variables.`,
]}
/>
</YStack>
The output is smaller bundles, better runtime performance, and many more native
primitives used on the web.
Here's what it does, in code:
<TamaguiExamplesCode />
See [more examples on the homepage](/).
Notice that the compiler turned the `Text` into a `p`, and the `YStack` into a
`div` (on native, this would be `Text` and `View`). This is known as
tree-flattening, and for both web and native it yields very nice improvements to
render performance.
This is a typical performance improvement, where much of the gains come from
flattening:
<YStack my="$2">
<BenchmarkChartWeb />
</YStack>
Across a few apps, we've seen 30-50% of components typically flatten, with a
higher percent achievable just by being aware of how the flattening optimizes
(adding the `// debug` comment to the top of the file will show a fuller
output).
Meanwhile, on Native, because we can't optimize to anything beyond vanilla React
Native code, the gains are less. Still, the results are impressive given you now
have performance within 5% of hand-optimizing React Native code, except you get
a whole suite of features for free.
<YStack my="$2">
<BenchmarkChartNative />
</YStack>
You can see [more Benchmarks with explanations here](/docs/intro/benchmarks).
The compiler itself deserves more detail, which we'll expand on in the blog. For
now, this serves as a decent introduction.
Compilers can dramatically improve code sharing without the typical sacrifice of
performance. They don't solve every problem of universal apps, but by making
responsive styling, themes and interactive styles all perform at native levels,
they unlock sharing a much larger percentage of the components located in the
middle to bottom of the render tree in apps.
It gives us a new choice, "Universal + Compiler" that lets us ship fast while
still feeling native:
<Table my="$6">
<TableCol>
<TableCell head></TableCell>
<TableCell>4</TableCell>
</TableCol>
<TableCol>
<TableCell head>Strategy</TableCell>
<TableCell>Universal + Compiler</TableCell>
</TableCol>
<TableCol>
<TableCell head>Native + Web</TableCell>
<TableCell>✅</TableCell>
</TableCol>
<TableCol>
<TableCell head>Code Sharing</TableCell>
<TableCell>~60-90%</TableCell>
</TableCol>
<TableCol>
<TableCell head>Feels native</TableCell>
<TableCell>✅</TableCell>
</TableCol>
<TableCol>
<TableCell head>Ship Fast</TableCell>
<TableCell>✅</TableCell>
</TableCol>
</Table>
Tamagui makes React faster, but mostly makes _making_ React faster.
Give Tamagui a try with `npm create tamagui@latest`.