Replay Button
Re-runs (or resets) a demo by replacing the wrapping element's inner HTML with a saved snapshot, then calling Alpine. so every Alpine scope re-binds. Pairs with the data-replayable="true" attribute emitted by any WireKit preview that can be re-run or reset — animation-capable components (<x-wirekit::, <x-wirekit::, animated card / hero / cta / feature / footer, chart) and state-mutating components whose demo is "used up" by interaction (a dismissible <x-wirekit:: or <x-wirekit::).
Why this component exists: it primarily powers WireKit's own documentation site — docs.wirekit.app wraps every re-runnable example in the replay pattern and renders a ↻ Replay control on top. Since it ships in the package regardless, it doubles as a ready-made building block for your own app: wire it up to re-run an animation, reset an interactive demo to its starting state, or restore a dismissed badge or alert. Combine it with the data-replayable contract below and use it however fits your UI.
How it works
The pattern requires three pieces:
- A wrapper element carrying
data-replay-targetanddata-replay-source="<encoded snapshot>". - Inside that wrapper: the actual demo (
<x-wirekit::,reveal> <x-wirekit::, etc.) which emitsstat animate> data-replayable="true". - The
<x-wirekit::itself, inside that same wrapper.replay-button> closest()matches the element itself or one of its ancestors and never a sibling, so a button placed next to the target instead of within it finds nothing — and finding nothing is silent: the button still renders, still focuses, still activates, and does nothing at all.
On click, the button walks closest('[data-replay-target]') from itself, replaces that element's innerHTML with the snapshot, then calls Alpine. to re-mount.
The docs site auto-injects this pattern around every :::preview block whose source matches a data-replayable shape, so developers reading the docs see a "↻ Replay" button on every re-runnable or resettable example — an animated demo, or a dismissible badge / alert whose chips can be restored after they are dismissed.
Usage
The simplest shape — drop an animated demo into a :::preview block and the docs site auto-wires the replay-target wrapper around it:
For developers wiring the pattern manually in their own app (outside the docs auto-injection), the full shape is:
@php
// Render the demo AND its button once, so the snapshot and the first paint
// cannot drift apart.
$demo = Blade::render(
'<x-wirekit::reveal preset="fade">Hello!</x-wirekit::reveal>'
.'<x-wirekit::replay-button />'
);
@endphp
<div data-replay-target data-replay-source="{{ $demo }}">
{!! $demo !!}
</div>
The replay assigns the snapshot to the target's innerHTML, which replaces everything inside it — the button included. So the button has to be part of the snapshot as well as part of the initial render; leave it out and the first click deletes the control that triggered it.
The data-replay-source attribute MUST hold the rendered markup, HTML-escaped: the handler reads it back through dataset and assigns it to innerHTML, and innerHTML does not run Blade, so a raw <x-wirekit::…> tag in the snapshot comes back as an unknown element rather than as the component. Blade's {{ }} echo does the escaping on its own — do not wrap it in e(...) as well, which escapes twice and restores the snapshot as visible angle-bracket text.
The button picks up the closest data-replay-target ancestor automatically. To target a specific selector instead, pass target="…" — that selector is also resolved with closest(), so it too must match the button's own element or one of its ancestors:
<x-wirekit::replay-button target="[data-demo-root]" />
Customizing the icon
Pass slot content to override the default circular-arrow glyph:
<x-wirekit::replay-button>
<x-wirekit::icon name="refresh" size="sm" />
Replay
</x-wirekit::replay-button>
Style hooks
Single BEM root: wk-replay-button. Override via your app.css:
.wk-replay-button {
color: var(--color-wk-text-subtle);
transition: color var(--transition-wk-duration);
}
.wk-replay-button:hover {
color: var(--color-wk-text);
}
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
label |
string |
'Replay' |
Becomes aria-label on the rendered <button>. |
target |
string|null |
null |
Optional CSS selector; when set, the click handler walks $el.closest(selector) instead of [data-replay-target]. |
Accessibility
- Default
aria-label="Replay"(overridable via thelabelprop). - Renders as a real
<button type="button">so keyboard activation (Space / Enter) works without extra wiring. - Dispatches
wirekit:replayed(bubbling) on thedata-replay-targetroot after re-mount; listeners can hook into the event for analytics / focus management.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus to / away from the button (native browser tab order). |
Space |
Activate the replay (re-mount the demo). |
Enter |
Activate the replay (re-mount the demo). |
Focus survives a replay, and it takes a deliberate step to do so. The button sits inside the target whose innerHTML is replaced, so the element you just activated is destroyed and rebuilt as a different node — left alone, the browser would drop focus to the document body and your next Tab would restart at the top of the page. The component therefore re-focuses the rebuilt button itself whenever the button held focus at activation, which is exactly the keyboard case; a pointer activation that never focused the button moves nothing. If the snapshot happens to carry no replay button, focus lands on the target instead, so you stay at the demo either way.
That restore runs before wirekit:replayed is dispatched, so it is a default rather than a fight: listen for the event and move focus onto the demo's first interactive element if that reads better for your page.