Toast

Accessible toasts with stacking, gestures, and animations

Toast presents time-sensitive messages with accessible announcements, stacking, gestures, and animated entry or exit.

Features

  • Composable component API

  • Smooth stacking with hover-to-expand

  • Swipe dismissal with spring physics

  • Promise toasts for async operations

  • All Tamagui animation drivers supported

Installation

Toast ships with tamagui: there’s nothing extra to install:

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Toast.tsx (registry item "toast")
// npm dependencies: @tamagui/core @tamagui/stacks @tamagui/text @tamagui/toast
import { Toast, toast } from "../components/tamagui/Toast";

If you’re using the unstyled primitives directly, it’s also published on its own:

yarn add @tamagui/toast

Native OS toasts are opt-in. By default toasts render as in-app Tamagui views on every platform, so adding Toast to your app never pulls in a native dependency. See Native toasts if you want the real iOS/Android system toast.

Quick start

Toast is a composable component API: you mount the parts, and toast() drives them from anywhere in your app.

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// components/tamagui/Toast.tsx (registry item "toast")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui @tamagui/stacks @tamagui/text @tamagui/toast
import { Button } from "../components/tamagui/Button";
import { Toast, toast } from "../components/tamagui/Toast";
function App() {
return <Toast position="bottom-right">
<Toast.Viewport>
<Toast.List />
</Toast.Viewport>
<Button onPress={() => toast('Hello!')}>Show Toast</Button>
</Toast>;
}

Mount this inside your TamaguiProvider, usually next to your app navigation or root layout. toast() is global, so callers don’t need to be inside <Toast>.

<Toast.List /> renders the default styled toast. Pass renderItem to own the markup completely:

<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> )} />

Omit renderItem and Toast.List renders the default styled toast content.

Anatomy

<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 reference

toast()

The imperative toast function:

// 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:

toast.promise(saveData(), {
loading: 'Saving...',
success: 'Saved!',
error: 'Failed to save',
})

Toast (Root)

Props

  • position

    'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'

    Default: 

    'bottom-right'

    Position of the toast viewport.

  • duration

    number

    Default: 

    4000

    Default toast duration in milliseconds.

  • gap

    number

    Default: 

    14

    Gap between toasts when expanded.

  • visibleToasts

    number

    Default: 

    4

    Maximum number of visible toasts.

  • swipeDirection

    'auto' | 'left' | 'right' | 'up' | 'down'

    Default: 

    'auto'

    Swipe direction to dismiss. 'auto' detects based on position.

  • closeButton

    boolean

    Default: 

    false

    Show close button on toasts.

  • native

    boolean

    Default: 

    false

    Use the native OS toast on iOS/Android instead of in-app views. Requires burnt.

  • Toast.Viewport

    The portal container for toasts. Extends View.

    Props

  • offset

    number | { top?: number, right?: number, bottom?: number, left?: number }

    Default: 

    16

    Offset from screen edge.

  • hotkey

    string[]

    Default: 

    ['altKey', 'KeyT']

    Hotkey to focus viewport.

  • label

    string

    Default: 

    'Notifications'

    Aria label for the viewport.

  • Toast.Item

    Individual toast wrapper with stacking and drag support. Extends View.

    Props

  • toast (required)

    ToastT

    The toast data object.

  • index (required)

    number

    Index in the toast list (for stacking).

  • Supports render prop for custom content:

    <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 View.

    Toast.Action

    Action button. Extends View.

    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.

    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 toasts

    Toasts render as in-app Tamagui views on every platform by default. To use the real iOS/Android system toast instead, install Burnt and import its setup module at your app entry:

    yarn add burnt
    import '@tamagui/native/setup-burnt'

    Then opt in on the root:

    <Toast native>{/* ... */}</Toast>

    Burnt is resolved lazily, so apps that don’t set it up never load it and never need the native module.

    Native safe area

    On native, toasts portal to root and automatically respect safe area insets (status bar, Dynamic Island, home indicator) when configured:

    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:

    <Toast.Item toast={t} index={i} backgroundColor="blue-100" borderRadius="6">
    <Toast.Title color="blue-950" fontWeight="bold">
    {t.title}
    </Toast.Title>
    <Toast.Description color="blue-800">
    {t.description}
    </Toast.Description>
    </Toast.Item>

    Migrating from v2

    The v2 imperative API (ToastProvider, ToastViewport, useToastController, useToastState) was removed. Replace the provider tree with the composable parts shown above, and call toast() directly instead of going through a controller hook:

    // v2
    const toast = useToastController()
    toast.show('Saved!', { message: 'Your changes are live' })
    // v3
    toast('Saved!', { description: 'Your changes are live' })

    See the upgrade guide for the full migration.

    Source

    v2-look Toast (v2 composable API): card background/border/radius/padding/shadow/focus outline, styled title/description text, rounded close button, action/cancel buttons, and the styled default toast content wired through Toast.List renderItem, over the unstyled @tamagui/toast behavior (state engine, stacking, drag-to-dismiss, timers). This is the exact source the registry ships — copy it into your app and the examples above import from your copy.

    components/tamagui/Toast.tsx
    // Styled Toast = the unstyled @tamagui/toast behavior + the default skin, // layered here in `tamagui`. Single skin definition; the shadcn registry item is // generated from this file. Because the behavior renders its default toast // content internally, the styled default content lives HERE and is wired through // Toast.List's `renderItem`, so the copy-paste registry item is a complete, // customizable toast (no hidden styling in the behavior package). // // Surfaced at both the `tamagui` root (shadowing the unstyled @tamagui/toast // composable Toast) and the `tamagui/toast` subpath. There is exactly one Toast // API in v3 — the old imperative ToastProvider/useToastController was removed. import { Toast as ToastBehavior, toast, useToastItem, useToasts, type ExternalToast, type ToastItemRenderProps, type ToastListProps, type ToastPosition, type ToastRootProps, type ToastT, } from '@tamagui/toast' import { createRefComponent, styled, type TamaguiElement, withStaticProperties, } from '@tamagui/core' import { SizableText } from '@tamagui/text' import { XStack, YStack } from '@tamagui/stacks' /* ------------------------------------------------------------------------------------------------- * Styled parts — the default skin over the unstyled @tamagui/toast primitives. * -----------------------------------------------------------------------------------------------*/ export const ToastItem = styled(ToastBehavior.Item, { displayName: 'ToastItem', backgroundColor: 'background', borderRadius: '6', paddingHorizontal: '4', paddingVertical: '3', borderWidth: 1, borderColor: 'border-color', shadowColor: 'rgba(0, 0, 0, 0.15)', shadowOffset: { width: 0, height: 4 }, shadowRadius: 12, outlineWidth: 'focus-visible:2px', outlineColor: 'focus-visible:color-8', outlineStyle: 'focus-visible:solid', }) export const ToastTitle = styled(ToastBehavior.Title, { displayName: 'ToastTitle', color: 'color', fontWeight: '600', size: '4', }) export const ToastDescription = styled(ToastBehavior.Description, { displayName: 'ToastDescription', color: 'color-11', size: '2', }) // flat on purpose: the glyph is the whole affordance, so it only brightens on // hover instead of carrying a chip of its own export const ToastClose = styled(ToastBehavior.Close, { displayName: 'ToastClose', backgroundColor: 'transparent', color: 'color-9 hover:color press:color', }) export const ToastAction = styled(ToastBehavior.Action, { displayName: 'ToastAction', borderRadius: '2', paddingHorizontal: '2', backgroundColor: 'color-5 hover:color-6 press:color-7', }) /* ------------------------------------------------------------------------------------------------- * ToastDefaultContent — the default toast body (icon / title / description / * actions / close), styled. Rendered via Toast.List's renderItem below. * -----------------------------------------------------------------------------------------------*/ function ToastDefaultContent({ toast: t }: { toast: ToastT }) { const { closeButton } = useToasts() const { handleClose } = useToastItem() const dismissible = t.dismissible !== false const title = typeof t.title === 'function' ? t.title() : t.title const description = typeof t.description === 'function' ? t.description() : t.description return ( <XStack alignItems="flex-start" gap="3"> <ToastBehavior.Icon /> <YStack flex={1} gap="1"> {title ? <ToastTitle>{title}</ToastTitle> : null} {description ? <ToastDescription>{description}</ToastDescription> : null} {t.action || t.cancel ? ( <XStack gap="2" marginTop="2"> {t.cancel ? ( <ToastAction backgroundColor="transparent" onPress={(e: any) => { t.cancel?.onClick?.(e) handleClose() }} > <SizableText size="2" color="color-11"> {t.cancel.label} </SizableText> </ToastAction> ) : null} {t.action ? ( <ToastAction backgroundColor="color-11 hover:color-10 press:color-9" onPress={(e: any) => { t.action?.onClick?.(e) if (!(e as any).defaultPrevented) { handleClose() } }} > <SizableText size="2" fontWeight="600" color="background"> {t.action.label} </SizableText> </ToastAction> ) : null} </XStack> ) : null} </YStack> {closeButton && dismissible ? <ToastClose /> : null} </XStack> ) } const renderStyledToast = ({ toast: t, index }: ToastItemRenderProps) => ( <ToastItem toast={t} index={index}> <ToastDefaultContent toast={t} /> </ToastItem> ) // Capture the behavior parts up front. `withStaticProperties` assigns onto the // component it is given, so composing directly onto ToastBehavior would rewrite // @tamagui/toast's own Toast.List/.Item for every consumer of the unstyled // package — and ToastList below would end up rendering itself. const BehaviorList = ToastBehavior.List const BehaviorViewport = ToastBehavior.Viewport const BehaviorIcon = ToastBehavior.Icon // Styled list — defaults renderItem to the styled default content so a bare // `<Toast.List />` is styled; a consumer-supplied renderItem still overrides it. function ToastList(props: ToastListProps) { return <BehaviorList renderItem={renderStyledToast} {...props} /> } /* ------------------------------------------------------------------------------------------------- * Toast — the styled composable API (styled parts on the behavior root). * -----------------------------------------------------------------------------------------------*/ // A distinct root, so the styled parts hang off this skin instead of mutating // the shared behavior component (see BehaviorList above). const ToastRoot = createRefComponent<TamaguiElement, ToastRootProps>( function Toast(props, ref) { return <ToastBehavior {...props} ref={ref} /> } ) export const Toast = withStaticProperties(ToastRoot, { Viewport: BehaviorViewport, List: ToastList, Item: ToastItem, Title: ToastTitle, Description: ToastDescription, Close: ToastClose, Action: ToastAction, Icon: BehaviorIcon, }) // Re-export the non-visual surface so `tamagui/toast` is a drop-in for the // styled toast: imperative API, render hooks, and the public types. export { toast, useToastItem, useToasts } export type { ExternalToast, ToastPosition, ToastT }

    Dependencies

    yarn add @tamagui/core @tamagui/stacks @tamagui/text @tamagui/toast

    Expects theme tokens: background, border-color, color, color-3, color-4, color-5, color-6, color-7, color-8, color-10, color-11, color-11. Native: requires a Portal provider at the app root for the toast viewport to mount above content

    Need raw behavior without any skin? tamagui/unstyled re-exports the @tamagui/ui primitives (advanced).