---
title: Animations
description: Motion design tokens, animation presets, and the wirekitAnimate Alpine helper
visibility: guest
draft: false
---

# Animations

WireKit ships a small motion system: six design tokens, eleven keyframe presets (each with `in` and `out` variants = 22 keyframes), corresponding utility classes, and the `wirekitAnimate` Alpine helper that drives the [`<x-wirekit::reveal>`](components/reveal.md) Blade wrapper.

Everything in this system honors `prefers-reduced-motion: reduce` — the global `@media` block in `dist/wirekit.css` snaps `animation-duration` to `0.01ms` for any class matching `wk-animate-*`, so reveal-on-scroll degrades gracefully to "appear in final state".

## Motion tokens

| Token | Default | Purpose |
|-------|---------|---------|
| `--motion-wk-duration-fast` | `150ms` | Quick state changes — hover, focus rings. Aliased from legacy `--transition-wk-duration`. |
| `--motion-wk-duration-normal` | `300ms` | **Default for all `wk-animate-*` utilities.** Material / shadcn-aligned. |
| `--motion-wk-duration-slow` | `600ms` | Hero-section entrance, staged reveals. |
| `--motion-wk-easing-out` | `cubic-bezier(0.16, 1, 0.3, 1)` | Ease-out cubic — "decelerates into final state" — natural reveal feel. |
| `--motion-wk-easing-in` | `cubic-bezier(0.7, 0, 0.84, 0)` | Ease-in cubic — "accelerates out of state" — used for out-presets that fade content away. |
| `--motion-wk-easing-spring` | `cubic-bezier(0.5, 1.5, 0.5, 1)` | Spring-overshoot — used for `bounce`, `flip`, `rotate`, `spring` presets to give visible elasticity. |

## The 11 preset bases

Each base has both an `-in` (appear into final state) and `-out` (leave to opacity-0 / off-screen) variant. The `-in` variants are by far the more common case.

| Base | Best for | Easing | Direction |
|------|----------|--------|-----------|
| `fade` | Content reveals where shape doesn't matter | `out` | Opacity only |
| `slide-up` | Cards / list items entering from below | `out` | translateY 1rem → 0 |
| `slide-down` | Notifications, alerts dropping in | `out` | translateY -1rem → 0 |
| `slide-left` | Sidebar reveals from the right | `out` | translateX 1rem → 0 |
| `slide-right` | Sidebar reveals from the left | `out` | translateX -1rem → 0 |
| `scale` | Cards in feature grids — subtle pop | `out` | scale 0.92 → 1 |
| `zoom` | Hero images, modal entrances | `out` | scale 0.5 → 1 |
| `flip` | Card flips, decision reveals (3D) | `spring` | rotateY -90deg → 0 |
| `rotate` | Tag-pull / reveal-with-spin (decorative) | `spring` | rotate -12deg + scale 0.9 → 1 |
| `bounce` | Hero CTA appearing — calls attention | `spring` (slow) | scale 0.3 → 1.05 → 0.9 → 1 |
| `spring` | List items with subtle elasticity | `spring` (slow) | translateY 0.75rem + scale 0.95 → settled |

## Interactive preset gallery

Each preview wraps a `<x-wirekit::card>` in `<x-wirekit::reveal>` and uses the default `trigger="viewport"`: the animation fires when the card scrolls into view. The `:once="false"` prop lets the animation re-fire each time the card crosses back into the viewport, and the replay icon in the preview's top-right corner replays without scrolling. All previews use `duration="slow"` (600ms) so the motion is easy to follow.

::: tip Same gallery, different surface
The [`<x-wirekit::reveal>`](components/reveal.md) component-docs page shows the same 11 presets via the same Blade wrapper. The two pages exist for different reasons: `/animations` is the **motion-system reference** (tokens, keyframe definitions, helper API), `/components/reveal` is the **component-API page**. The gallery itself is identical on purpose — one visual atlas, two entry points.
:::

### Fade family

:::preview{title="fade"}
<x-wirekit::reveal preset="fade-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>fade-in</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

### Slide family

:::preview{title="slide-up"}
<x-wirekit::reveal preset="slide-up-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>slide-up-in</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="slide-down"}
<x-wirekit::reveal preset="slide-down-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>slide-down-in</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="slide-left"}
<x-wirekit::reveal preset="slide-left-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>slide-left-in</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="slide-right"}
<x-wirekit::reveal preset="slide-right-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>slide-right-in</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

### Scale family

:::preview{title="scale"}
<x-wirekit::reveal preset="scale-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>scale-in</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="zoom"}
<x-wirekit::reveal preset="zoom-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>zoom-in (more dramatic than scale)</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

### Spring family (overshoot easing)

:::preview{title="flip"}
<x-wirekit::reveal preset="flip-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>flip-in (3D rotateY)</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="rotate"}
<x-wirekit::reveal preset="rotate-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>rotate-in</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="bounce"}
<x-wirekit::reveal preset="bounce-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>bounce-in (elastic)</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="spring"}
<x-wirekit::reveal preset="spring-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>spring-in (subtle elasticity)</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

## Out variants — animate to invisible

Every preset ships an `-out` counterpart that animates the element away to `opacity: 0`. The card below fades out the moment it enters the viewport — every other preset follows the same shape (`<preset>-out`). Use the Replay button at the top-right of the preview to re-fire the animation.

:::preview{title="fade-out — fires on scroll into view with 1000ms delay"}
<x-wirekit::reveal preset="fade-out" trigger="viewport" duration="slow" :delay="1000" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>Waits 1000ms after entering the viewport, then fades to invisible. Press Replay to see it again.</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

::: warning Out-presets end at opacity 0
The element is invisible after an `-out` animation finishes. Pair `-out` with `wire:replace`, Alpine `x-show`, or a manual DOM removal — never as a standalone reveal that leaves a permanently-invisible card on the page.
:::

## Duration comparison

Same `slide-up-in` preset rendered with each of the three duration tokens. At `fast` (150ms) the motion is barely perceptible; at `slow` (600ms) the elasticity becomes obvious.

:::preview{title="fast (150ms)"}
<x-wirekit::reveal preset="slide-up-in" duration="fast" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>fast — 150ms</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="normal (300ms, default)"}
<x-wirekit::reveal preset="slide-up-in" duration="normal" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>normal — 300ms (default)</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

:::preview{title="slow (600ms)"}
<x-wirekit::reveal preset="slide-up-in" duration="slow" :once="false" style="--wk-stagger-step: 0ms">
    <x-wirekit::card><x-wirekit::card.body>slow — 600ms</x-wirekit::card.body></x-wirekit::card>
</x-wirekit::reveal>
:::

## Utility classes (no Alpine)

For developers who don't want Alpine in the loop, the utility classes apply the keyframe directly:

```html
<div class="wk-animate-fade-in" data-replayable="true">Reveal me</div>
<div class="wk-animate-slide-up-in wk-animate-slow" data-replayable="true">Slow slide-up</div>
```

Pair any preset class with one of the duration modifiers (`wk-animate-fast`, `wk-animate-normal`, `wk-animate-slow`) to override the default 300ms.

### Replay-button contract for utility-class animations

WireKit Blade components that drive animations (`<x-wirekit::reveal>`, `<x-wirekit::stat animate>`, `<x-wirekit::card animateIn="…">`, `<x-wirekit::hero animateIn="…">`, etc.) automatically emit `data-replayable="true"` on their root element. `docs.wirekit.app`'s preview chrome detects this attribute and surfaces a `↻ Replay` button on every preview block so readers can re-watch the animation without reloading the page.

When you paint an animation via the raw `wk-animate-*` utility classes (no Blade component), there's no PHP-side render path to emit the attribute automatically. To keep the replay button surfacing on utility-class animations, **add `data-replayable="true"` to the same element that carries the `wk-animate-*` class**:

```html
<span class="wk-animate-fade-in" data-replayable="true">Reveal me</span>
```

The attribute is otherwise inert — it adds no styling, no JavaScript binding, no accessibility implications. It exists purely as a discovery marker for replay tooling. On a developer app without the replay surface, the attribute is a harmless no-op.

## The `wirekitAnimate` Alpine helper

When you need finer control — viewport triggering, click triggering, manual dispatch, conditional once/repeat behavior — use the Alpine helper directly:

```blade
<div data-replayable="true" x-data="wirekitAnimate('slide-up-in', { trigger: 'viewport', threshold: 0.4 })">
  …content…
</div>

<div data-replayable="true" x-data="wirekitAnimate('bounce-in', { trigger: 'click', once: false })">
  Click to bounce again
</div>

<div data-replayable="true" x-data="wirekitAnimate('flip-in', { trigger: 'manual' })">
  …animated when window.dispatchEvent(new Event('wirekit:reveal'))
</div>
```

| Option | Type | Default | Notes |
|--------|------|---------|-------|
| `trigger` | string | `'viewport'` | `'viewport'` / `'click'` / `'manual'` |
| `once` | bool | `true` | If false, re-fires on every trigger |
| `threshold` | float | `0.4` | IntersectionObserver threshold (only used for `viewport`) |
| `duration` | string | `'normal'` | `'fast'` / `'normal'` / `'slow'` |

Most developers should use the [`<x-wirekit::reveal>`](components/reveal.md) Blade wrapper instead — same options, cleaner API, validates against the preset enum.

## Reduced motion

The animation system is fully accessible:

- The global `@media (prefers-reduced-motion: reduce)` block in `wirekit.css` snaps `animation-duration` to `0.01ms` on any element matching `[class*="wk-animate-"]` or `[class*="animate-"]`.
- Element ends up in its final state immediately; no transform, no opacity transition, no perceptible motion.
- The Alpine helper still adds the class on reduced-motion (so `opacity: 0` keyframes don't leave the element invisible) — the global block handles the snap.

For developers building their own animation systems on top of WireKit's tokens, mirror this pattern: either define your animations inside `@media (prefers-reduced-motion: no-preference) { ... }` OR add a global override that targets your custom utility classes.

## Per-component `animateIn` props

Several marketing-heavy components accept an `animateIn` prop that wires the Reveal helper internally — saves the wrapper:

```blade
{{-- Equivalent to wrapping in <x-wirekit::reveal preset="fade-in"> --}}
<x-wirekit::card animateIn="fade">…</x-wirekit::card>

<x-wirekit::feature animateIn="slide-up" icon="bolt" title="Fast">…</x-wirekit::feature>

<x-wirekit::hero animateIn="zoom-in">…</x-wirekit::hero>
```

Components with built-in entrance transitions — `modal`, `drawer`, `toast`, `tooltip`, `dropdown`, `popover` — do NOT accept `animateIn` (would conflict with their own motion).

See each component's docs page for the full list of supported animateIn presets.

### Tailwind-UI naming-convention aliases

Both `<x-wirekit::reveal preset="…">` and the per-component `animateIn` prop accept four short-form aliases for the directional slide presets — convenient when porting from `fade-up` / `fade-down` style conventions used by Tailwind UI / Headless UI / Framer Motion patterns:

| Short form   | Canonical preset |
|--------------|------------------|
| `fade-up`    | `slide-up-in`    |
| `fade-down`  | `slide-down-in`  |
| `fade-left`  | `slide-left-in`  |
| `fade-right` | `slide-right-in` |

```blade
{{-- These render identical motion: --}}
<x-wirekit::reveal preset="fade-up">…</x-wirekit::reveal>
<x-wirekit::hero animateIn="fade-up">…</x-wirekit::hero>

{{-- …and are equivalent to the canonical form: --}}
<x-wirekit::reveal preset="slide-up-in">…</x-wirekit::reveal>
<x-wirekit::hero animateIn="slide-up-in">…</x-wirekit::hero>
```

The aliases resolve at component-render time; the underlying Alpine helper always sees the canonical `slide-*-in` preset name. The map is byte-identical across every surface — guarded by `FadePresetAliasConsistencyTest` so a divergence between `<x-wirekit::reveal>` and `animateIn` blocks the build.

## Pitfalls

- **Above-the-fold + viewport trigger** — element is already intersecting at page load; the animation fires immediately and the user sees no perceived motion. For hero-section reveals, use `trigger="manual"` + dispatch the event on `DOMContentLoaded`.
- **Out-presets default state** — `*-out` keyframes end at `opacity: 0`; the element is invisible after the animation. Pair them with `wire:replace` or Alpine `x-show` toggles, never as a standalone reveal.
- **Don't bake animations into form components** — input focus rings, validation transitions, and other state changes are handled by `--transition-wk-duration` (150ms). Reveal animations are for entrance / exit of large content blocks, not micro-interactions.

## See also

- [`<x-wirekit::reveal>`](components/reveal.md) — the Blade wrapper
- [`<x-wirekit::stat animate>`](components/stat.md) — counter-animation (separate Alpine plugin)
- [Theming](theming.md) — design-token reference
- WAI-ARIA APG: [Animation from Interactions](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions.html)
