Skip to content

Add .fixed positioning modifier to x-anchor#4773

Merged
calebporzio merged 7 commits intoalpinejs:mainfrom
nicolagianelli:fixed-anchor
Apr 7, 2026
Merged

Add .fixed positioning modifier to x-anchor#4773
calebporzio merged 7 commits intoalpinejs:mainfrom
nicolagianelli:fixed-anchor

Conversation

@nicolagianelli
Copy link
Copy Markdown
Contributor

@nicolagianelli nicolagianelli commented Mar 17, 2026

What

Adds a new .fixed modifier to the x-anchor directive that instructs Floating UI to use its position: fixed strategy instead of the default position: absolute.

<div x-show="open" x-anchor.fixed="$refs.button">
    Dropdown content
</div>

Why

x-anchor currently hardcodes position: absolute when applying styles. This works for most cases, but it breaks down when:

  • The reference element lives inside a container with overflow: hidden / overflow: clip / overflow: auto — the popover gets clipped along with the parent.
  • You want the popover to stay pinned to the viewport regardless of scroll.

Floating UI natively supports a fixed strategy for exactly this — Alpine just didn't have a way to pass it through.

How

  • getOptions() returns a new strategy: 'fixed' | 'absolute' field, derived from whether the .fixed modifier is present. Defaults to 'absolute', so existing x-anchor usage is byte-identical in behavior.
  • computePosition() receives the strategy option.
  • setStyles() takes the strategy as an argument (defaulting to 'absolute') and applies it as the CSS position value.
  • The clone/morph branch also reads strategy from getOptions(modifiers) and forwards it to setStyles(). This is necessary for correctness: when .fixed is active, the (x, y) stored on _x_anchor are viewport-relative, so the pre-existing branch that called setStyles(el, x, y) would have applied viewport coordinates under position: absolute after a morph, placing the popover in the wrong spot. This PR implicitly fixes that.

Backward compatibility

Fully additive. strategy defaults to 'absolute' in every code path, and setStyles defaults its parameter to 'absolute' too. Any existing x-anchor expression without the .fixed modifier produces identical output before and after this change.

Documentation

  • Tightens the Fixed positioning section's lede to describe when absolute falls short.
  • Adds a prominent heads-up about the transformed-ancestor gotcha — if an ancestor has transform, filter, perspective, backdrop-filter, will-change, or contain, the CSS spec says it becomes a new containing block for position: fixed descendants, which is the Struggling with x-class binding #1 footgun when .fixed appears to do nothing.
  • Adds a note under Manual styling explaining how to combine .no-style with .fixed correctly (the caller must apply position: fixed themselves, because $anchor.x/y are returned in the coordinate space of whichever strategy is active).

Tests

New Cypress tests in tests/cypress/integration/plugins/anchor.spec.js:

  • .fixed applies position: fixed
  • absence of .fixed keeps the default position: absolute (backward-compat regression guard)
  • modifier order is interchangeable (.bottom.fixed === .fixed.bottom)
  • .fixed still correctly anchors when the reference lives inside a position: relative; overflow: hidden clipping container
  • .fixed strategy survives an Alpine.morph() pass — verifies the clone-branch setStyles call now correctly re-applies the strategy

All existing anchor tests continue to pass unchanged.

Scope

The .match-width modifier that was present in an earlier version of this PR has been removed. It is orthogonal to .fixed (works with either positioning strategy) and deserves its own PR where its inline-width override behavior and bundle cost can be reviewed on their own merits. A follow-up PR will reintroduce it with docs and tests.


Original work by @nicolagianelli. Scope cleanup, tests, and docs expansion by @calebporzio.

nicolagianelli and others added 7 commits March 17, 2026 09:32
# Conflicts:
#	packages/anchor/src/index.js
This PR was originally scoped to add the .fixed positioning modifier.
A second, unrelated modifier (.match-width) was added in a follow-up
commit but was never mentioned in the PR description. Since .match-width
is orthogonal to .fixed (it works with either strategy) it deserves its
own PR where it can be reviewed on its own merits — specifically the
inline-width override behavior, the bundle cost of importing Floating
UI's size middleware, and potential future extensions like match-height
or a configurable property.

This revert keeps the .fixed work intact and drops .match-width back to
parity with main. A follow-up PR will reintroduce .match-width with
docs and tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers:

- .fixed applies position: fixed to the anchored element
- absence of .fixed keeps the default position: absolute (regression
  guard for backward compatibility)
- modifier order is interchangeable (.bottom.fixed === .fixed.bottom)
- .fixed still correctly anchors when the reference element lives
  inside a position:relative; overflow:hidden container
- .fixed strategy survives an Alpine.morph() pass, verifying the
  clone-branch setStyles() now correctly re-applies the strategy
  instead of the hardcoded 'absolute' that existed before this PR

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Tighten the Fixed positioning lede to actually describe when the
  default absolute strategy falls short
- Add a prominent heads-up about the transformed-ancestor gotcha:
  transform/filter/perspective/backdrop-filter/will-change/contain
  on an ancestor creates a new containing block for position:fixed,
  which is the alpinejs#1 footgun when .fixed appears to do nothing
- Add a note under Manual styling explaining how to combine
  .no-style with .fixed correctly ($anchor.x/y are returned in the
  coordinate space of whichever strategy is active, so the user must
  apply position: fixed themselves)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@calebporzio calebporzio changed the title Add fixed positioning option to x-anchor directive Add .fixed positioning modifier to x-anchor Apr 7, 2026
@calebporzio calebporzio merged commit 6b0a4ee into alpinejs:main Apr 7, 2026
1 check passed
@calebporzio
Copy link
Copy Markdown
Collaborator

thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants