# motion-components
> Framework-agnostic web components for spring-based motion: reveals, hover and press responses, text effects, animated icons, scroll scenes, charts and ready-made widgets. One HTML tag per effect, built on Lit and Motion.
- Install: `npm install motion-components`, then `import 'motion-components'` for every tag, or `import 'motion-components/motion-reveal'` for one.
- No build step: ``.
- Works in plain HTML, React 19, Vue, Svelte, Angular, Astro and Alpine. Wrap your own markup: `
Hello
`.
- Every animation is interruptible and respects `prefers-reduced-motion`.
- Every animated component has `play()`, `pause()`, `finish()`, `cancel()`, `playState` and `finished`, and fires `motion-start`, `motion-finish` and `motion-cancel` (bubbling, composed).
- Time is in seconds. Spring settings are `duration` and `bounce`.
##
Scroll-driven blur reveal. Maps the element's scroll progress through the
viewport to opacity, blur, and Y-translation. Direction controls whether the
blur happens on entry, exit, or both.
**Use it for:** hero images, quotes and section headings that should come
into focus, or fade away, as the page scrolls.
**Avoid it for:** an entrance that should play on its own once the content
is in view; use `motion-blur-in`, which plays over a fixed time. Avoid
`direction="both"` for text people need to read, because the text is fully
clear only when it is in the middle of the screen.
**Accessibility:** the content stays in the page while it is faded or
blurred, so screen readers can read it at any scroll position. Only
opacity, filter and transform change, so the layout does not shift.
**Reduced motion:** the content shows fully visible, sharp and in place at
once, for every `direction`, and it does not change as the page scrolls.
`motion-start` and `motion-finish` still fire, and `cancel()` keeps the
content visible.
**Common mistakes:** placing it near the end of the page, or on a page too
short to scroll. With `direction="in"`, the content is fully clear only
when its center reaches the middle of the screen, so it can stay faded and
blurred. Use `motion-blur-in` there.
Docs: https://www.motion-components.dev/docs/reveal/motion-blur/
### Install
```js
import 'motion-components/motion-blur'
```
Or without a build step:
```html
```
### Example
```html
```
### Attributes
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `intensity` | `number` | `10` | Maximum blur amount in pixels at the unfocused extreme. |
| `y` | `number` | `12` | Vertical translation in pixels at the unfocused extreme. |
| `once` | `boolean` | `true` | When `true` (and `direction="in"`), latch focused state on first reveal. Set `once="false"` to turn it off. |
| `direction` | `'in' \| 'out' \| 'both'` | `'in'` | `'in'` blurs on entry, `'out'` blurs on exit, `'both'` blur-focus-blur. |
### Properties
| Property | Type | Description |
| --- | --- | --- |
| `playState` | `'idle' \| 'running' \| 'paused' \| 'finished'` | Current state: `'idle'`, `'running'`, `'paused'` or `'finished'`. |
| `finished` | `Promise` | Resolves when the current run settles. A new run replaces the promise. |
### Methods
| Method | Description |
| --- | --- |
| `replay()` | Resets the latch and re-binds the scroll handler. |
| `play()` | Starts the animation, or resumes it when paused. Resolves once the run settles. Under `prefers-reduced-motion: reduce` the final state is applied immediately without animating. |
| `pause()` | Holds the animation at its current position. No-op unless running. |
| `finish()` | Jumps to the final state and settles. Fires `motion-finish`. |
| `cancel()` | Reverts to the initial state and returns to idle. Fires `motion-cancel`. |
### Events
The `motion-*` events bubble and are composed.
| Event | Description |
| --- | --- |
| `motion-start` | When a run starts. |
| `motion-finish` | When a run finishes, or `finish()` jumps to the end. |
| `motion-cancel` | When `cancel()` stops a run and resets it. |
### Slots
| Slot | Description |
| --- | --- |
| (default) | The content to blur on scroll. |
##
Viewport-triggered blur reveal. Starts blurred and translated, snaps to
focus once the element enters the viewport. The dominant editorial
entrance pattern of 2024–2025.
**Use it for:** headlines, hero text and images that should come into
focus with a short animation the first time they scroll into view.
**Avoid it for:** effects that should follow the scroll position; use
`motion-blur`. For a list of items, use `motion-stagger`. For a fade
without blur, use `motion-reveal`.
**Accessibility:** the content stays in the page while it is hidden and
blurred, so screen readers read it before it animates. Only opacity, filter
and transform change, so the layout does not shift.
**Reduced motion:** the content is never hidden or blurred. It shows at its
final position at once, with no animation. The `motion-start` and
`motion-finish` events still fire when it enters the view.
**Common mistakes:** wrapping a whole page section in one `motion-blur-in`,
so nothing shows until 10% of it is in view; wrap each block instead, or
lower `threshold`. Changing `threshold` after the element has rendered has
no effect, because it is read only once.
Docs: https://www.motion-components.dev/docs/reveal/motion-blur-in/
### Install
```js
import 'motion-components/motion-blur-in'
```
Or without a build step:
```html
```
### Example
```html
Headline
```
### Attributes
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `duration` | `number` | `0.7` | Spring duration of the reveal animation, in seconds. |
| `intensity` | `number` | `10` | Initial blur in pixels; animates to 0 on reveal. |
| `y` | `number` | `12` | Initial vertical offset in pixels; animates to 0 on reveal. |
| `threshold` | `number` | `0.1` | IntersectionObserver threshold (0–1) at which the reveal triggers. |
| `once` | `boolean` | `true` | When `true`, only animate the first time the element enters view. Set `once="false"` to turn it off. |
### Properties
| Property | Type | Description |
| --- | --- | --- |
| `playState` | `'idle' \| 'running' \| 'paused' \| 'finished'` | Current state: `'idle'`, `'running'`, `'paused'` or `'finished'`. |
| `finished` | `Promise` | Resolves when the current run settles. A new run replaces the promise. |
### Methods
| Method | Description |
| --- | --- |
| `replay()` | Resets to the blurred initial state and re-runs the reveal. |
| `play()` | Starts the animation, or resumes it when paused. Resolves once the run settles. Under `prefers-reduced-motion: reduce` the final state is applied immediately without animating. |
| `pause()` | Holds the animation at its current position. No-op unless running. |
| `finish()` | Jumps to the final state and settles. Fires `motion-finish`. |
| `cancel()` | Reverts to the initial state and returns to idle. Fires `motion-cancel`. |
### Events
The `motion-*` events bubble and are composed.
| Event | Description |
| --- | --- |
| `motion-start` | When a run starts. |
| `motion-finish` | When a run finishes, or `finish()` jumps to the end. |
| `motion-cancel` | When `cancel()` stops a run and resets it. |
### Slots
| Slot | Description |
| --- | --- |
| (default) | The content to blur-reveal. |
##
Viewport-triggered fade + translate reveal. Starts hidden, animates in
with a spring once the element intersects the viewport.
**Use it for:** headings, cards and images that should fade and rise into
place the first time they scroll into view.
**Avoid it for:** content in the first screen that people need at once.
For a list of items, use `motion-stagger`, which reveals children one after
another.
**Accessibility:** the content stays in the page while it is hidden, so
screen readers read it before it animates. Only opacity and transform
change, so the layout does not shift.
**Reduced motion:** the content shows at its final position at once, with
no animation.
**Common mistakes:** wrapping a whole page section in one reveal, so nothing
shows until 10% of it is in view; wrap each block instead, or lower
`threshold`. Putting a `motion-reveal` directly inside `motion-stagger`
animates the same element twice.
Docs: https://www.motion-components.dev/docs/reveal/motion-reveal/
### Install
```js
import 'motion-components/motion-reveal'
```
Or without a build step:
```html
```
### Example
```html
Reveals when scrolled into view
```
### Attributes
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `duration` | `number` | `0.6` | Spring duration of the reveal animation, in seconds. |
| `y` | `number` | `24` | Initial vertical offset in pixels; element rises to 0 on reveal. |
| `threshold` | `number` | `0.1` | IntersectionObserver threshold (0–1) at which the reveal triggers. |
| `once` | `boolean` | `true` | When `true`, only animate the first time the element enters view. Set `once="false"` to turn it off. |
### Properties
| Property | Type | Description |
| --- | --- | --- |
| `playState` | `'idle' \| 'running' \| 'paused' \| 'finished'` | Current state: `'idle'`, `'running'`, `'paused'` or `'finished'`. |
| `finished` | `Promise` | Resolves when the current run settles. A new run replaces the promise. |
### Methods
| Method | Description |
| --- | --- |
| `replay()` | Re-runs the reveal animation from its hidden initial state. |
| `play()` | Starts the animation, or resumes it when paused. Resolves once the run settles. Under `prefers-reduced-motion: reduce` the final state is applied immediately without animating. |
| `pause()` | Holds the animation at its current position. No-op unless running. |
| `finish()` | Jumps to the final state and settles. Fires `motion-finish`. |
| `cancel()` | Reverts to the initial state and returns to idle. Fires `motion-cancel`. |
### Events
The `motion-*` events bubble and are composed.
| Event | Description |
| --- | --- |
| `motion-start` | When a run starts. |
| `motion-finish` | When a run finishes, or `finish()` jumps to the end. |
| `motion-cancel` | When `cancel()` stops a run and resets it. |
### Slots
| Slot | Description |
| --- | --- |
| (default) | The content to reveal on scroll-in. |
##
Staggered viewport-triggered reveal for a list of children. Fades and
translates each direct child in sequence once the first child enters the viewport.
**Use it for:** lists, card grids and icon rows whose items should fade and
rise into place one after another when the list scrolls into view.
**Avoid it for:** a single block of content; use `motion-reveal` or
`motion-blur-in`. Avoid long lists that go far below the screen: the whole
sequence starts when the first child comes into view, so items lower down
can finish animating before anyone scrolls to them.
**Accessibility:** the children stay in the page while they are hidden, so
screen readers read them before they animate. Only opacity and transform
change, so the layout does not shift.
**Reduced motion:** all children show at their final position at once,
with no animation. No run starts, so `motion-start` and `motion-finish` do
not fire.
**Common mistakes:** only direct child elements animate. Wrapping all items
in one `
` or `
` makes them animate as one block; put the items
directly inside `motion-stagger`. Text placed directly inside, outside a
child element, is not animated.
Docs: https://www.motion-components.dev/docs/reveal/motion-stagger/
### Install
```js
import 'motion-components/motion-stagger'
```
Or without a build step:
```html
```
### Example
```html
One
Two
Three
```
### Attributes
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `interval` | `number` | `0.06` | Delay between each child's reveal, in seconds. |
| `duration` | `number` | `0.5` | Spring duration of each child's reveal, in seconds. |
| `y` | `number` | `16` | Initial vertical offset in pixels for each child; rises to 0. |
| `from` | `'first' \| 'last' \| 'center'` | `'first'` | Stagger origin: `'first'`, `'last'` or `'center'`. |
| `once` | `boolean` | `true` | When `true`, only animate the first time the list enters view. Set `once="false"` to turn it off. |
### Properties
| Property | Type | Description |
| --- | --- | --- |
| `playState` | `'idle' \| 'running' \| 'paused' \| 'finished'` | Current state: `'idle'`, `'running'`, `'paused'` or `'finished'`. |
| `finished` | `Promise` | Resolves when the current run settles. A new run replaces the promise. |
### Methods
| Method | Description |
| --- | --- |
| `replay()` | Resets and re-runs the staggered reveal. |
| `play()` | Starts the animation, or resumes it when paused. Resolves once the run settles. Under `prefers-reduced-motion: reduce` the final state is applied immediately without animating. |
| `pause()` | Holds the animation at its current position. No-op unless running. |
| `finish()` | Jumps to the final state and settles. Fires `motion-finish`. |
| `cancel()` | Reverts to the initial state and returns to idle. Fires `motion-cancel`. |
### Events
The `motion-*` events bubble and are composed.
| Event | Description |
| --- | --- |
| `motion-start` | When a run starts. |
| `motion-finish` | When a run finishes, or `finish()` jumps to the end. |
| `motion-cancel` | When `cancel()` stops a run and resets it. |
### Slots
| Slot | Description |
| --- | --- |
| (default) | The list of children to reveal in sequence. |
##
Spring-based hover transform primitive. Animates scale, translation, rotation,
and skew on mouse enter, springs back on leave. Composes with any slotted content.
**Use it for:** buttons, links, cards and images that should grow, move or
rotate a little while the mouse pointer is over them.
**Avoid it for:** feedback on click or tap; use `motion-press`. For effects
that follow the pointer, use `motion-magnetic` or `motion-tilt`. Do not use
it as the only sign that something is interactive, because it does not run
on keyboard focus.
**Accessibility:** it adds no role, `tabindex` or ARIA attributes, so the
slotted content keeps its own role and keyboard behavior. The effect reacts
to mouse enter and leave only, so keyboard focus does not trigger it; give
the content its own visible focus style. Only transform changes, so the
layout does not shift, but the moved content can cover nearby elements.
**Reduced motion:** mouse enter and leave do nothing, and the content stays
at its rest state.
**Common mistakes:** wrapping a block element such as a card. The host is
`display: inline-block`, so the card can shrink to the width of its
content; set `display: block` on `motion-hover`.
Docs: https://www.motion-components.dev/docs/respond/motion-hover/
### Install
```js
import 'motion-components/motion-hover'
```
Or without a build step:
```html
```
### Example
```html
```
### Attributes
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `scale` | `number` | `1.05` | Scale factor on hover. `1` is no scaling. |
| `x` | `number` | `0` | Horizontal translation in pixels on hover. |
| `y` | `number` | `0` | Vertical translation in pixels on hover. |
| `rotate` | `number` | `0` | Rotation in degrees on hover. |
| `skew` | `number` | `0` | Skew along the X axis in degrees on hover. |
| `duration` | `number` | `0.3` | Spring duration of the hover transition, in seconds. |
| `bounce` | `number` | `0.3` | Spring bounciness (0 = critically damped, higher = more elastic). |
| `disabled` | `boolean` | `false` | When `true`, ignores pointer input and settles to the rest state. |
### Slots
| Slot | Description |
| --- | --- |
| (default) | The content that lifts on hover. |
##
Magnetic cursor pull. Slotted content drifts toward the pointer by
`strength`, springs back when the mouse leaves. Best on small interactive
targets like buttons and icons.
**Use it for:** small targets such as buttons, icon buttons and short links
that should drift a little toward the mouse pointer.
**Avoid it for:** large areas such as cards or images. The pull is a
fraction of the distance from the center, so large elements move far. Use
`motion-tilt` or `motion-hover` there.
**Accessibility:** it adds no role, `tabindex` or ARIA attributes, so the
slotted content keeps its own role and keyboard behavior. The pull reacts
to mouse movement only, so keyboard focus does not move it. Only transform
changes, so the layout does not shift. Put a real link or button inside.
**Reduced motion:** mouse movement does nothing, and the content stays in
its normal position.
**Common mistakes:** expecting the pull to start before the pointer reaches
the element. It reacts only while the pointer is over `motion-magnetic`
itself; to make the active area larger, add `padding` to `motion-magnetic`.
Docs: https://www.motion-components.dev/docs/respond/motion-magnetic/
### Install
```js
import 'motion-components/motion-magnetic'
```
Or without a build step:
```html
```
### Example
```html
Contact
```
### Attributes
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `strength` | `number` | `0.4` | Pull strength as a fraction of cursor-to-center distance. |
| `duration` | `number` | `0.5` | Spring duration of the pull and release transitions, in seconds. |
| `bounce` | `number` | `0.4` | Spring bounciness of the pull and release (0 = no overshoot). |
| `disabled` | `boolean` | `false` | When `true`, ignores pointer input and settles to the rest state. |
### Slots
| Slot | Description |
| --- | --- |
| (default) | The element that follows the cursor. |
##
Tactile press primitive. Scales slotted content down on `pointerdown`
and bounces back on release for a physical click feel.
**Use it for:** buttons, toggles and clickable cards that should shrink a
little while pressed with a mouse, finger or pen, then spring back on
release.
**Avoid it for:** content that does nothing when clicked, because the press
makes it look clickable. For hover feedback, use `motion-hover`.
**Accessibility:** it adds no role, `tabindex` or keyboard handling, so it
does not make content clickable or focusable. Put a real `