Skip to main content
Copy for LLM

Animations

WireKit ships a small motion system: a handful of design tokens, the 11 keyframe preset bases below (each with in and out variants), 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 1ms 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.
--motion-wk-delay-none 0ms The delay rung a reveal starts from.
--motion-wk-delay-sm 75ms Enough to read as deliberate rather than as lag.
--motion-wk-delay-md 150ms The step a staggered group is usually built on.
--motion-wk-delay-lg 300ms Second-tier content — a subtitle after its heading.
--motion-wk-delay-xl 500ms Last in a sequence; past this a reader stops waiting for it.

<x-wirekit::reveal delay="md"> resolves to var(--motion-wk-delay-md); a raw number (:delay="1000") bypasses the rungs and sets the milliseconds directly. Override a rung in your @theme block and every reveal using it moves together — which is the reason they are tokens rather than five numbers spread across the catalog.

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 How much must be in view before it fires (only used for viewport). A target taller than the screen fires once it fills that share of the viewport's height
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.

Flashing a row that changed

x-wk-flash lights a row up in a tone for a moment when it changes: green for an addition, amber for one fewer, red for a removal. It works on a list item, a table row or a card, and the server drives it by writing three attributes on the row:

Attribute What it says
data-wk-flash The tone: success, warning, danger, info, primary or neutral
data-wk-flash-key A value that is new with every change, a counter or a time. A new key plays the flash again, in the same tone too
data-wk-flash-leave The row is on its way out. It flashes once more and is taken out of the page when the flash ends
Rows that just changed
Item Quantity
Espresso 2
Croissant 1
Orange juice 0
{{-- 1. The directive on every row; the attributes only on the row that changed --}}
<x-wirekit::table.row
    x-wk-flash
    :data-wk-flash="$changed[$line->id]['tone'] ?? null"
    :data-wk-flash-key="$changed[$line->id]['key'] ?? null"
>
    {{-- 2. The cells of the line --}}
</x-wirekit::table.row>

A row that is removed has to be drawn once more to be seen leaving: render it in the render that removes it, with data-wk-flash="danger" and data-wk-flash-leave, and leave it out of the next one. While it flashes it is hidden from assistive technology and cannot be pressed.

The tints are tokens, --color-wk-flash-success and its siblings, so a quieter or a stronger flash is one value in your theme. The flash lasts --motion-wk-duration-slow.

Under reduced motion the tint does not fade. It appears at once, stays for the same time and is gone at once, so the change is still there to see. data-reduce-motion on <html> decides first, as it does for every other animation here.

Bringing a result into view

x-wk-scroll-into-view brings a region into view and gives it the focus when an action makes it appear or change: a form that opens at the top of the page, a key shown once, a message above a long list. Without it, a reader who pressed a button far down the page sees nothing happen. The server decides when, through one attribute:

Attribute What it says
data-wk-scroll-key A value that is new with every result to show, an id or a time. The region comes into view when it appears with a key and again whenever the key changes. Without a key it stays where it is, so a page that loads with the region on it does not jump
{{-- 1. Rendered after the action, with a key that is new every time --}}
@if ($createdToken)
    <section x-wk-scroll-into-view data-wk-scroll-key="{{ $createdToken->id }}">
        <x-wirekit::card>
            <x-wirekit::clipboard-button :value="$createdToken->plainTextToken">Copy the new key</x-wirekit::clipboard-button>
        </x-wirekit::card>
    </section>
@endif
// 2. The action that produces it, in your Livewire component
public function createToken(): void
{
    $this->createdToken = auth()->user()->createToken($this->name);
}

A region already in full view is not scrolled. The focus moves to it either way, so the keyboard goes on where the reader is looking; a region that cannot take the focus gets tabindex="-1" for that, and keeps it through the component's next render for as long as it holds the focus, so a request that answers while the reader is still there does not move the focus to the page. Add .nofocus, as in x-wk-scroll-into-view.nofocus, to leave the focus where it is. A sticky header that would cover the region is answered by the region's own scroll-margin-top, which the scroll respects: class="scroll-mt-20".

The same works in an application whose main area scrolls instead of the window. There "in full view" means in full view of that area, so a region cut off by its top edge, under a fixed header, comes in like one far above it. The band a pinned header or bar reports as its area's scroll-padding, as a sticky page header does, counts as covered too, so a region standing under that header comes into view. A glide that is stopped on the way is followed by a jump once it has had its time, so the region does not stay out of view. When one render brings several regions, the first of them on the page comes into view and takes the focus, and the others stay where they are.

Under reduced motion the page jumps to the region instead of gliding. data-reduce-motion on <html> decides first. For the other direction, back to the top of a long page, there is the scroll-to-top button.

Reduced motion

The animation system is fully accessible:

  • The global @media (prefers-reduced-motion: reduce) block in wirekit.css snaps animation-duration to 1ms on WireKit's own elements and on everything inside them — the selector is [class^="wk-"] *, a descendant combinator, and the !important on it is load-bearing rather than lazy (Tailwind's duration utilities sit at the same specificity, so without it the winner would depend on how a JIT build happened to order its output). In practice that is your whole page. wk-scrollbar is the class WireKit puts on <html> and <body>, so once it is there every element in the document is a descendant of a wk- element and the clamp reaches all of it: a bare <div> with animation: spin 2s appended straight to <body> is clamped to the same 1ms under reduce. Treat the rule as document-wide rather than component-scoped. If your application needs motion anyway, the lever is data-reduce-motion="no-preference" on <html> — see theming.

  • One element can keep its motion: data-wk-motion="essential". It releases that clamp for the element it sits on and everything inside it, in both halves — the OS one and the data-reduce-motion="reduce" one. It is for motion that IS the information: a live trace, a queue that visibly moves work along, a shape whose stillness would read as a hang. WCAG 2.3.3 exempts motion essential to the functionality, and that exemption is the whole boundary here — an animation that merely looks better in motion does not qualify, and marking it makes your page worse for the reader who asked. It does not restart WireKit's own indicators: a spinner, a skeleton and an indeterminate progress bar are stopped by rules of their own rather than slowed, and each announces its state through role="status", so nothing is lost with the movement.

    {{-- The trace is the reading; frozen, it says nothing. --}}
    <div data-wk-motion="essential" class="ecg-trace">…</div>
    
  • 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.

View transitions

Livewire animates a DOM change through the browser's View Transitions API when you name the elements that survive it:

{{-- 1. The same name on both sides of the change is what pairs the two states. --}}
<x-wirekit::card wire:transition="summary">
    {{ $summary }}
</x-wirekit::card>

WireKit ships no JavaScript for this, on purpose. Livewire already detects document.startViewTransition before using it, turns the animation off under prefers-reduced-motion: reduce, and steps aside while a modal <dialog> is open. A second driver in this package would race the first one on the same mutation, and the loser would be whichever snapshot the browser took second.

A browser without the API applies the same DOM change instantly. Nothing about the result depends on the animation — it is polish on top of an update that already happened.

When the shape changes too

Two snapshots with different aspect ratios are stretched into each other by default, which reads as a squash rather than as a move. The fix is per transition name, so it belongs in your own stylesheet next to the name you chose:

@supports (view-transition-name: none) {
    ::view-transition-old(summary),
    ::view-transition-new(summary) {
        /* 1. Cross-fade off: both snapshots stay opaque and the new one covers the old. */
        animation: none;
        mix-blend-mode: normal;
        /* 2. Fill the group box and crop, rather than stretch into the other aspect ratio. */
        height: 100%;
        overflow: clip;
        object-fit: cover;
    }
}

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

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store