easeful
v0.1.3
Published
Enter and exit animations for components that already have open and closed state. One attribute, and no JavaScript in the browser.
Downloads
573
Maintainers
Readme
easeful
Enter and exit animations for components that already have open and closed state. One attribute, and no JavaScript in the browser.
<Dialog.Content data-motion="scale-fade" />That is the whole API. The library matches on the state attributes the host
component already writes, so there is nothing to wire up and no wrapper to add.
It works with Radix UI, Base UI, the native <dialog> and [popover] elements,
and any element you toggle hidden on.
Install
npm i easeful
npx easeful initinit does the two setup steps below plus the lint rule, printing the diff and asking
before it writes. npx easeful doctor checks them and exits non-zero, so it can run in
CI. Both are idempotent, and both decline rather than guess when a project is shaped in
a way they cannot read.
Any React app: Next, Vite, React Router, Astro with React islands. The stylesheet is plain CSS and works anywhere. The types are React only.
Import the layer once, globally. If Tailwind v4 is present, the @layer line
has to come before any @import, because a layer's position is fixed where it
is first named. Get that wrong and the library fails silently: the attribute is
there, the CSS is loaded, and nothing animates.
@layer easeful, theme, base, components, utilities;
@import "easeful";
@import "tailwindcss";Then activate the types, for autocomplete on the attribute and its values. They do
not enforce anything: TypeScript leaves JSX attributes with hyphens in their names
unchecked, so data-motion="nonsense" compiles. Annotate a value to get a real
error. React only, and the stylesheet works with or without this step.
// easeful-env.d.ts, or any declaration file your tsconfig includes
/// <reference types="easeful/types" />The types array in tsconfig.json works too. It also switches off automatic
inclusion of every other @types package, so node and anything else ambient has
to be listed beside it, which is why the reference file is the instruction here.
For enforcement rather than autocomplete, the package also ships an ESLint rule.
It reads the same motion.json the CSS is generated from and fails on an unknown
preset, a repeat, more than two, a pair that does not compose, or a bad
data-motion-state. ESLint 9 or newer, flat config, two lines:
// eslint.config.mjs
import easeful from "easeful/eslint"
export default [...easeful.configs.recommended]It reads literal values and conditional branches. data-motion={value} is source
a linter cannot resolve, so it is left alone.
Coding with an agent
AGENTS.md ships in this package and is generated from the same manifest as the CSS, so
the vocabulary an agent reads is the vocabulary the stylesheet matches. For Claude Code,
the same body is a skill:
npx easeful skill # .claude/skills/easeful/SKILL.md, commit it
npx easeful skill --global # or once, for every project on the machineIt is also at easeful.sanyam.sh/skill.md.
Use
Add the attribute to anything that already signals open and closed state. The second half of the match is written for you, by Radix, by Base UI, or by the browser.
<Dialog.Overlay data-motion="fade" />
<Dialog.Content data-motion="scale-fade" />Four presets: fade, scale-fade, slide-up, collapse. Compose two with a
space: data-motion="fade slide-up". One escape hatch, data-motion-state, for
a host that publishes no open and closed pair of its own.
AGENTS.md in this package is the full surface, generated from the same manifest
the CSS, the types and the lint rule are, so an attribute the types offer is an
attribute a rule matches. Since the compiler will not police the value, that file
and the linter are the authority. It is written for a coding agent and it is the
shortest complete reference for a person too.
Two rules worth knowing
A collapse panel takes a single element child, and that child is what gets
clipped, so padding belongs on it rather than on the panel. The preset animates a
grid row rather than a height, because height: auto is not interpolatable and
the property that makes it so is Chromium-only.
Do not put Framer Motion on the same element. Both want the transform. easeful
writes the translate and scale longhands specifically so your own transform
survives, but two libraries animating one element take turns overwriting each
other.
Overriding it
Everything ships inside @layer easeful at deliberately low priority, so your own
unlayered CSS wins with no !important. Retiming every preset at once is one
declaration:
:root {
--motion-duration: 240ms;
--motion-ease: cubic-bezier(0.2, 0.9, 0.1, 1);
}Docs
Every component, shown with the attribute and without it, at
easeful.sanyam.sh. Every page is also served as markdown at its
own path plus .md, indexed at /llms.txt.
MIT licensed.
