Hanzo GUI

Toast

Use to show feedback to user interactions.

  • Automatically closes
  • Pause closing on hover, focus, window blur and mobile touch
  • Supports closing via swipe gesture
  • Easily animatable with Hanzo GUI's animation drivers
  • Native toasts included for Android, iOS and web (notification API)

Installation

Toast is included by default in @hanzo/gui. If you're using it standalone:

yarn add @hanzogui/toast

Native Toast Setup

For native toasts on iOS/Android (using Burnt by Fernando Rojo), install the native dependency and import the setup module:

yarn add burnt
// App.tsx - before any Hanzo GUI imports
import '@hanzogui/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

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

PropTypeDefaultRequired
labelstringNotification-
durationnumber5000-
swipeDirectionSwipeDirectionright-
swipeThresholdnumber50-
idstringA unique generated ID-
nativeboolean | ToastNativePlatform | ToastNativePlatform[]--
burntOptionsOmit<BurntToastOptions, 'title' | 'message' | 'duration'>--
notificationOptionsNotificationOptions--

ToastViewport

The portal for toasts to be directed to. Should be used inside ToastProvider. Beyond Stack Props, adds:

PropTypeDefaultRequired
hotkeystring[]['F8']-
labelstringNotifications ({hotkey})-
namestring--
multipleToastsboolean--
portalToRootboolean | numberfalse-
unstyledbooleanfalse-

Toast

Contains the Title, Description, Action and Close component. Should be used inside ToastProvider. Extends Stack and adds:

PropTypeDefaultRequired
forceMountboolean--
type'foreground' | 'background'--
durationnumber--
defaultOpenboolean--
openboolean--
onOpenChange(open: boolean): void--
onEscapeKeyDown(): DismissableProps['onEscapeKeyDown']--
onPause(): void--
onResume(): void--
onSwipeStart(event: SwipeEvent): void--
onSwipeMove(event: SwipeEvent): void--
onSwipeCancel(event: SwipeEvent): void--
onSwipeEnd(event: SwipeEvent): void--
viewportNamestringdefault-
unstyledbooleanfalse-

Toast.Title

Should be used inside Toast. Extends SizableText, adding:

PropTypeDefaultRequired
unstyledbooleanfalse-

Toast.Description

Should be used inside Toast. Extends SizableText, adding:

PropTypeDefaultRequired
unstyledbooleanfalse-

Toast.Close

Should be used inside Toast. Extends Stack. You can pass asChild to this component and use a custom <Button> inside.

Toast.Action

Should be used inside Toast. Extends Stack. 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.

PropTypeDefaultRequired
show(title: string, showOptions?: ShowToastOptions): void--
hide(): void--
optionsToastOptions--

useToastState

Used to render out your toast contents. Should be used inside ToastProvider. Doesn't take in anything and returns ToastData.

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:
<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>. For instance, if you want them to appear from top right:

<ToastViewport flexDirection="column-reverse" top={0} right={0} />

Or for bottom center:

<ToastViewport flexDirection="column" bottom={0} left={0} right={0} />

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.

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.

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:

const App = () => {
  return (
    <ToastProvider>
      <ToastViewport /> {/* default viewport */}
      <ToastViewport name="viewport-custom" />
    </ToastProvider>
  )
}

And then, use the viewport's name on the toasts.

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.

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:

const toastState = useToastState()
toastState.myPreset // or toastState.customData.myPreset

To add TypeScript auto-completion for your custom fields, you can use TS module augmentation:

declare module '@hanzogui/toast' {
  interface CustomData {
    myPreset: 'error' | 'success' | 'warning'
  }
}

Without hooks

You can also use toasts without the hooks.

You can't use native toasts this way.

Single Toast

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

To use multiple toasts, you should pass multipleToasts to your ToastViewport. Otherwise there'll be issues when swipe-dismissing or animating toasts.

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

Last updated on

On this page