Using Skirbi with Livewire
@skirbi/semtic and @skirbi/pinta are built to survive being morphed by
Livewire (and by Alpine’s own morph, which Livewire uses under the hood).
A Skirbi component renders itself once, on connect, into real HTML; a
Livewire re-render then morphs the DOM around it rather than replacing it
wholesale, so component state (open/closed, focus, scroll position) isn’t
lost just because a wire:model changed somewhere on the page.
This is the same “components are resistant to morphing” principle from Why Skirbi; this guide is the practical side of it, for a real Livewire app rather than the design rationale.
Morph control attributes
@skirbi/sugar’s withConnectedSucu mixin (which every morph-aware
Skirbi component uses) recognizes a small set of attributes that let you
override its default morph behaviour per instance:
morph-ignore– don’t observe this element for morphs at all. Use it on a component whose own internal state should never be touched by a Livewire re-render (a modal that’s mid-animation, a panel a user is actively editing inside).morph-child,morph-attribute,morph-subtree– narrow what’s observed instead of ignoring it outright: child-list changes only, attribute changes only, or the whole subtree. Reach for these when the default (child-list only) isn’t matching what Livewire is actually changing.morph-debug– logs every observed mutation for that element to the console. Turn it on when a component isn’t reacting the way you expect and you need to see what Livewire’s morph is actually doing to it.
Overriding a component’s template
Every Skirbi component looks for a <template id="..."> matching its own
template ID before falling back to the one it ships with (see
HtmlTemplate in @skirbi/sugar’s HTMLElementSugar). That means a
project can override a component’s internal structure – including adding
morph-ignore to specific nodes inside it – without forking the
component itself, just by shipping a <template> with the right id
earlier in the page.
For example, overriding <dibuho-container-panel>’s template to keep its
header out of the morph-observed set:
<template id="dibuho-container-panel-template">
<semtic-panel type="card" dibuho-container-panel>
<semtic-stack dibuho-container-panel-stack>
<semtic-header morph-ignore dibuho-container-panel-header></semtic-header>
<semtic-stack dibuho-container-panel-body></semtic-stack>
</semtic-stack>
</semtic-panel>
</template>
Every <dibuho-container-panel> on the page then renders from this
template instead of its built-in one – no change to the component, no
extra JS, just an inline <template>.
Binding wire:model
A Blade component wrapping a Skirbi input just needs to keep whatever
wire:model* attribute was passed in and put it on the real control
Livewire needs to bind to (typically an <input>/<select>/<textarea>
inside the component, reachable via a [wc-control] hook):
@props(['name' => null])
@php
$model = $attributes->whereStartsWith('wire:model')->first();
@endphp
<semtic-input name="{{ $name }}">
<input wc-control {{ $model ? "wire:model=$model" : '' }}>
</semtic-input>
The component itself doesn’t know or care about Livewire; it only cares that the control it renders is a real, bindable form element.
See also
- Why Skirbi – “Components are resistant to morphing”
- Runtime Enhancement Contract – the deeper spec behind build-time rendering vs. runtime enhancement