Alpine.js x-anchor Gets Fixed Positioning, .noflip, and a Livewire Teleport Fix
Alpine.js v3.15.9 through v3.15.12 quietly reworked x-anchor with .fixed positioning, .noflip, dynamic refs, and fixed a Livewire teleport memory leak.
If you build dropdowns, popovers, or tooltips in a Laravel or Livewire app, there is a good chance you already lean on Alpine’s Anchor plugin without thinking much about it. x-anchor has quietly been one of Alpine’s most useful plugins since it shipped, wrapping the Floating UI positioning engine in a single directive. Over the last several point releases, from v3.15.9 through the current v3.15.12, the Alpine core team made a handful of changes to it that solve real, specific problems: positioning that breaks inside scrollable containers, popovers that flip when you don’t want them to, anchors that need to move at runtime, and a memory leak that showed up specifically when Alpine’s x-teleport lives inside a Livewire component that re-renders a lot.
None of these are headline features. All of them are the kind of thing you hit in production and then spend an afternoon working around, only to find out later that a newer patch release already fixed it.
The overflow problem .fixed solves
x-anchor’s default behavior applies position: absolute to the anchored element and calculates top/left relative to the reference element. That works fine until the reference element lives inside a container with overflow: hidden, overflow: clip, or overflow: auto. In that case the anchored dropdown gets clipped right along with everything else in the container, even though visually it should be able to render outside those bounds; a scrollable table with a row-action menu is the classic case.
v3.15.12 added a .fixed modifier (PR #4773) that tells Floating UI to use a fixed positioning strategy instead:
<div x-data="{ open: false }" class="overflow-y-auto max-h-96">
<button x-ref="rowAction" @click="open = !open">Actions</button>
<div x-show="open" x-anchor.fixed="$refs.rowAction" @click.outside="open = false">
<ul>
<li><a href="#">Edit</a></li>
<li><a href="#">Delete</a></li>
</ul>
</div>
</div>
With .fixed, the menu escapes the scroll container’s clipping instead of getting cut off at its edge. One catch worth knowing before you burn an hour debugging it: a transform, filter, perspective, backdrop-filter, will-change, or contain property on any ancestor element creates a new containing block for fixed-position descendants, per the CSS spec. If one of your ancestors sets any of those (Tailwind’s transform utility classes are an easy way to do this by accident), .fixed will behave like .absolute again and stay clipped. If .fixed seems to do nothing, check for a transformed ancestor before assuming the modifier is broken.
.noflip for when auto-flipping fights your layout
By default, x-anchor flips the anchored element’s position when there isn’t enough viewport room to render it in the requested direction, a bottom-anchored dropdown near the bottom of the screen will flip to render above its reference element instead. That is usually the right call, but it actively fights you in a few specific layouts: large mega-menus, popovers teleported to <body> where the flip calculation loses context of the original trigger’s visual position, or any UI where you’d rather let content scroll than have it jump to the opposite side.
v3.15.11 added .noflip (PR #4791) to opt out of that behavior entirely:
<div x-show="open" x-anchor.bottom-start.noflip="$refs.trigger">
Dropdown content
</div>
Combine it with .fixed when you need both behaviors at once: x-anchor.fixed.noflip="$refs.trigger".
Dynamic references: x-anchor can now follow a moving target
Before v3.15.9, x-anchor’s reference element was effectively fixed at initialization. If the thing you wanted to anchor to changed at runtime, say, a tooltip that needs to follow whichever table cell is currently focused, you had to tear down and rebuild the directive. PR #4735 added support for a dynamic reference, so the expression passed to x-anchor can now be reactive:
<div x-data="{ activeCell: null }">
<table>
<tr>
<td @focus="activeCell = $el" tabindex="0">Row 1</td>
<td @focus="activeCell = $el" tabindex="0">Row 2</td>
</tr>
</table>
<div x-show="activeCell" x-anchor="activeCell">
Cell details
</div>
</div>
As activeCell changes, the anchored tooltip repositions to track it, no manual re-initialization needed.
The @teleport fix that matters most for Livewire
The change most worth knowing about if you run Livewire is buried in the v3.15.12 changelog: “Fix @teleport leaking detached nodes on every Livewire render” (PR #4822). x-teleport moves a DOM node to a different location in the document, commonly used to render a modal or dropdown at <body> level so it isn’t clipped by a parent’s overflow or z-index stacking context. Before this fix, every time Livewire morphed a component containing a teleported element, Alpine detached the old teleported node from the DOM tree but never released Alpine’s internal references to it. Alpine’s reactivity system kept the detached node, and everything it was bound to, alive in memory.
For a page that renders once, this is invisible. For a Livewire component that re-renders frequently, a polling dashboard, a live search results panel, a component with wire:poll on it, this compounds every single render. Nothing crashes immediately, but memory climbs steadily on any long-lived page session, and it is the kind of leak that only shows up as “the tab gets sluggish after twenty minutes,” which is miserable to trace back to a teleport directive three components deep.
If your app combines Livewire with teleported Alpine components (a common pattern for global modals triggered from deeply nested components) and you’ve been on an Alpine version older than v3.15.12, this alone is a good reason to update. There’s no code change required on your end. Confirm your installed version and bump it:
npm list alpinejs
npm install alpinejs@latest
Or if you’re pulling from the CDN, make sure you’re pinned to 3.15.12 or later rather than a loose 3.x.x range that might resolve to a cached older build:
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
A few smaller fixes worth knowing about
Two other changes in this run of releases are worth a mention even though they aren’t x-anchor specific:
x-forno longer crashes on null-prototype objects. If you’ve usedObject.groupBy()(native in modern JS engines) to reshape data before looping over it withx-for, earlier Alpine versions would throw because the grouped result objects don’t have the standardObjectprototype. Fixed in v3.15.12 (PR #4825).- CSP evaluator short-circuiting. If you run Alpine in CSP-build mode (common if your Laravel app enforces a strict Content-Security-Policy without
unsafe-eval),&&and||expressions weren’t short-circuiting correctly, meaning both sides of a conditional could evaluate even when they shouldn’t. Fixed in v3.15.12 (PR #4823).
Where this leaves things
None of this is a rewrite, Alpine is still on the 3.15.x line with no v4 announcement as of this writing, and the core mental model (small directives, no build step, 7 kB gzipped) hasn’t moved. What has moved is exactly the set of rough edges you’d expect a mature plugin to sand down after a few years in production across thousands of Livewire and Laravel apps: positioning inside scroll containers, unwanted flip behavior, anchors that need to track a moving element, and a leak that only announces itself after a component has been alive and re-rendering for a while.
If you maintain a Laravel or Symfony app with Alpine and Livewire in the stack, check your package.json (or CDN pin) against 3.15.12 today. The teleport fix in particular is the kind of bug you don’t notice until a user complains that “the site gets slow if I leave it open,” and by then it’s a much longer debugging session than a version bump would have been.
Sources
- Alpine.js Releases — github.com/alpinejs/alpine
- Anchor Plugin documentation — alpinejs.dev
- Add
x-anchor.noflipmodifier, PR #4791 — github.com - Add
x-anchor.noflipmodifier discussion — github.com - Fix
@teleportleaking detached nodes on every Livewire render, PR #4822 — github.com - Add .fixed positioning modifier to x-anchor, PR #4773 — github.com
- feat(x-anchor): allow dynamic reference to be used with x-anchor, PR #4735 — github.com