easeul

Radix UIKeyframes

Toast

Open and close are covered. Swipe-follow is not, because the direction depends on where the app anchors its viewport.

Every value the manifest allows on this host, single presets and the pairs that compose. The first is what this component ships with. Both stages are the same component, so anything that changes between them is the attribute.

withoutno attribute
    with easefuldata-motion="slide-up"
      host writes

      data-state="open" | "closed"

      easeful matches

      [data-motion~="slide-up"][data-state="closed"]

      what runs

      animation: motion-exit 150ms

      you write

      <Toast.Root data-motion="slide-up" />

      Where the attribute goes

      The parts this demo composes, and the value each one carries. Everything inside an animated part is that part's content, so the tree stops there.

      1. Toast.Provider
      2. button
      3. Toast.Rootdata-motion="slide-up"
      4. Toast.Viewport

      Source

      "use client"
      
      import { useState } from "react"
      import { Toast } from "radix-ui"
      import type { DemoProps } from "@/components/demos/props"
      
      export function RadixToast({ motion }: DemoProps) {
        const [sent, setSent] = useState<number[]>([])
      
        return (
          <Toast.Provider swipeDirection="right">
            <button
              type="button"
              className="btn"
              onClick={() => setSent((list) => [...list, (list.at(-1) ?? 0) + 1])}
            >
              Send a toast
            </button>
            {sent.map((n) => (
              <Toast.Root
                key={n}
                className="panel"
                data-motion={motion}
                duration={3500}
                /* Dropping the toast from state the instant it closes unmounts it
                 * before the exit can run, which is why this had no exit animation.
                 * Radix keeps the node alive to animate it, so the cleanup has to
                 * wait until after that. */
                onOpenChange={(open) => {
                  if (!open) {
                    window.setTimeout(() => setSent((list) => list.filter((x) => x !== n)), 400)
                  }
                }}
              >
                <Toast.Title className="panel__title">Deployed</Toast.Title>
                <Toast.Description className="panel__body">Build {n} is live.</Toast.Description>
              </Toast.Root>
            ))}
            <Toast.Viewport className="toast-viewport" />
          </Toast.Provider>
        )
      }
      The whole file, read from disk at build time. Both copies above render from it, and the motion prop is the only difference between them.