Toast

Toast notifications for React Native with an imperative API and swipe-to-dismiss gestures.

Toast is the React Native implementation shipped by the native-toast registry item. The component adapts the registry's web-facing composition to native primitives and touch-based interaction.

The native Toast system provides lightweight, transient notifications without requiring a component instance at every call site. Mount ToasterProvider once near the root of the application, then use the exported toast API to create notifications.

Toasts animate into view, automatically dismiss after their configured duration, and can be swiped horizontally to dismiss.

Installation

CLI

npx shadcn add https://ui.tiesen.id.vn/r/native-toast.json

Manual

Copy and paste the following code into your project.

import type { LucidePropsWithClassName } from 'lucide-uniwind'import type { LayoutChangeEvent } from 'react-native'import {  CheckCircleIcon,  CircleAlertIcon,  InfoIcon,  TriangleAlertIcon,} from 'lucide-uniwind'import * as React from 'react'import { Animated, Easing, PanResponder, Text, View } from 'react-native'import { cn } from '@/lib/utils'type ToastType = 'default' | 'success' | 'info' | 'error' | 'warning'interface ToastOptions {  icon?: (props: LucidePropsWithClassName) => React.ReactNode  duration?: number}interface ToastItem {  id: string  type: ToastType  title: string  description?: string  options?: ToastOptions}type ToastListener = (toast: ToastItem) => voidconst listeners = new Set<ToastListener>()const emitToast = (toast: Omit<ToastItem, 'id'>) => {  const item: ToastItem = {    ...toast,    id: Math.random().toString(36).slice(2, 9),  }  for (const listener of listeners) listener(item)}const toast = {  show: (title: string, description?: string, options?: ToastOptions) =>    emitToast({ type: 'default', title, description, options }),  success: (title: string, description?: string, options?: ToastOptions) =>    emitToast({ type: 'success', title, description, options }),  error: (title: string, description?: string, options?: ToastOptions) =>    emitToast({ type: 'error', title, description, options }),  info: (title: string, description?: string, options?: ToastOptions) =>    emitToast({ type: 'info', title, description, options }),  warning: (title: string, description?: string, options?: ToastOptions) =>    emitToast({ type: 'warning', title, description, options }),}interface ToasterContextValue {  toast: typeof toast  dismiss: (id: string) => void}const ToasterContext = React.createContext<ToasterContextValue | null>(null)const useToast = () => {  const ctx = React.use(ToasterContext)  if (!ctx) throw new Error('useToast must be used within a ToasterProvider')  return ctx}const toastVariants = {  default: {    icon: null,    root: 'bg-popover border-border',    title: 'text-popover-foreground',    description: 'text-popover-foreground/80',  },  success: {    icon: CheckCircleIcon,    root: 'bg-[color-mix(in_oklab,var(--success)_10%,var(--popover))] border-success/30',    title: 'text-success',    description: 'text-success/80',  },  error: {    icon: CircleAlertIcon,    root: 'bg-[color-mix(in_oklab,var(--destructive)_10%,var(--popover))] border-destructive/30',    title: 'text-destructive',    description: 'text-destructive/80',  },  info: {    icon: InfoIcon,    root: 'bg-[color-mix(in_oklab,var(--info)_10%,var(--popover))] border-info/30',    title: 'text-info',    description: 'text-info/80',  },  warning: {    icon: TriangleAlertIcon,    root: 'bg-[color-mix(in_oklab,var(--warning)_10%,var(--popover))] border-warning/30',    title: 'text-warning',    description: 'text-warning/80',  },}function ToastCard({  item,  position,  onDismiss,}: Readonly<{  item: ToastItem  position: 'top' | 'bottom'  onDismiss: (id: string) => void}>) {  const initialTranslateY = position === 'top' ? -50 : 50  const exitTranslateY = position === 'top' ? -20 : 20  const translateY = React.useMemo(    () => new Animated.Value(initialTranslateY),    [initialTranslateY]  )  const translateX = React.useMemo(() => new Animated.Value(0), [])  const opacity = React.useMemo(() => new Animated.Value(0), [])  const toastWidthRef = React.useRef<number>(0)  const variant = toastVariants[item.type] ?? toastVariants.default  const Icon = item.options?.icon ?? variant.icon  const dismiss = React.useCallback(    (direction: 'up/down' | 'left' | 'right' = 'up/down') => {      let toValueX = 0      let toValueY = 0      if (direction === 'left') toValueX = -400      else if (direction === 'right') toValueX = 400      else toValueY = exitTranslateY      Animated.parallel([        Animated.timing(translateX, {          toValue: toValueX,          duration: 200,          useNativeDriver: true,        }),        Animated.timing(translateY, {          toValue: toValueY,          duration: 200,          useNativeDriver: true,        }),        Animated.timing(opacity, {          toValue: 0,          duration: 200,          useNativeDriver: true,        }),      ]).start(() => onDismiss(item.id))    },    [exitTranslateY, item.id, onDismiss, opacity, translateX, translateY]  )  const panResponder = React.useMemo(    () =>      // oxlint-disable-next-line react/refs      PanResponder.create({        onStartShouldSetPanResponder: () => false,        onMoveShouldSetPanResponder: (_, gestureState) =>          Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&          Math.abs(gestureState.dx) > 5,        onPanResponderMove: (_, gestureState) =>          translateX.setValue(gestureState.dx),        onPanResponderRelease: (_, gestureState) => {          const width = toastWidthRef.current          const threshold = width ? width * 0.4 : 120          if (gestureState.dx > threshold) dismiss('right')          else if (gestureState.dx < -threshold) dismiss('left')          else            Animated.spring(translateX, {              toValue: 0,              useNativeDriver: true,              bounciness: 8,            }).start()        },      }),    [dismiss, translateX]  )  const handleLayout = React.useCallback((event: LayoutChangeEvent) => {    toastWidthRef.current = event.nativeEvent.layout.width  }, [])  React.useEffect(() => {    Animated.parallel([      Animated.timing(translateY, {        toValue: 0,        duration: 250,        easing: Easing.out(Easing.ease),        useNativeDriver: true,      }),      Animated.timing(opacity, {        toValue: 1,        duration: 250,        useNativeDriver: true,      }),    ]).start()    const timer = setTimeout(() => dismiss(), item.options?.duration ?? 3000)    return () => clearTimeout(timer)  }, [dismiss, item.options?.duration, opacity, translateY])  const Content = (    <>      <Text className={cn('text-base font-medium', variant.title)}>        {item.title}      </Text>      {!!item.description && (        <Text className={cn('text-sm font-medium', variant.description)}>          {item.description}        </Text>      )}    </>  )  return (    <Animated.View      onLayout={handleLayout}      {...panResponder.panHandlers}      style={{ transform: [{ translateY }, { translateX }], opacity }}      className={cn(        'my-1.5 rounded-xl border p-3 shadow-md',        Icon ? 'flex-row gap-2' : 'flex-col gap-1',        variant.root      )}    >      {Icon && <Icon className={cn('mt-1 size-4', variant.title)} />}      {Icon ? <View className='flex-col gap-1'>{Content}</View> : Content}    </Animated.View>  )}interface ToasterProviderProps {  children: React.ReactNode  position?: 'top' | 'bottom'}function ToasterProvider({ children, position = 'top' }: ToasterProviderProps) {  const [toasts, setToasts] = React.useState<ToastItem[]>([])  const dismiss = React.useCallback((id: string) => {    setToasts((prev) => prev.filter((t) => t.id !== id))  }, [])  React.useEffect(() => {    const handleNewToast = (newToast: ToastItem) => {      setToasts((prev) => [newToast, ...prev])    }    listeners.add(handleNewToast)    return () => {      listeners.delete(handleNewToast)    }  }, [])  const contextValue = React.useMemo(() => ({ toast, dismiss }), [dismiss])  return (    <ToasterContext value={contextValue}>      {children}      <View        pointerEvents='box-none'        className={cn(          'absolute right-4 left-4 z-9999',          position === 'top' ? 'top-4' : 'bottom-4'        )}      >        {toasts.map((item) => (          <ToastCard            key={item.id}            item={item}            onDismiss={dismiss}            position={position}          />        ))}      </View>    </ToasterContext>  )}export { ToasterProvider, useToast, toast }

Features

  • Imperative toast.show, success, error, info, and warning methods.
  • Top or bottom placement.
  • Automatic dismissal with a configurable duration per toast.
  • Horizontal swipe-to-dismiss gesture.
  • Built-in icons for semantic toast types.
  • Custom icon support.

Usage

import {
  ToasterProvider,
  toast,
} from '@/components/native/toast'

export function App() {
  return (
    <ToasterProvider>
      <YourApp />
    </ToasterProvider>
  )
}

function SaveButton() {
  return (
    <Button
      onPress={() => {
        toast.success(
          'Saved successfully',
          'Your changes have been saved.'
        )
      }}
    >
      Save
    </Button>
  )
}

Toast types

The exported API provides five semantic methods:

toast.show('Something happened')
toast.success('Saved successfully')
toast.info('New information')
toast.warning('Check this before continuing')
toast.error('Something went wrong')

Duration

Pass duration in the options object. The default duration is 3000 milliseconds.

toast.success(
  'Uploaded',
  'The file is ready.',
  { duration: 5000 }
)

Custom icons

You can replace the default semantic icon with a custom Lucide icon renderer.

toast.info(
  'Sync started',
  undefined,
  {
    icon: (props) => <CloudIcon {...props} />,
  }
)

Position

Set the provider position to top or bottom.

<ToasterProvider position='bottom'>
  <App />
</ToasterProvider>

Swipe to dismiss

Toasts respond to horizontal gestures. A swipe of roughly 40% of the toast width dismisses the toast; shorter swipes spring back into place.

API Reference

ToasterProvider

PropTypeDefault
childrenReact.ReactNode—
position"top" | "bottom""top"

Mount one ToasterProvider near the application root.

toast.show

toast.show(
  title: string,
  description?: string,
  options?: ToastOptions
)

Displays a default toast.

toast.success

toast.success(
  title: string,
  description?: string,
  options?: ToastOptions
)

Displays a success toast.

toast.info

toast.info(
  title: string,
  description?: string,
  options?: ToastOptions
)

Displays an informational toast.

toast.warning

toast.warning(
  title: string,
  description?: string,
  options?: ToastOptions
)

Displays a warning toast.

toast.error

toast.error(
  title: string,
  description?: string,
  options?: ToastOptions
)

Displays an error toast.

ToastOptions

PropertyTypeDefault
icon(props: LucidePropsWithClassName) => React.ReactNodesemantic icon
durationnumber3000

useToast

const { toast, dismiss } = useToast()

Returns the toast API and a dismiss function. useToast must be called inside ToasterProvider.

ToastType

type ToastType =
  | 'default'
  | 'success'
  | 'info'
  | 'error'
  | 'warning'

On this page