Skip to content
LaravelBlog

livewire/livewire: wire:transition no longer goes silent after a Flux toast auto-dismisses

LaravelBlogBot
LaravelBlogBot

Contributed by ghabriel25

Part of the livewire v4.4.7 release

Livewire v4 includes a targeted fix for wire:transition (#10629). Until now, any open popover on the page — even one with no visible content — caused Livewire to skip View Transitions entirely. That quietly broke animations on pages using Flux toasts: after a toast auto-dismissed, its popover host stayed open in the DOM, and every subsequent wire:transition on the page was silently skipped. The fix refines that guard so only visibly populated popovers block transitions, while modal dialogs continue to block them exactly as before.

The bug: transitions stopped after a toast came and went

wire:transition is built on the View Transitions API. Its ::view-transition pseudo-elements paint in the top layer — above popovers and modal dialogs — so running a transition while that kind of UI is on screen can cause the animation to render over it. To prevent that, the old implementation used a very conservative guard:

if (document.querySelector('dialog:modal, :popover-open')) return callback()

The problem is that :popover-open only tells you a popover is open, not that it is showing anything. Some components keep a popover mounted and open as a persistent container while its contents are hidden — Flux's toast host being the notable example. Once a toast auto-dismissed, the empty host still matched :popover-open, so the guard matched forever after. The result was no error and no warning, just animations that silently stopped working for the rest of the page session (tracked as flux#2783).

What changed

Modal dialogs still suppress transitions unconditionally. For popovers, Livewire now only skips the transition when the open popover actually contains something visible:

if (document.querySelector('dialog:modal')) return callback()

for (let popover of document.querySelectorAll(':popover-open')) {
    for (let el of popover.querySelectorAll('*')) {
        if (el.checkVisibility()) return callback()
    }
}

The behavior now breaks down into three cases:

  • Modal dialog open → transition is skipped (unchanged).
  • Popover open with a visible descendant → transition is skipped.
  • Popover open but visually empty → transition runs again.

One subtlety worth knowing: the popover host element itself always passes checkVisibility() while open, which is why the check inspects descendants rather than the popover element. Visible descendants are the proxy for "this popover is actually showing UI." The PR also adds two browser tests that stub document.startViewTransition and assert whether it was invoked — one with a persistently open but contentless popover, one with visible popover content — locking in both sides of the behavior.

Why checkVisibility() and not something simpler?

Checking text content, child count, or specific element names would all be unreliable. A popover can contain non-textual UI, hidden DOM nodes, or structural markup that doesn't correspond to anything on screen. Component-specific selectors (say, sniffing for Flux's markup) would couple Livewire's core to another package's implementation details.

Element.checkVisibility() asks the browser directly whether an element would actually be rendered, accounting for things like display: none and other ways content gets hidden. That keeps the logic generic and answers exactly the question that matters.

Note that the :popover-open check couldn't simply be removed, either. A genuinely visible popover must still block transitions, otherwise the original paint-over-popover problem returns. The new code handles both failure modes at once.

Upgrade impact coming from v3.8.10

This fix lives on the 4.x branch, so v3.8.10 still has the old guard — the "dead transitions after a toast" behavior is part of what you leave behind when you move to v4. There is no API change involved: wire:transition usage is identical, with no new attributes, config, or opt-ins. The practical difference is that transitions will now run in situations where they previously did nothing. If your app uses persistent popover hosts — toast containers, some dropdown or tooltip patterns — pages that felt statically broken should start animating again:

<div>
    {{-- Stays mounted as an open popover host, even when empty --}}
    <flux:toast />

    <button wire:click="$toggle('show')">Toggle</button>

    @if ($show)
        <div wire:transition>
            This animates again — even after the toast dismissed itself.
        </div>
    @endif
</div>

On browser support: checkVisibility() is available in Chrome/Edge 105+, Firefox 106+, and Safari 16.4+. If your app uses popover-based components at all, the Popover API itself already requires newer browsers than that, so you're covered.

Takeaways

  • Fixes wire:transition being silently skipped after a Flux toast auto-dismissed (flux#2783).
  • Modal dialogs still suppress transitions unconditionally, as before.
  • Open popovers now only block transitions when they contain visible content.
  • Detection uses the native Element.checkVisibility() — no coupling to any component library.
  • Ships in Livewire v4; no API changes, but expect transitions to newly run on pages with persistent popover hosts.

Sources

More from this release

Related Articles