easeul

Documentation32 components

Animate what your components already know

A dialog knows when it is open. A menu knows when it is closing. That state is already in the DOM as an attribute, and easeful is a CSS layer that animates it. No JavaScript ships to the browser, and the whole API is one attribute you add yourself.

Getting started

Two steps, and the second one is the whole API.

  1. Import the layer once

    Everything ships inside @layer easeful, deliberately low priority, so your own unlayered CSS overrides it without !important.

    css
    /* Once, globally. If Tailwind v4 is present, the layer order
       must be declared before any import. */
    @layer easeful, theme, base, components, utilities;
    @import "easeful";
  2. Add the attribute to anything with open and closed state

    There is no provider, no wrapper component, and nothing to wire up. The second half of the match, the state attribute, is already being written by Radix, Base UI, or the browser.

    tsx
    <Dialog.Content data-motion="scale-fade" />

The preset vocabulary

These 4 names are the entire surface. They are generated from the manifest, so a value that typechecks is a value that exists. The attribute reference lists the two attributes and every token beside them.

PresetWhat it doesComposes with
fadeFades opacity only. Overlays, backdrops, and anything where movement would distract.slide-up
scale-fadeFades in while scaling up slightly. Default choice for dialogs, dropdowns, and popovers.slide-up
slide-upFades in while sliding up from slightly below. Menus and content anchored above their trigger.fade, scale-fade
collapseAnimates a panel between zero and its natural height. The preset for accordions and collapsibles, where fading alone leaves the layout jumping. Takes a single element child.fade

Compose two of them with a space:

tsx
<Dialog.Content data-motion="fade slide-up" />

Why the exit is the hard part

Entering is easy: the element mounts and you animate it. Leaving is not, because the element has to survive long enough to be animated, and each host decides that differently.

Radix keeps a closing node mounted and waits for animationend. A transition-only implementation shows no exit on Radix at all, because a transition never fires that event. Base UI writes starting and ending style attributes instead and prefers transitions, because a transition can be cancelled part way through rather than snapping back. Native elements need display and overlay in the transition list under allow-discrete, or they leave the top layer before the exit can play.

easeful ships all of it and gates each path on the attributes the host already writes. You never pick one. It is worth knowing only when something looks wrong, which is why each component page names the path it took.

What it deliberately does not do

Motion, and nothing else. Focus trapping, positioning, dismissal, and ARIA stay with the host component, which already does them properly. If a preset does not exist for what you need, add it to the manifest and regenerate rather than writing one-off keyframes.

Every component, with and without

Every page mounts the same component twice, once with the attribute and once without. Nothing else differs between the two copies, so anything you see is the library.