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.
Toast.ProviderbuttonToast.Rootdata-motion="slide-up"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>
)
}motion prop is the only difference between them.