Skip to main content
WireKit
Copy for LLM

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

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

fade
fade-in

Slide family

slide-up
slide-up-in
slide-down
slide-down-in
slide-left
slide-left-in
slide-right
slide-right-in

Scale family

scale
scale-in
zoom
zoom-in (more dramatic than scale)

Spring family (overshoot easing)

flip
flip-in (3D rotateY)
rotate
rotate-in
bounce
bounce-in (elastic)
spring
spring-in (subtle elasticity)

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.

fade-out — fires on scroll into view with 1000ms delay
Waits 1000ms after entering the viewport, then fades to invisible. Press Replay to see it again.

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.

fast (150ms)
fast — 150ms
normal (300ms, default)
normal — 300ms (default)
slow (600ms)
slow — 600ms

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 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:

{{-- 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 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