1. Introduction
CSS absolute positioning allows authors to place boxes anywhere on the page, without regard to the layout of other boxes besides their containing block. This flexibility can be very useful, but also very limiting—often you want to position relative to some other box. Anchor positioning (via the position-anchor and position-area properties and/or the anchor functions anchor() and anchor-size()) allows authors to achieve this, “anchoring” an absolutely positioned box to one or more other boxes on the page (its anchor references, while also allowing them to try several possible positions to find the “best” one that avoids overlap/overflow.
.anchor{ anchor-name : --tooltip; } .tooltip{ /* Fixpos means we don’t need to worry about containing block relationships; the tooltip can live anywhere in the DOM. */ position: fixed; /* All the anchoring behavior will default to referring to the --tooltip anchor. */ position-anchor: --tooltip; /* Align the tooltip’s bottom to the top of the anchor; this also defaults to horizontally center-aligning the tooltip and the anchor (in horizontal writing modes). */ position-area: block-start; /* Automatically swap if this overflows the window so the tooltip’s top aligns to the anchor’s bottom instead. */ position-try: flip-block; /* Prevent getting too wide */ max-inline-size:20 em ; }
1.1. Value Definitions
This specification follows the CSS property definition conventions from [CSS2] using the value definition syntax from [CSS-VALUES-3]. Value types not defined in this specification are defined in CSS Values & Units [CSS-VALUES-3]. Combination with other CSS modules may expand the definitions of these value types.
In addition to the property-specific values listed in their definitions, all properties defined in this specification also accept the CSS-wide keywords as their property value. For readability they have not been repeated explicitly.
Like most operations in CSS besides selector matching, features in this specification operate over the flattened element tree.
2. Determining the Anchor
2.1. Creating an Anchor: the anchor-name property
Name: | anchor-name |
---|---|
Value: | none | <dashed-ident># |
Initial: | none |
Applies to: | all elements that generate a principal box |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
The anchor-name property declares that an element is an anchor element, whose principal box is an anchor box, and gives it a list of anchor names to be targeted by. Values are defined as follows:
- none
-
The property has no effect.
- <dashed-ident>#
-
If the element generates a principal box, the element is an anchor element, with a list of anchor names as specified. Each anchor name is a tree-scoped name.
Otherwise, the property has no effect.
Anchor names do not need to be unique. Not all elements are capable of being the target anchor element of a given box. Thus a name can be reused in multiple places if the usages are scoped appropriately.
Note: If multiple elements share an anchor name and are all visible to a given positioned box, the target anchor element will be the last one in DOM order. The anchor-scope property can be used to further limit what names are visible to a given referencing box.
Anchor names are not scoped by containment by default; even if an element has style or layout containment (or any similar sort of containment), the anchor names of its descendants are visible to elements elsewhere in the page.
Note: While an element is in the skipped contents of another element (due to content-visibility: hidden, for instance), it’s not an acceptable anchor element, effectively acting as if it had no names.
2.1.1. Implicit Anchor Elements
Some specifications can define that, in certain circumstances, a particular element is an implicit anchor element for another element.
TODO: Fill in an example new popover-related details (once that finally lands in the HTML spec).
Implicit anchor elements can be referenced with the auto keyword in position-anchor, or by omitting the anchor reference in anchor functions.
Pseudo-elements have the same implicit anchor element as their originating element, unless otherwise specified.
Note: Without this, these pseudo-elements, which are often inaccessible by other specifications, cannot be positioned with implicit anchor elements.
2.1.2. The Anchor’s Position
Several features of this specification refer to the position and size of an anchor box. The anchor box's position and size is determined after layout, and for these purposes includes position-based adjustments (such as position: relative or position: sticky).
Post-layout effects, such as transform, do not affect the anchor box’s position.
Note: Allowing an anchor to opt into including the effects of transform or similar properties might be allowed in the future.
2.2. Scoping Anchor Names: the anchor-scope property
Name: | anchor-scope |
---|---|
Value: | none | all | <dashed-ident># |
Initial: | none |
Applies to: | all elements |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
This property scopes the specified anchor names, and lookups for these anchor names, to this element’s subtree. See § 2 Determining the Anchor.
Values have the following meanings:
- none
- No changes in anchor name scope.
- all
- Specifies that all anchor names defined by this element or its descendants—whose scope is not already limited by a descendant using anchor-scope—to be in scope only for this element’s descendants; and limits descendants to only match anchor names to anchor elements within this subtree.
- <dashed-ident>
- Specifies that a matching anchor name defined by this element or its descendants—whose scope is not already limited by a descendant using anchor-scope—to be in scope only for this element’s descendants; and limits descendants to only match these anchor names to anchor elements within this subtree.
This property has no effect on implicit anchor elements.
li{ anchor-name : --list-item; anchor-scope : --list-item; } li .positioned{ position : absolute; position-anchor : --list-item; position-area : inline-start; }
Without anchor-scope,
all of the li
elements would be visible
to all of the positioned elements,
and so they’d all positioned themselves relative to the final li
,
stacking up on top of each other.
2.3. Finding an Anchor
Several things in this specification find a target anchor element, given an anchor specifier, which is either a <dashed-ident> (and a tree-scoped reference) that should match an anchor-name value elsewhere on the page, or the keyword auto, or nothing (a missing specifier).
Note: The general rule captured by these conditions is that an element can only be a positioned box’s target anchor element if its own box is fully laid out before the positioned box that wants to reference it is laid out. CSS’s layout rules provide some useful guarantees about this, depending on the anchor and positioned box’s relationship with each other and their containing blocks. The list of conditions below exactly rephrases the stacking context rules into just what’s relevant for this purpose, ensuring there is no possibly circularity in anchor positioning.
-
If anchor spec was not passed, return the default anchor element if it exists, otherwise return nothing.
-
If anchor spec is auto:
-
If the Popover API defines an implicit anchor element for query el which is an acceptable anchor element for query el, return that element.
-
Otherwise, return nothing.
Note: Future APIs might also define implicit anchor elements. When they do, they’ll be explicitly handled in this algorithm, to ensure coordination.
-
-
Otherwise, anchor spec is a <dashed-ident>. Return the last element el in tree order that satisfies the following conditions:
-
el is an anchor element with an anchor name of anchor spec.
-
el’s anchor name and anchor spec are both associated with the same tree root.
Note: The anchor name is a tree-scoped name, while anchor spec is a tree-scoped reference.
-
el is an acceptable anchor element for query el.
If no element satisfies these conditions, return nothing.
Note: anchor-scope can restrict the visibility of certain anchor names, which can affect what elements can be anchor elements for a given lookup.
-
Note: An anchor-name defined by styles in one shadow tree won’t be seen by anchor functions in styles in a different shadow tree, preserving encapsulation. However, elements in different shadow trees can still anchor to each other, so long as both the anchor-name and anchor function come from styles in the same tree, such as by using ::part() to style an element inside a shadow. (Implicit anchor elements also aren’t intrinsically limited to a single tree, but the details of that will depend on the API assigning them.)
-
possible anchor is either an element or a fully styleable tree-abiding pseudo-element.
-
possible anchor is in scope for positioned el, per the effects of anchor-scope on positioned el or its ancestors.
-
possible anchor is laid out strictly before positioned el, aka one of the following is true:
-
positioned el is in a higher top layer than possible anchor
-
Both elements are in the same top layer but have different containing blocks, and positioned el’s containing block is an ancestor of possible anchor’s containing block in the containing block chain, aka one of the following:
-
positioned el’s containing block is the viewport, and possible anchor’s containing block isn’t.
-
positioned el’s containing block is the initial containing block, and possible anchor’s containing block is generated by an element,
-
both elements' containing blocks are generated by elements, and positioned el’s containing block is an ancestor in the flat tree to that of possible anchor’s containing block.
-
-
Both elements are in the same top layer and have the same containing block, and are both absolutely positioned, and possible anchor is earlier in flat tree order than positioned el.
-
Both elements are in the same top layer and have the same containing block, but possible anchor isn’t absolutely positioned.
-
-
If possible anchor is in the skipped contents of another element, then positioned el is in the skipped contents of that same element.
Note: In other words, positioned el can anchor to possible anchor if they’re both in the same skipped "leaf", but it can’t anchor "across" leafs. This means skipping an element that contains both of them won’t suddenly cause the positioned el to move to another anchor, but still prevents positioned elements elsewhere in the page from anchoring to the skipped element.
2.4. Default Anchors: the position-anchor property
Name: | position-anchor |
---|---|
Value: | auto | <anchor-name> |
Initial: | auto |
Applies to: | absolutely positioned boxes |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
The position-anchor property specifies the default anchor element, which is used by position-area, position-try, and (by default) all anchor functions applied to this element. position-anchor is a reset-only sub-property of position.
- auto
-
Use the implicit anchor element if it exists; otherwise the box has no default anchor element.
- <anchor-name>
-
The target anchor element selected by the specified <anchor-name> is the box’s default anchor element.
The principal box of the default anchor element is the box’s default anchor box.
.anchored{ position : absolute; top : calc ( .5 em +anchor ( outside)); /* Since no anchor name was specified, this automatically refers to the default anchor box. */ } .foo.anchored{ position-anchor : --foo; } .bar.anchored{ position-anchor : --bar; }
2.5. Anchor Relevance
When determining whether an element el is relevant to the user, if a descendant of el is a target anchor element for a positioned box (which itself is not skipped and whose containing block is not el or a descendant of el), then el must be considered relevant to the user.
Note: This means that, for example, an anchor in a content-visibility: auto subtree will prevent its subtree from skipping its contents as long as the positioned box relying on it is also not skipped. (Unless the anchor and the positioned box are both under the same content-visibility: auto element; they can’t cyclicly keep each other visible.)
3. Anchor-Based Positioning
An absolutely positioned box can position itself relative to one or more anchor boxes on the page.
The position-area property offers a convenient grid-based concept for positioning relative to the default anchor box; for more complex positioning or positioning relative to multiple boxes, the anchor() function can be used in the inset properties to explicitly refer to edges of an anchor box.
3.1. The position-area Property
Name: | position-area |
---|---|
Value: | none | <position-area> |
Initial: | none |
Applies to: | positioned boxes with a default anchor box |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | TBD |
Most common use-cases of anchor positioning are only concerned with the edges of the positioned box’s containing block and the edges of the default anchor box. These lines can be thought of as defining a 3×3 grid; position-area lets you easily specify what area of this position-area grid to lay out the positioned box in. Its syntax is:
<position-area> = [ [ left | center | right | span-left | span-right | x-start | x-end | span-x-start | span-x-end | x-self-start | x-self-end | span-x-self-start | span-x-self-end | span-all ] || [ top | center | bottom | span-top | span-bottom | y-start | y-end | span-y-start | span-y-end | y-self-start | y-self-end | span-y-self-start | span-y-self-end | span-all ] | [ block-start | center | block-end | span-block-start | span-block-end | span-all ] || [ inline-start | center | inline-end | span-inline-start | span-inline-end | span-all ] | [ self-block-start | center | self-block-end | span-self-block-start | span-self-block-end | span-all ] || [ self-inline-start | center | self-inline-end | span-self-inline-start | span-self-inline-end | span-all ] | [ start | center | end | span-start | span-end | span-all ]{1,2} | [ self-start | center | self-end | span-self-start | span-self-end | span-all ]{1,2} ]
- none
-
The property has no effect.
- <position-area>
-
If the box does not have a default anchor box, or is not an absolutely positioned box, this value has no effect.
Otherwise, it has the following effects:
-
The property selects a region of the position-area grid, and makes that the box’s containing block. See § 3.1.1 Resolving <position-area>s for details.
Note: This means that the inset properties specify offsets from the position-area, and some property values, like max-height: 100%, will be relative to the position-area as well.
-
The normal value for the self-alignment properties behaves as either start, end, or anchor-center, depending on the positioning of the region, to give a good default alignment for the positioned box. Again, see § 3.1.1 Resolving <position-area>s for details.
-
Any auto inset properties resolve to 0.
-
The box is not considered to be in a legacy alignment mode in either axis.
-
This behavior makes it more likely that positioned boxes remain visible and within their intended bounds, even when their containing block ends up smaller than anticipated.
For example, a position-area: bottom span-right value lets the positioned box stretch from its anchor’s left edge to its containing block’s right edge, and left-aligns it in that space by default. But if the positioned box is larger than that space (such as if the anchor is very close to the right edge of the screen), it will shift leftwards to stay visible.
3.1.1. Resolving <position-area>s
The position-area grid is a 3×3 grid, composed of four grid lines in each axis. In order (and using the writing mode of the containing block):
-
the start edge of the box’s pre-modification containing block, or the start edge of the default anchor box if that is more start-ward
-
the start edge of the default anchor box
-
the end edge of the default anchor box
-
the end edge of the box’s pre-modification containing block, or the end edge of the default anchor box if that is more end-ward.
A <position-area> selects a region of this grid by specifying the rows and columns the region occupies, with each of the two keywords specifying one of them:
- start, end, self-start, self-end
- top, bottom, left, right
- y-start, y-end, y-self-start, y-self-end
- x-start, x-end, x-self-start, x-self-end
- block-start, block-end, block-self-start, block-self-end
- inline-start, inline-end, inline-self-start, inline-self-end
- center
- top, bottom, left, right
-
The single corresponding row or column, depending on which axis this keyword is specifying.
Like in anchor(), the plain logical keywords (start, end, etc) refer to the writing mode of the box’s containing block. The x-start/etc determine their direction in the same way, but in the specified physical axis.
The self-* logical keywords (self-start, x-self-end, etc) are identical, but refer to the box’s own writing mode.
- span-start, span-end
- span-top, span-bottom
- span-y-start, span-y-end
- span-x-start, span-x-end
- span-block-start, span-block-end
- span-inline-start, span-inline-end
- span-top, span-bottom
-
Two rows or columns, depending on which axis this keyword is specifying: the center row/column, and the row/column corresponding to the other half of the keyword as per the single-track keywords.
(For example, span-top spans the first two rows—the center row and the top row.)
- span-all
-
All three rows or columns, depending on which axis this keyword is specifying.
Some keywords are ambiguous about what axis they refer to: center, span-all, and the start/etc keywords that don’t specify the block or inline axis explicitly. If the other keyword is unambiguous about its axis, then the ambiguous keyword is referring to the opposite axis. (For example, in block-start center, the center keyword is referring to the inline axis.) If both keywords are ambiguous, however, then the first refers to the block axis of the box’s containing block, and the second to the inline axis. (For example, span-all start is equivalent to span-all inline-start.)
If only a single keyword is given, it behaves as if the second keyword is span-all if the given keyword is unambigous about its axis; otherwise, it behaves as if the given keyword was repeated. (For example, top is equivalent to top span-all, but center is equivalent to center center.)
The <position-area> also implies a default self-alignment, which will be used if the self-alignment property on the box is normal:
-
If the only the center region in an axis is selected, or all three regions are selected, the default alignment in that axis is anchor-center.
-
Otherwise, the default alignment in that axis is toward the non-specified side region: if it’s specifying the "start" region of its axis, the default alignment in that axis is end; etc.
Note: When the default anchor box is partially or completely outside of the pre-modified containing block, some of the position-area grid’s rows or columns can be zero-sized.
3.2. The anchor() Function
An absolutely positioned box can use the anchor() function as a value in its inset properties to refer to the position of one or more anchor boxes. The anchor() function resolves to a <length>. It is only valid to be used in the inset properties.
Name: | top, left, right, bottom |
---|---|
New values: | <anchor()> |
<anchor()> = anchor( <anchor-name>? && <anchor-side>, <length-percentage>? ) <anchor-name> = <dashed-ident> <anchor-side> = inside | outside | top | left | right | bottom | start | end | self-start | self-end | <percentage> | center
The anchor() function has three arguments:
-
the <anchor-name> value specifies how to find the anchor element it will be drawing positioning information from. Its possible values are:
- <dashed-ident>
-
Specifies the anchor name it will look for. This name is a tree-scoped reference.
- omitted
-
Selects the default anchor element defined for the box, if possible.
See target anchor element for details.
-
the <anchor-side> value refers to the position of the corresponding side of the target anchor element. Its possible values are:
- inside
- outside
-
Resolves to one of the anchor box’s sides, depending on which inset property it’s used in. inside refers to the same side as the inset property (attaching the positioned box to the "inside" of the anchor box), while outside refers to the opposite.
- top
- right
- bottom
- left
- right
-
Refers to the specified side of the anchor box.
Note: These are only usable in the inset properties in the matching axis. For example, left is usable in left, right, or the logical inset properties that refer to the horizontal axis.
- start
- end
- self-start
- self-end
- end
-
Refers to one of the sides of the anchor box in the same axis as the inset property it’s used in, by resolving the keyword against the writing mode of either the positioned box (for self-start and self-end) or the positioned box’s containing block (for start and end).
- <percentage>
- center
-
Refers to a position a corresponding percentage between the start and end sides, with 0% being equivalent to start and 100% being equivalent to end.
center is equivalent to 50%.
- inside
-
the optional <length-percentage> final argument is a fallback value, specifying what the function should resolve to if it’s an invalid anchor function.
An anchor() function representing a valid anchor function resolves at computed value time (using style & layout interleaving) to the <length> that would align the edge of the positioned boxes' inset-modified containing block corresponding to the property the function appears in with the specified border edge of the target anchor element, assuming that all scroll containers between the target anchor element and the positioned box’s containing block are scrolled to their initial scroll position (but see § 3.4 Taking Scroll Into Account).
Note: This means that transitions or animations of a property using an anchor function will work "as expected" for all sorts of possible changes: the anchor box moving, anchor elements being added or removed from the document, the anchor-name property being changed on anchors, etc.
If the target anchor element is fragmented, the axis-aligned bounding rectangle of the fragments' border boxes is used instead.
.bar
element’s block-start edge
with the --foo anchor’s block-start edge.
On the other hand,
in .bar { inset-block-end: anchor(--foo block-start); },
it will instead resolve to the length
that’ll line up the .bar
element’s block-end edge
with the --foo anchor’s block-start edge.
Since inset-block-start and inset-block-end values specify insets from different edges (the block-start and block-end of the element’s containing block, respectively), the same anchor() will usually resolve to different lengths in each.
For example, the following will set up the element so that its inset-modified containing block is centered on the anchor box and as wide as possible without overflowing the containing block:
.centered-message{ position : fixed; max-width : max-content; justify-self : center; --center : anchor ( --x50 % ); --half-distance : min ( abs ( 0 % -var ( --center)), abs ( 100 % -var ( --center)) ); left : calc ( var ( --center) -var ( --half-distance)); right : calc ( var ( --center) -var ( --half-distance)); bottom : anchor ( --x top); }
This might be appropriate for an error message
on an input
element,
for example,
as the centering will make it easier to discover
which input is being referred to.
3.3. Centering on the Anchor: the anchor-center value
Name: | justify-self, align-self, justify-items, align-items |
---|---|
New values: | anchor-center |
The self-alignment properties allow an absolutely positioned box to align itself within the inset-modified containing block. The existing values, plus carefully chosen inset properties, are usually enough for useful alignment, but a common case for anchored positioning—centering over the anchor box—requires careful and somewhat complex set-up to achieve.
The new anchor-center value makes this case extremely simple: if the positioned box has a default anchor box, then it is centered (insofar as possible) over the default anchor box in the relevant axis.
Additionally, any auto inset properties resolve to 0.
If the box is not absolutely positioned, or does not have a default anchor box, this value behaves as center and has no additional effect on how inset properties resolve.
Note: Similar to position-area, when using anchor-center, if the anchor is too close to the edge of the box’s original containing block, by default it will “shift” from being purely centered, in order to remain within the original containing block. See CSS Box Alignment 3 § 4.4 Overflow Alignment: the safe and unsafe keywords and scroll safety limits for more details.
3.4. Taking Scroll Into Account
For performance reasons, implementations usually perform scrolling on a separate scrolling/"compositing" thread, which has very limited capabilities (simple movement/transforms/etc., but no layout or similar expensive operations) and thus can be relied upon to respond to scrolling fast enough to be considered "instant" to human perception.
If scrolling just causes an anchor-positioned element to move, there is in theory no issue; the movement can be performed on the scrolling thread so the positioned element moves smoothly with the scrolling content. However, anchor positioning allows an element to make the positions of its own opposite edges depend on things in different scrolling contexts, which means scrolling could move just one edge and cause a size change, and thus perform layout. This can’t be performed on the scrolling thread!
To compensate for this, while still allowing as much freedom to anchor to various elements as possible, anchor positioning uses a combination of remembered scroll offsets and compensating for scroll.
-
When a positioned element is first displayed, or when it changes fallbacks, its position is correctly calculated according to the up-to-date position of all anchor references.
If these anchor references are in a different scroll context, their total scroll offsets are memorized, and layout will continue using those memorized offsets, even if those elements are scrolled later. (Only the scroll offsets are memorized; their actual laid-out positions are freshly calculated each time and remain accurate.) They’ll only recalculate if the positioned element stops being displayed and starts again, or changes fallbacks.
-
The one exception to this is the default anchor element; if it’s scrolled away from its remembered scroll offset, the positioned element moves with it. Because this is *purely* a shift in position, the positioned element can’t change size or otherwise require layout in response.
The end result is that anchor positioning should generally "just work", regardless of what the element is anchoring to, but it might be limited in how it can respond to scrolling.
An anchor recalculation point also occurs for an element when determining position fallback styles for that element; if it changes fallback styles as a result, it uses the result of the anchor recalculation point associated with the chosen set of fallback styles.
When an anchor recalculation point occurs for an element abspos, then for every element anchor referenced by one of abspos’s anchor references, it associates a remembered scroll offset equal to the current sum of the scroll offsets of all scroll container ancestors of anchor, up to but not including abspos’s containing block. The remembered scroll offset also accounts for other scroll-dependent positioning changes, such as position: sticky. If abspos has a default anchor element, it always calculates a remembered scroll offset for it, even if abspos doesn’t actually have an anchor reference to it.
All anchor references are calculated as if all scroll containers were at their initial scroll position, and then have their associated remembered scroll offset added to them.
Transforms have the same issue as scrolling, so Anchor Positioning similarly doesn’t pay attention to them normally. Can we go ahead and incorporate the effects of transforms here?
The above allows a positioned element to respond to the scroll positions of its anchor references once, but if any of them are scrolled, the positioned element will no longer appear to be anchored to them (tho it will continue to respond to their non-scrolling movement). While this problem can’t be solved in general, we can respond to the scrolling of one anchor reference; specifically, the default anchor element:
-
abspos has a default anchor box.
-
abspos has an anchor reference to its default anchor box or at least to something in the same scrolling context, aka at least one of:
-
abspos’s used self-alignment property value in that axis is anchor-center;
-
abspos has a non-none value for position-area
-
at least one anchor() function on abspos’s used inset properties in the axis refers to a target anchor element with the same nearest scroll container ancestor as abspos’s default anchor box.
-
Note: If abspos has a position options list, then whether it compensates for scroll in an axis is also affected by the applied fallback style.
abspos’s default scroll shift is a pair of lengths for the horizontal and vertical axises, respectively. Each length is calculated as:
-
If abspos is compensating for scroll in the axis, then the length is the difference between the remembered scroll offset of the default anchor element and what its current remembered scroll offset would be if it were recalculated.
-
Otherwise, the length is 0.
After layout has been performed for abspos, it is additionally shifted by the default scroll shift, as if affected by a transform (before any other transforms).
Define the precise timing of the snapshot: updated each frame, before style recalc.
Similar to remembered scroll offset, can we pay attention to transforms on the default anchor element?
Note: While remembered scroll offsets affect the value of anchor() functions, default scroll shift directly shifts the element, after determining the value of its inset properties, applying alignment, etc. This is usually indistinguishable, but cases like round(anchor(outside), 50px), which transform the default anchor element’s position in a non-linear fashion, will expose the difference in behavior.
3.5. Validity
An anchor() function is a valid anchor function only if all the following conditions are true:
-
It’s being used in an inset property on an absolutely positioned box.
-
If its <anchor-side> specifies a physical keyword, it’s being used in an inset property in that axis. (For example, left can only be used in left, right, or a logical inset property in the horizontal axis.)
-
The result of determining the target anchor element is not nothing when given the querying element as the element it’s used on, and the anchor specifier as the <anchor-name> value specified in the function.
If any of these conditions are false, the anchor() function resolves to its specified fallback value. If no fallback value is specified, it makes the declaration referencing it invalid at computed-value time.
3.6. Conditional Hiding: the position-visibility property
Name: | position-visibility |
---|---|
Value: | always | [ anchors-valid || anchors-visible || no-overflow ] |
Initial: | anchors-visible |
Applies to: | absolutely positioned boxes |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
There are some conditions in which it might not make sense to display an absolutely positioned box. This property allows such boxes to be made conditionally visible, depending on some commonly needed layout conditions.
- always
-
This property has no effect. (The box is displayed without regard for its anchors or its overflowing status.)
- anchors-valid
-
If any of the box’s required anchor references do not resolve to a target anchor element, the box is .
What is a required anchor reference? anchor() functions that don’t have a fallback value; the default anchor *sometimes*? Need more detail here.
Any anchors are missing, or all anchors are missing? I can see use-cases for either, potentially. Do we want to make a decision here, or make it controllable somehow?
- anchors-visible
-
If the box has a default anchor box but that anchor box is clipped by intervening boxes, this box is also .
- no-overflow
-
If the box overflows its inset-modified containing block even after applying position-try, then the box is .
Note: This means that if an abspos is next to its anchor in the DOM, for example, it’ll remain visible even if its default anchor is scrolled off, since it’s clipped by the same scroller anyway.
Make sure this definition of clipped is consistent with View Transitions, which wants a similar concept.
anchor is also considered clipped by intervening boxes if it is by position-visibility.
Note: This ensures that in a "chained anchor" situation, if the first abspos is hidden due to this property (due to its anchor being scrolled off), then another abspos using it as an anchor will also be hidden, rather than also floating in a nonsensical location.
A box that is strongly hidden acts as if it and all of its descendants were visibility: hidden, regardless of what their actual visibility value is.
3.7. Accessibility Implications
It’s important to remember that Anchor Positioning does not automatically establish any semantic relationship between a positioned box and any of its anchors, because it can be used in many different ways. Authors must not rely solely on a visual connection implied by the positioning to link elements together semantically; without additional help, the elements often have no meaningful DOM relationship, making them difficult or impossible to use in non-visual user agents, like screen readers.
Many features on the web platform, both existing and upcoming, allow establishing such connections explicitly, so that non-visual user agents can also benefit.
For example, the Popover API in HTML automatically links the invoker button to the popover element, including automatically adjusting tabbing order; it also establishes the invoker button as the implicit anchor element for the popover, making it easy to use Anchor Positioning as well.
In more general cases,
ARIA features such as
the aria-details
or aria-describedby
attributes
on an anchor element
can provide this information
in a slightly more manual fashion;
in concert with the role
attribute on the positioned element,
non-visual user agents
can tell their users about the relationship between the elements
and let them automatically navigate between them.
4. Anchor-Based Sizing
An absolutely positioned box can use the anchor-size() function in its sizing properties to refer to the size of one or more anchor boxes. The anchor-size() function resolves to a <length>. It is only valid to be used in the accepted @position-try properties.
4.1. The anchor-size() Function
Name: | width, height, min-width, min-height, max-width, max-height, top, left, right, bottom, margin-top, margin-left, margin-right, margin-bottom |
---|---|
New values: | <anchor-size()> |
anchor-size() = anchor-size( [ <anchor-name> || <anchor-size> ]? , <length-percentage>? ) <anchor-size> = width | height | block | inline | self-block | self-inline
The anchor-size() function is similar to anchor(), and takes the same arguments, save that the <anchor-side> keywords are replaced with <anchor-size>, referring to the distance between two opposing sides.
The physical <anchor-size> keywords (width and height) refer to the width and height, respectively, of the target anchor element. Unlike anchor(), there is no restriction on having to match axises; for example, width: anchor-size(--foo height); is valid.
The logical <anchor-size> keywords (block, inline, self-block, and self-inline) map to one of the physical keywords according to either the writing mode of the box (for self-block and self-inline) or the writing mode of the box’s containing block (for block and inline).
If the <anchor-size> keyword is omitted, it defaults to behaving as whatever keyword matches the axis of the property that anchor-size() is used in. (For example, width: anchor-size() is equivalent to width: anchor-size(width).)
An anchor-size() function representing a valid anchor-size function resolves at computed value time (via style & layout interleaving) to the <length> separating the relevant border edges (either left and right, or top and bottom, whichever is in the specified axis) of the target anchor element.
4.2. Validity
An anchor-size() function is a valid anchor-size function only if all the following conditions are true:
-
It’s being used in a sizing property, an inset property, or a margin property on an absolutely positioned box. (These are the accepted @position-try properties that allow lengths.)
-
There is a target anchor element for the box it’s used on, and the <anchor-name> value specified in the function.
If any of these conditions are false, the anchor-size() function resolves to its specified fallback value. If no fallback value is specified, it makes the declaration referencing it invalid at computed-value time.
5. Overflow Management
Anchor positioning, while powerful, can also be unpredictable. The anchor box might be anywhere on the page, so positioning a box in any particular fashion (such as above the anchor, or the right of the anchor) might result in the positioned box overflowing its containing block or being positioned partially off screen.
To ameliorate this, an absolutely positioned box can use the position-try-fallbacks property to refer to several variant sets of positioning/alignment properties (generated from the box’s existing styles, or specified in @position-try rules) that the UA can try if the box overflows its initial position. Each is applied to the box, one by one, and the first that doesn’t cause the box to overflow its containing block is taken as the winner.
position-try-order allows these options to additional be sorted by the available space they define, if it’s more important for the box to have as much space as possible rather than strictly follow some declared order.
5.1. Giving Fallback Options: the position-try-fallbacks property
Name: | position-try-fallbacks |
---|---|
Value: | none | [ [<dashed-ident> || <try-tactic>] | <'position-area'> ]# |
Initial: | none |
Applies to: | absolutely positioned boxes |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
This property provides a list of alternate positioning styles to try when the absolutely positioned box overflows its inset-modified containing block. This position options list is initially empty.
Each comma-separated entry in the list is a separate option: either the name of a @position-try block, or a <try-tactic> representing an automatic transformation of the box’s existing computed style.
Values have the following meanings:
- none
-
The property has no effect; the box’s position options list is empty.
- <dashed-ident>
-
If there is a @position-try rule with the given name, its associated position option is added to the position options list.
Otherwise, this value has no effect.
- <try-tactic>
-
Automatically creates a position option from the box’s computed style, by swapping due to a try-tactic according to the specified keyword, and adding the constructed position option to the box’s position options list. and adds it to the position options list.
<try-tactic> = flip-block || flip-inline || flip-start
- flip-block
-
swaps the values in the block axis (between, for example, margin-block-start and margin-block-end), essentially mirroring across an inline-axis line.
- flip-inline
-
swaps the values in the inline axis, essentially mirroring across a block-axis line.
- flip-start
-
swaps the values of the start properties with each other, and the end properties with each other (between, for example, margin-block-start and margin-inline-start), essentially mirroring across a diagonal drawn from the start-start corner to the end-end corner.
If multiple keywords are given, the transformations are composed in order to produce a single position option.
- <dashed-ident> || <try-tactic>
-
Combines the effects of the previous two options: if there is a @position-try rule with the given name, applies its position option to the base style, then transforms it according to the specified <try-tactic> and adds the result to the box’s position options list.
Otherwise, does nothing.
- <'position-area'>
-
Automatically creates a position option composed solely of a position-area property with the given value.
-
If directions are opposites along the same axis, they are “opposing”. Otherwise (when they are specifying different axises), they are “perpendicular”.
-
Determine the specified values of the accepted @position-try properties on el, and let styles be the result.
-
Substitute variables, env() functions, and similar arbitrary substitution functions in styles.
For env() functions, if the referenced environment variable is associated with a direction or axis (such as safe-area-inset-top), switch the referenced environment variable corresponding to directions.
For example, if top: env(safe-area-inset-top); is specified, and directions are up and left, the env() will resolve as if env(safe-area-inset-left) had been specified instead. (And then, in the next step, will actually swap into the left property.) -
Swap the values of the styles between the associated properties corresponding to directions.
For example, if "top" and "left" are being swapped, then the values of margin-top and margin-left are swapped, width and height are swapped, etc.Note: If the directions are opposites along the same axis, some properties (like width or align-self) wont' swap, since they’re associated with themselves across the two directions, but their values might be changed by the next step.
-
Modify the values of the properties as they swap to match the new directions, as follows:
-
For inset properties, change the specified side in anchor() functions to maintain the same relative relationship to the new direction that they had to the old.
If a <percentage> is used, and directions are opposing, change it to 100% minus the original percentage.
For example, if "top" and "left" are being swapped, then margin-top: anchor(bottom) will become margin-left: anchor(right).If "top" and "bottom" are being swapped, then margin-top: anchor(20%) will become margin-bottom: anchor(80%).
-
For sizing properties, change the specified axis in anchor-size() functions to maintain the same relative relationship to the new direction that they had to the old.
For example, if "top" and "left" are being swapped, then width: anchor-size(width) will become height: anchor-size(height). -
For the self-alignment properties, if directions are opposing, change the specified <self-position> (or left/right keywords), if any, to maintain the same relative relationship to the new direction that they had to the old.
For example, if "top" and "bottom" are being swapped, then align-self: start will become align-self: end.However, align-self: center will remain unchanged, as it has the same relationship to both directions.
Similarly, align-self: first baseline will remain unchanged, as it’s a <baseline-position> rather than a <self-position>.
-
For position-area, change the value so that the selected rows/columns of the position-area grid maintain the same relative relationship to the new direction that they had to the old.
For example, if "top" and "left" are being swapped, then position-area: left span-bottom will become position-area: top span-right.
-
-
Return styles.
5.2. Determining Fallback Order: the position-try-order property
Name: | position-try-order |
---|---|
Value: | normal | <try-size> |
Initial: | normal |
Applies to: | absolutely positioned boxes |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
This property specifies the order in which the position options list will be tried.
<try-size> = most-width | most-height | most-block-size | most-inline-size
- normal
-
Try the position options in the order specified by position-try-fallbacks.
- most-width
- most-height
- most-block-size
- most-inline-size
- most-height
-
For each entry in the position options list, apply that position option to the box, and find the specified inset-modified containing block size that results from those styles. Stably sort the position options list according to this size, with the largest coming first.
.anchor{ anchor-name : --foo; } .list{ position : fixed; position-anchor : --foo; position-area : block-end span-inline-end; align-self : start; position-try-fallbacks : --bottom-scrollable, flip-block, --top-scrollable; position-try-order : most-height; } @position-try --bottom-scrollable{ align-self : stretch; } @position-try --top-scrollable{ position-area : block-start span-inline-end; align-self : stretch; }
The flip-block auto-generated option and the --top-scrollable option will always find the same available height, since both of them stretch vertically from the top edge of the containing block to the top of the anchor, so they’ll retain their specified order. This causes the box to first try to align against the anchor at its natural height (using align-self: end, auto-reversed from the base styles), but if that also causes overflow, it’ll fall back to just filling the space and being scrollable instead.
5.3. The position-try Shorthand
Name: | position-try |
---|---|
Value: | <'position-try-order'>? <'position-try-fallbacks'> |
Initial: | see individual properties |
Applies to: | see individual properties |
Inherited: | see individual properties |
Percentages: | see individual properties |
Computed value: | see individual properties |
Animation type: | see individual properties |
Canonical order: | per grammar |
This shorthand sets both position-try-fallbacks and position-try-order. If <'position-try-order'> is omitted, it’s set to the property’s initial value.
5.4. The @position-try Rule
The @position-try rule defines a position option with a given name, specifying one or more sets of positioning properties that can be applied to a box via position-try-fallbacks,
The syntax of the @position-try rule is:
@position-try <dashed-ident> { <declaration-list> }
The <dashed-ident> specified in the prelude is the rule’s name. If multiple @position-try rules are declared with the same name, the last one in document order "wins".
The @position-try rule only accepts the following properties:
It is invalid to use !important on the properties in the <declaration-list>. Doing so causes the property it is used on to become invalid, but does not invalidate the @property-try rule as a whole.
All of the properties in a @position-try are applied to the box as part of the Position Fallback Origin, a new cascade origin that lies between the Author Origin and the Animation Origin.
Similar to the Animation Origin, use of the revert value acts as if the property was part of the Author Origin, so that it instead reverts back to the User Origin. (As with the Animation Origin, however, revert-layer has no special behavior and acts as specified.)
Note: The accepted @position-try properties are the smallest group of properties that affect just the size and position of the box itself, without otherwise changing its contents or styling. This significantly simplifies the implementation of position fallback while addressing the fundamental need to move an anchor-positioned box in response to available space. Since these rules override normal declarations in the Author Origin, this also limits the poor interactions of @position-try declarations with the normal cascading and inheritance of other properties. It is expected that a future extension to container queries will allow querying an element based on the position fallback it’s using, enabling the sort of conditional styling not allowed by this restricted list.
Note: If multiple boxes using different anchors want to use the same fallback positioning, just relative to their own anchor elements, omit the <anchor-name> in anchor() and specify each box’s anchor in position-anchor instead.
Note: The most common types of fallback positioning (putting the positioned box on one side of the anchor normally, but flipping to the opposite side if needed) can be done automatically with keywords in position-try-fallbacks, without using @position-try at all.
5.5. Applying Position Fallback
When a positioned box (shifted by its default scroll shift) overflows its inset-modified containing block, and has a non-empty position options list, it determines position fallback styles to attempt to find an option that avoids overflow.
These modified styles are applied to the element via interleaving, so they affect computed values (and can trigger transitions/etc) even though they depend on layout and used values.
-
With new styles inserted into the cascade in the position fallback origin, resolve the cascade, and perform enough layout to determine el’s used styles.
For the purpose of calculating these styles, a hypothetical anchor recalculation point is calculated, and the resulting hypothetical remembered scroll offsets are used to determine el’s styles.
-
Return el’s used styles.
-
Let current styles be the current used styles of abspos (which might be the result of earlier fallback).
-
For each option in the position options list:
-
If option is currently abspos’s last successful position option, continue.
-
Let adjusted styles be the result of applying a position option option to abspos.
-
Let el rect be the size and position of abspos’s margin box, and cb rect be the size and position of abspos’s inset-modified containing block, when laid out with adjusted styles.
-
If cb rect was negative-size in either axis and corrected into zero-size, continue.
Note: This prevents a zero-size el rect from still being considered "inside" a negative-size cb rect and getting selected as a successful option.
-
If el rect is not fully contained within cb rect, continue.
-
Return adjusted styles, along with the associated set of remembered scroll offsets that were hypothetically calculated for them.
-
-
Assert: The previous step finished without finding a position option that avoids overflow.
-
Return current styles.
Note: Descendants overflowing el don’t affect this calculation, only el’s own margin box.
Note: Because we purposely skip the position option currently in effect, it doesn’t get its remembered scroll offsets updated; if none of the other fallbacks work and we stick with the current styles, all the remembered scroll offsets stay the same.
During a full layout pass, once a box has determined its fallback styles (or determined it’s not using any), laying out later boxes cannot change this decision.
Layout does not "go backward", in other words.
ResizeObserver
events are determined and delivered:
-
If a box el is absolutely positioned, set its last successful position option to the set of accepted @position-try properties (and values) that it’s currently using.
-
Otherwise, if el has a last successful position option and if any of the following are true of it, remove its last successful position option:
-
el is not absolutely positioned
-
el’s computed value for position-try-fallbacks has changed
-
Any of the @position-try rules referenced by el’s position-try-fallbacks have been added, removed, or mutated.
Then, determine position fallback styles for el and set its last successful position option to the set of accepted @position-try properties (and values) that it’s now using.
-
Note: The timing of this recording/removal is intentionally identical to the treatment of last remembered sizes.
Implementations may choose to impose an implementation-defined limit on the length of position options lists, to limit the amount of excess layout work that may be required. This limit must be at least five.
#myPopover{ position : fixed; top : anchor ( --button bottom); left : anchor ( --button left); position-try-fallbacks : flip-inline, flip-block, flip-block flip-inline; /* The popover is at least as wide as the button */ min-width:anchor-size ( --button width); /* The popover is at least as tall as 2 menu items */ min-height:6 em ; }
6. DOM Interfaces
6.1. The CSSPositionTryRule interface
The CSSPositionTryRule
interface represents
the @position-try rule:
[Exposed =Window ]interface :
CSSPositionTryRule CSSRule {readonly attribute CSSOMString name ; [SameObject ,PutForwards =cssText ]readonly attribute CSSPositionTryDescriptors style ; }; [Exposed =Window ]interface :
CSSPositionTryDescriptors CSSStyleDeclaration {attribute CSSOMString ;
margin attribute CSSOMString ;
marginTop attribute CSSOMString ;
marginRight attribute CSSOMString ;
marginBottom attribute CSSOMString ;
marginLeft attribute CSSOMString ;
marginBlock attribute CSSOMString ;
marginBlockStart attribute CSSOMString ;
marginBlockEnd attribute CSSOMString ;
marginInline attribute CSSOMString ;
marginInlineStart attribute CSSOMString ;
marginInlineEnd attribute CSSOMString ;
margin-top attribute CSSOMString ;
margin-right attribute CSSOMString ;
margin-bottom attribute CSSOMString ;
margin-left attribute CSSOMString ;
margin-block attribute CSSOMString ;
margin-block-start attribute CSSOMString ;
margin-block-end attribute CSSOMString ;
margin-inline attribute CSSOMString ;
margin-inline-start attribute CSSOMString ;
margin-inline-end attribute CSSOMString ;
inset attribute CSSOMString ;
insetBlock attribute CSSOMString ;
insetBlockStart attribute CSSOMString ;
insetBlockEnd attribute CSSOMString ;
insetInline attribute CSSOMString ;
insetInlineStart attribute CSSOMString ;
insetInlineEnd attribute CSSOMString ;
top attribute CSSOMString ;
left attribute CSSOMString ;
right attribute CSSOMString ;
bottom attribute CSSOMString ;
inset-block attribute CSSOMString ;
inset-block-start attribute CSSOMString ;
inset-block-end attribute CSSOMString ;
inset-inline attribute CSSOMString ;
inset-inline-start attribute CSSOMString ;
inset-inline-end attribute CSSOMString ;
width attribute CSSOMString ;
minWidth attribute CSSOMString ;
maxWidth attribute CSSOMString ;
height attribute CSSOMString ;
minHeight attribute CSSOMString ;
maxHeight attribute CSSOMString ;
blockSize attribute CSSOMString ;
minBlockSize attribute CSSOMString ;
maxBlockSize attribute CSSOMString ;
inlineSize attribute CSSOMString ;
minInlineSize attribute CSSOMString ;
maxInlineSize attribute CSSOMString ;
min-width attribute CSSOMString ;
max-width attribute CSSOMString ;
min-height attribute CSSOMString ;
max-height attribute CSSOMString ;
block-size attribute CSSOMString ;
min-block-size attribute CSSOMString ;
max-block-size attribute CSSOMString ;
inline-size attribute CSSOMString ;
min-inline-size attribute CSSOMString ;
max-inline-size attribute CSSOMString ;
placeSelf attribute CSSOMString ;
alignSelf attribute CSSOMString ;
justifySelf attribute CSSOMString ;
place-self attribute CSSOMString ;
align-self attribute CSSOMString ;
justify-self attribute CSSOMString ;
positionAnchor attribute CSSOMString ;
position-anchor attribute CSSOMString ;
positionArea attribute CSSOMString ; };
position-area
Its name
attribute
represents the name declared in the rule’s prelude.
Its style
attribute
represents the properties declared in the rule’s body,
in the specified order.
On getting, it must return a CSSPositionTryDescriptors
object
for the @position-try at-rule,
with the following properties:
- computed flag
-
Unset
- readonly flag
-
Unset
- declarations
-
The declared descriptors in the rule, in specified order.
- parent CSS rule
-
The context object
- owner node
-
Null
7. Appendix: Style & Layout Interleaving
Style & layout interleaving is a technique where a style update can occur on a subtree during the layout process, resulting in retroactive updates to elements’ computed styles.
This is not the correct spec for this concept, it should probably go in Cascade, but I need a sketch of it to refer to.
Note: Style & layout interleaving is already used with container queries and container query lengths. A length like 10cqw is resolved into a computed length using layout information about the query container’s size, which can thus trigger transitions when the container changes size between layouts.
The accepted @position-try properties are also interleaved when resolving fallback (see position-try).
Obviously this needs way more details filled in, but for now "act like you already do for container queries" suffices. That behavior is also undefined, but at least it’s interoperable (to some extent?).
8. Security Considerations
No Security issues have been raised against this document.
9. Privacy Considerations
No Privacy issues have been raised against this document.
10. Changes
Significant changes since the 26 March 2024 Working Draft:
-
Renamed inset-area to position-area
-
Added position-anchor: auto to use the implicit anchor element as the default anchor element
-
Added top-layer details to the acceptable anchor element definition
-
Removed the implicit keyword from the anchor functions (omitting an anchor name does the same thing)
-
Removed the "reduce the size of the available space" behavior from anchor-center, now that Align defines how to shift to avoid overflow.
-
Added position-visibility, to hide the positioned element in some cases.
-
Added a11y guidance (§ 3.7 Accessibility Implications)
-
Expanded the set of properties that anchor-size() is used in (to all the relevant accepted @position-try properties).
-
Renamed position-try-options to position-try-fallbacks
-
Removed the inset-area() function from position-try-fallbacks (you just use a position-area value directly now).
-
Defined how to swap due to a try-tactic.
-
Expanded the details of how to determine position fallback styles.