Skip to content
ssntl/ui

Toast

A floating notification you can swipe away: it follows the finger and only leaves on release.

    Installation

    $ pnpm dlx shadcn@latest add @ssntl/toast

    Usage

    tsx
    import { Toast, ToastAction, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
    
    <ToastProvider swipeDirection="right">
      <Toast open={open} onOpenChange={setOpen}>
        <ToastTitle>Order archived</ToastTitle>
        <ToastDescription>It moved to Archive with its history.</ToastDescription>
        <ToastAction altText="Open Archive to restore it">Undo</ToastAction>
      </Toast>
      <ToastViewport />
    </ToastProvider>

    Rules

    What this component expects from you, as it ships in components/ui/toast.md next to the code. The full file adds when to use it, keyboard, the UI foundations it applies and every shared rule.

    components/ui/toast.md
    ## Rules for Toast
    
    - **Floating notification.** Elevation-4, above the page and its menus; announced politely, paused while hovered or focused, reachable with F8.
    - **Responsive gesture.** The toast follows the finger 1:1 from the first pixel.
    - **Continuous gesture.** The pointer stays captured even when it leaves the toast.
    - **Cancelable gesture.** It dismisses on release past the threshold; letting go short of it springs back.
    - One action at most, with `altText` for people who can't reach the toast in time.
    - With an action or an error it stays until dismissed (duration={Infinity}); a plain confirmation lasts at least 5 seconds.
    - The close button reads “Dismiss notification” by default; change `closeLabel` in other languages.
    Open the full rules file (opens in a new tab)

    Code

    components/ui/toast.tsx
    "use client"
    
    import * as React from "react"
    import { Toast as ToastPrimitive } from "radix-ui"
    
    import { cn } from "@/lib/utils"
    import { CloseButton } from "@/registry/ssntl/ui/close-button"
    
    // A floating notification (Radix Toast): announced politely, paused while
    // hovered or focused, and reachable with F8. Floating notification:
    // elevation 4, above the page and its menus.
    // Gestures: swipe right to dismiss. The toast follows the finger 1:1 from
    // the first pixel (responsive), keeps the pointer even off the toast
    // (continuous) and only dismisses on release past the threshold; letting go
    // short of it springs back (cancelable).
    const ToastProvider = ToastPrimitive.Provider
    
    function ToastViewport({
      className,
      ...props
    }: React.ComponentProps<typeof ToastPrimitive.Viewport>) {
      return (
        <ToastPrimitive.Viewport
          data-slot="toast-viewport"
          className={cn(
            "fixed inset-x-4 bottom-4 z-50 mx-auto grid max-w-sm gap-2 outline-none",
            className
          )}
          {...props}
        />
      )
    }
    
    function Toast({
      className,
      children,
      closeLabel = "Dismiss notification",
      ...props
    }: React.ComponentProps<typeof ToastPrimitive.Root> & {
      /** The close button's accessible name. */
      closeLabel?: string
    }) {
      return (
        <ToastPrimitive.Root
          data-slot="toast"
          className={cn(
            "relative grid touch-pan-y gap-1 rounded-lg bg-popover p-5 pe-16 text-left text-popover-foreground elevation-4 select-none",
            "data-[state=closed]:pop-out data-[state=open]:pop-in",
            "data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=move]:transition-none",
            "data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[translate] data-[swipe=cancel]:duration-(--duration-menu) data-[swipe=cancel]:ease-out",
            "data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=end]:pop-out",
            className
          )}
          {...props}
        >
          {children}
          <ToastPrimitive.Close asChild>
            <CloseButton label={closeLabel} className="absolute end-2 top-2" />
          </ToastPrimitive.Close>
        </ToastPrimitive.Root>
      )
    }
    
    function ToastTitle({
      className,
      ...props
    }: React.ComponentProps<typeof ToastPrimitive.Title>) {
      return (
        <ToastPrimitive.Title
          data-slot="toast-title"
          className={cn("text-label", className)}
          {...props}
        />
      )
    }
    
    function ToastDescription({
      className,
      ...props
    }: React.ComponentProps<typeof ToastPrimitive.Description>) {
      return (
        <ToastPrimitive.Description
          data-slot="toast-description"
          className={cn(
            "text-caption text-pretty text-muted-foreground",
            className
          )}
          {...props}
        />
      )
    }
    
    /** One follow-up action, such as Undo; altText says how to do it otherwise. */
    function ToastAction({
      className,
      ...props
    }: React.ComponentProps<typeof ToastPrimitive.Action>) {
      return (
        <ToastPrimitive.Action
          data-slot="toast-action"
          className={cn(
            "mt-2 inline-flex h-9 w-fit cursor-pointer items-center rounded-full bg-secondary px-4 text-caption font-medium",
            "transition-[background-color,scale] duration-(--duration-fast) ease-out hover:bg-neutral-200 active:scale-(--es-press) motion-reduce:active:scale-100 dark:hover:bg-neutral-600",
            "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
            className
          )}
          {...props}
        />
      )
    }
    
    export {
      Toast,
      ToastAction,
      ToastDescription,
      ToastProvider,
      ToastTitle,
      ToastViewport,
    }