Add .fixed positioning modifier to x-anchor#4773
Merged
calebporzio merged 7 commits intoalpinejs:mainfrom Apr 7, 2026
Merged
Conversation
# 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>
Collaborator
|
thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a new
.fixedmodifier to thex-anchordirective that instructs Floating UI to use itsposition: fixedstrategy instead of the defaultposition: absolute.Why
x-anchorcurrently hardcodesposition: absolutewhen applying styles. This works for most cases, but it breaks down when:overflow: hidden/overflow: clip/overflow: auto— the popover gets clipped along with the parent.Floating UI natively supports a
fixedstrategy for exactly this — Alpine just didn't have a way to pass it through.How
getOptions()returns a newstrategy: 'fixed' | 'absolute'field, derived from whether the.fixedmodifier is present. Defaults to'absolute', so existingx-anchorusage is byte-identical in behavior.computePosition()receives thestrategyoption.setStyles()takes the strategy as an argument (defaulting to'absolute') and applies it as the CSSpositionvalue.strategyfromgetOptions(modifiers)and forwards it tosetStyles(). This is necessary for correctness: when.fixedis active, the(x, y)stored on_x_anchorare viewport-relative, so the pre-existing branch that calledsetStyles(el, x, y)would have applied viewport coordinates underposition: absoluteafter a morph, placing the popover in the wrong spot. This PR implicitly fixes that.Backward compatibility
Fully additive.
strategydefaults to'absolute'in every code path, andsetStylesdefaults its parameter to'absolute'too. Any existingx-anchorexpression without the.fixedmodifier produces identical output before and after this change.Documentation
transform,filter,perspective,backdrop-filter,will-change, orcontain, the CSS spec says it becomes a new containing block forposition: fixeddescendants, which is the Struggling with x-class binding #1 footgun when.fixedappears to do nothing..no-stylewith.fixedcorrectly (the caller must applyposition: fixedthemselves, because$anchor.x/yare returned in the coordinate space of whichever strategy is active).Tests
New Cypress tests in
tests/cypress/integration/plugins/anchor.spec.js:.fixedappliesposition: fixed.fixedkeeps the defaultposition: absolute(backward-compat regression guard).bottom.fixed===.fixed.bottom).fixedstill correctly anchors when the reference lives inside aposition: relative; overflow: hiddenclipping container.fixedstrategy survives anAlpine.morph()pass — verifies the clone-branchsetStylescall now correctly re-applies the strategyAll existing anchor tests continue to pass unchanged.
Scope
The
.match-widthmodifier 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.