easeul

TypeScript

Add easeful/types to your tsconfig so data-motion is checked against the preset vocabulary instead of accepting any string.

The types give an editor the attribute and its 4 values. They do not give you an error when you ignore them, and it is worth knowing exactly why before trusting them.

TypeScript does not check a JSX attribute whose name is not a valid identifier, and every data-* attribute qualifies. So data-motion="nonsense" compiles. So does data-motion={12345}. Rename it to dataMotion and the compiler objects immediately, which is the shape of the rule: the hyphen is what exempts it.

React only

The augmentation is declare module "react", so this whole page applies to React projects. Everywhere else the stylesheet works exactly the same and this step is skipped: adding easeful/types to a project without React fails to compile rather than doing nothing.

Activate

One line in a declaration file your tsconfig.json already includes.

ts
/// <reference types="easeful/types" />

The types array in tsconfig.json works too, and costs something: declaring it switches off automatic inclusion of every other @types package, so node and anything else ambient has to be listed beside it. This site's own app hit that and now uses the reference file.

What you get

The preset union, plus space-separated composition typed as a pair so "fade slide-up" checks and "fade nonsense" does not. Both attributes come from the manifest's own attribute list, which the reference prints in full.

ts
type MotionPreset = "fade" | "scale-fade" | "slide-up" | "collapse";

type MotionValue = MotionPreset | `${MotionPreset} ${MotionPreset}`;

What you actually get

Autocomplete, on the attribute name and on its values, in any editor running the TypeScript language service. That is worth having and it is the whole of it.

For a real error, annotate the value rather than writing it inline:

tsx
const value: MotionValue = "nonsense" // Type '"nonsense"' is not assignable

Enforce it

A lint rule ships in the same package, and this is where enforcement actually lives. Flat config, two lines:

js
// eslint.config.mjs
import easeful from "easeful/eslint"

export default [...easeful.configs.recommended]

It reads the same motion.json the CSS and the union above are generated from, so it cannot disagree with the stylesheet about what a preset is. On a literal value it fails on five things. An unknown name, with the nearest preset offered as an editor suggestion. The same preset twice. More than two of them. A pair the manifest does not compose, which is the one thing a union could never express: a type can list four strings, it cannot know that collapse and scale-fade want the same property. And a data-motion-state that is neither open nor closed.

One ceiling, and it is inherent to linting rather than a gap to close later. A rule sees source, so data-motion={value} cannot be resolved and is left alone. A conditional or an && with literal branches is read, which covers how a preset is usually written when it applies in only one state. This site runs the rule over its own app, where the demos take the preset as a prop and the chrome writes it inline.

Why this matters more for agents than for you

You would notice a dialog that did not animate. A coding agent often would not, and will confidently invent a preset name that sounds right. The generated union is what tells it the vocabulary, in the editor and in AGENTS.md, and the vocabulary is closed on purpose. What the compiler cannot do is fail a build on a wrong name. The lint rule above does, off the same manifest, so the manifest stays the authority either way.