Motion for the components you already have
181 effects across 13 categories. Each one is a CSS class you put on a WunderUI component — no provider, no hook, nothing to initialise, and nothing added to your JavaScript bundle.
Install
One package, one import, one CSS file. It ships no JavaScript.
npm install @wunderui/motion/* your Tailwind entry point, after the WunderUI tokens */
@import "tailwindcss";
@import "@wunderui/react/styles.css";
@import "@wunderui/motion";Use it
Two classes, and both do a job. wm is the marker the trigger and reduced-motion rules hook into; wm-<effect> is the effect.
import { Button } from "@wunderui/react"
<Button className="wm wm-halo-ring">Pro plan</Button>An effect class on its own still animates, but it will not obey the trigger attribute and it will not stop for prefers-reduced-motion. Use both.
Triggers
Without an attribute, an effect does what it is for: an entrance or a chart plays once, a rim or a loader loops. One attribute changes that.
{/* entrances, data and feedback play once and hold their end state */}
<Card className="wm wm-dialog-lift">…</Card>
{/* ambient effects, rims and loaders loop */}
<Badge className="wm wm-sweep-rim">Live</Badge>
{/* opt into the loop for an effect that plays once */}
<Card className="wm wm-bar-rise" data-wm="loop">…</Card>
{/* runs only while hovered or keyboard-focused */}
<Button className="wm wm-comet-rim" data-wm="hover">Hover me</Button>
{/* an effect on a part runs while its control is hovered */}
<Button data-wm-trigger>Download <Download className="wm wm-arrow-fall" data-wm="hover" /></Button>
{/* one pass of any effect, then it holds its end state */}
<Card className="wm wm-prism-card" data-wm="once">…</Card>Played once, an effect ends on its visible end state — the dialog stays open, the bars stay up. A state effect (a hover lift, a pressed button, a tab underline) is a transition between states you set, so it follows your component instead of a clock. data-wm-loop on a wrapper makes everything inside loop, which is what a style guide or a page like this one wants.
hover removes the animation rather than pausing it. A paused animation rests wherever it happened to be, which reads as a rendering bug; removing it puts the element back in its own base state.
Retuning an effect
Every effect reads the same custom properties, so you change speed and colour without forking the CSS.
| Property | Default | What it does |
|---|---|---|
| --wm-duration | per effect | length of one looping pass |
| --wm-in | a duration token | how long the entrance takes when it plays once |
| --wm-accent | #12AFF0 | leading colour |
| --wm-accent-2 | #555CF3 | second colour |
| --wm-accent-3 | #FE6BBA | third colour |
| --wm-surface | var(--card) | the opaque fill behind a rim effect |
| --wm-ring | 2px | rim thickness |
<Card
className="wm wm-prism-card"
style={{ "--wm-duration": "8s", "--wm-accent": "var(--brand-primary)" }}
>
…
</Card>What it costs you
The badge on every card is the dependency cost, stated on the card and not after the install.
What else you need
Nothing beyond the stylesheet for most of the set. These are the exceptions, and they are the whole list.
- Tailwind CSS v4 is not required. The effects are plain CSS and work in any stylesheet. The import line above uses the Tailwind entry point only because that is where the WunderUI tokens already are.
- A handful of effects need a little JavaScript. The ones marked CSS + a few lines read pointer coordinates into two custom properties, or step a glyph timer. The code is on the effect's own page — it is not hidden in a package.
- Motion for React is an optional peer. Nothing in this package imports it. It earns its place when an animation has to be interruptible mid-flight, follow a gesture, or survive a layout change — a CSS keyframe cannot do any of those. Everything catalogued here can be done without it.
- Reduced motion is handled for you. Under
prefers-reduced-motion: reduceeverywmeffect stops, and every effect’s base state is its visible end state: the dialog is open, the chart is drawn, the tick is ticked, a rim keeps its gradient and stops turning. Nothing is hidden and no state is lost. Loaders keep a slow opacity pulse instead of freezing, because a frozen loader reads as a hang.
Browser support
Where an effect needs something recent, the effect's page names the feature and the versions. The four the Borders set is built on are all Baseline.
| Feature | Status |
|---|---|
| @property | Baseline — Chrome 85, Firefox 128, Safari 16.4 |
| mask-composite | Baseline |
| offset-path | Baseline |
| conic-gradient | Baseline |
| @starting-style, transition-behavior: allow-discrete | Baseline since August 2024 — Chrome 117, Firefox 129, Safari 17.4 |
| View Transitions (same document) | Baseline since October 2025 — Firefox 144 was the last to ship it |
| animation-timeline: scroll() / view() | Not Baseline — Chrome 115, Safari 26; open in Firefox |
| interpolate-size: allow-keywords | Chromium 129 and up only |
Support checked September 2026.