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> 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.
Same gallery, different surface
The <x-wirekit::reveal> 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
Slide family
Scale family
Spring family (overshoot easing)
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.
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.
Utility classes (no Alpine)
For developers who don't want Alpine in the loop, the utility classes apply the keyframe directly:
<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:
<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:
<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> 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 inwirekit.csssnapsanimation-durationto0.01mson 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: 0keyframes 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:
{{-- 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 |
{{-- 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 onDOMContentLoaded. - Out-presets default state —
*-outkeyframes end atopacity: 0; the element is invisible after the animation. Pair them withwire:replaceor Alpinex-showtoggles, 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>— the Blade wrapper<x-wirekit::stat animate>— counter-animation (separate Alpine plugin)- Theming — design-token reference
- WAI-ARIA APG: Animation from Interactions