1. Introduction
While CSS generally determines the position and size of elements
according to their parents or other ancestors, absolutely positioned elements barely participate in their ancestors' layout.
Instead, they’re sized and positioned explicitly
by the inset properties and box alignment properties,
only referencing the final size and position of their containing block.
This provides extreme flexibility,
allowing elements to be positioned more or less arbitrarily,
including over the top of other elements
in ways that the layout methods don’t otherwise allow,
but in return it’s not very expressive—
2. Anchoring
The anchor functions anchor() and anchor-size(), defined below, give back some of the expressivity of ordinary layout without compromising on the flexibility and power of absolute positioning. Using these functions, one can size and position an absolutely positioned element relative to one or more anchor elements on the page. The @position-set rule allows even more flexibility, allowing multiple different sizes/positions to be tried out sequentially until one is found that fits within the containing block.
2.1. Anchor-based Positioning: the anchor() function
An absolutely-positioned element can use the anchor() function as a value in its inset properties to refer to the position of one or more anchor elements. The anchor() function resolves to a <length>.
<anchor()> = anchor( <anchor-element>? <anchor-side>, <length-percentage>? ) <anchor-element> = <dashed-ident> | implicit <anchor-side> = auto | auto-same | top | left | right | bottom | start | end | self-start | self-end | <percentage> | center
The anchor() function has three arguments:
-
the <anchor-element> value specifies how to find the anchor element it will be drawing positioning information from. If omitted, it behaves as the element’s default anchor specifier.
Its possible values are:
- <dashed-ident>
-
Specifies the anchor name it will look for. This name is a tree-scoped reference.
- implicit
-
Selects the implicit anchor element defined for the element, 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.
The auto and auto-same keywords indicate automatic anchor positioning. See § 2.1.1 Automatic Anchor Positioning for details.
The physical <anchor-side> keywords (left, right, top, and bottom) are only useful in inset properties corresponding to their corresponding axis: for example, using top and bottom in left or right (or in inset-inline-start if the inline axis is horizontal, etc) results in an invalid anchor query.
The logical <anchor-side> keywords (start, end, self-start, and self-end) map to one of the physical keywords depending on the property the function is being used in (top or bottom in the top or bottom properties, etc) and the writing mode of either the element (for self-start and self-end) or the writing mode of the element’s containing block (for start and end).
Do we need to refer to the anchor element’s writing mode? I think that’s too unpredictable to actually do anything useful. If you’re specifying your inset-inline-start property, you almost certainly want to refer to an anchor edge relative to your own directions, not an unpredictable edge based on the anchor.
A <percentage> value 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. The center keyword is equivalent to 50%.
-
the optional <length-percentage> final argument is a fallback value. If the anchor() represents an invalid anchor query, it resolves to this value rather that determining its value as detailed below.
If omitted, it defaults to 0px.
Computed value for anchor() probably needs to be the anchor() function, but with the target anchor element resolved. This allows for transitions to work properly with tree-scoped names, and with changing anchor elements. See Issue 8180.
An anchor() function representing a valid anchor query resolves at used value time to the <length> that would align the edge of the positioned elements' 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 element’s containing block are scrolled to their initial scroll position (but see anchor-scroll).
If the target anchor element is fragmented, the axis-aligned bounding rectangle of the fragments' border boxes is used instead.
Do we need to control which box we’re referring to, so you can align to padding or content edge?
If the positioned element has a snapshotted scroll offset, then it is additionally visually shifted by those offsets, as if by an additional translate() transform.
.bar
element’s top edge
with the --foo anchor’s top edge.
On the other hand,
in .bar { bottom: anchor(--foo top); },
it will instead resolve to the length
that’ll line up the .bar
element’s bottom edge
with the --foo anchor’s top edge.
Since top and bottom values specify insets from different edges (the top and bottom 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 element and as wide as possible without overflowing the containing block:
.centered-message{ position : fixed; max-width : max-content; justify-content : 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.
2.1.1. Automatic Anchor Positioning
If a positioned element uses the anchor() function with the auto or auto-same keywords in one of its inset properties, and the opposing inset property is auto, then the element is using automatic anchor positioning in that property’s axis. This will resolve the anchor() function’s <anchor-side> to the correct side of the anchor element, and automatically create entries in the position fallback list to flip the positioned element to the opposite side if necessary.
Automatic anchor positioning is only active if the opposite inset property is auto. (For example, if an element had top: anchor(auto);, it would have to also have bottom: auto;.) If this is not the case, the anchor() function represents an invalid anchor query, and the element does not use automatic anchor positioning in that axis.
When using automatic anchor positioning, the auto <anchor-side> behaves as the opposite side of the property it’s used in. That is, when used in top: anchor(auto);, it’s equivalent to top: anchor(bottom);; when used in bottom: anchor(auto);, it’s equivalent to bottom: anchor(top);; etc. The auto-same <anchor-side> behaves as the property it’s used in: top: anchor(auto-same); is equivalent to top: anchor(top);, etc.
Additionally, if the element has position-fallback: none, automatic anchor positioning causes the element to gain a position fallback list consisting of two entries:
-
one containing all the base-style properties on the element that are valid to use in @try rules, with auto/auto-same keywords resolved to their appropriate side.
-
one containing the same, but with the inset properties in each axis swapped, and the auto/auto-same keywords resolved to the opposite sides as well.
Note: If the element has a non-none position-fallback, these extra entries aren’t added. Since the position fallback list styles override the "base" styles immediately, this will usually mean you wouldn’t see a "base" anchor(auto) show up in the final styles at all, but if that does happen (it’s specified in a property that isn’t overriden by anything in the position fallback list), the only effect of the auto/auto-same is to resolve to the appropriate side keyword.
.foo{ position : absolute; top : calc ( .5 em +anchor ( --foo auto)); }
is equivalent to the following more verbose and explicit code:
.foo{ position : absolute; position-fallback : --flip; } @position-fallback --flip{ @try { top : calc ( .5 em +anchor ( --foo bottom)); bottom : auto; } @try { top : auto; bottom : calc ( .5 em +anchor ( --foo top)); } }
If the element uses automatic anchor positioning in both axises, it instead adds three entries to the position fallback list: one reversing just the block axis, one reversing just the inline axis, and finally one reversing both axises at once.
auto and auto-same used in a @try rule cause the rule to insert multiple (2 or 4) sets of entries into the position fallback list, as specified above, if they would validly trigger automatic anchor positioning.
2.2. Anchor-based Sizing: the anchor-size() function
An absolutely-positioned element can use the anchor-size() function in its sizing properties to refer to the size of one or more anchor elements. The anchor-size() function resolves to a <length>.
anchor-size() = anchor( <anchor-element>? <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 element (for self-block and self-inline) or the writing mode of the element’s containing block (for block and inline).
An anchor-size() function representing a valid anchor query resolves 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.
2.3. Taking Scroll Into Account: the anchor-scroll property
Name: | anchor-scroll |
---|---|
Value: | none | default | <anchor-element> |
Initial: | none |
Applies to: | absolutely-positioned elements |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
Because scrolling is often done in a separate thread from layout in implementations for performance reasons, but anchor() can result in both positioning changes (which can be handled in the scrolling thread) and layout changes (which cannot), anchor() is defined to assume all the scroll containers between the anchor element and the positioned element’s containing block are at their initial scroll position. This means a positioned element will not be aligned with its anchor if any of the scrollers are not at their initial positions.
The anchor-scroll property allows an author to compensate for this, without losing the performance benefits of the separate scrolling thread, so long as the positioned element is only anchoring to a single anchor element. Its values are:
- none
-
No effect.
- default
-
Behaves identically to <anchor-element>, but draws its value from anchor-default on the element.
- <anchor-element>
-
Selects a target anchor element the same as anchor(), which will be compensated for in positioning and fallback.
The snapshotted scroll offset is the sum of the offsets from the initial scroll position of all scroll container ancestors of the target anchor element, up to but not including query el’s containing block.
Define the precise timing of the snapshot: updated each frame, before style recalc.
2.4. Determining The 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 |
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 an anchor name equal to the <dashed-ident>. The anchor name is a tree-scoped name.
Otherwise, the property has no effect.
The anchor functions refer to an anchor element by name. That name is not necessarily unique on the page, however; even if it is, the anchor element in question might not be capable of anchoring the positioned element.
-
If anchor spec is implicit, and 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.
-
Otherwise, anchor spec is a <dashed-ident>. Return the first element el in tree order that satisfies the following conditions:
-
el is an anchor element with an anchor name of name.
-
el’s anchor name and name are both associated with the same tree root.
Note: The anchor name is a tree-scoped name, while name is a tree-scoped reference.
-
el is an acceptable anchor element for query el.
If no element satisfies these conditions, return nothing.
-
Note: The general rule captured by these conditions is that el must be fully laid out before query el is laid out. CSS’s rules about the layout order of stacking contexts give us assurances about this, and the list of conditions above exactly rephrases the stacking context rules into just what’s relevant for this purpose, ensuring there is no possibly circularity in anchor positioning.
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.)
-
query el is in a higher root layer than el.
-
query el and el are in the same root layer, and all of the following are true:
-
Either el is a descendant of query el’s containing block, or query el’s containing block is the initial containing block.
-
If el has the same containing block as query el, el is not absolutely positioned.
-
If el has a different containing block from query el, the last containing block in el’s containing block chain before reaching query el’s containing block is not absolutely positioned.
-
For the purposes of this algorithm, an element is in a particular root layer corresponding to the closest inclusive ancestor that is in the top layer, or the document if there isn’t one. Root layers are "higher" if their corresponding element is later in the top layer list; the layer corresponding to the document is lower than all other layers.
Note: This wording around "root layer" needs to live in a stacking-context spec, after pulling the top layer stuff out of [fullscreen].
An element can also have an implicit anchor element, used when an anchor function doesn’t specify an explicit anchor name.
Note: The Popover API, for example,
defines an implicit anchor element for a popover—
2.5. Default Anchors: the anchor-default property
Name: | anchor-default |
---|---|
Value: | <anchor-element> |
Initial: | implicit |
Applies to: | absolutely positioned elements |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
The anchor-default property defines the default anchor specifier for all anchor functions on the element, allowing multiple elements to use the same set of anchor functions (and position fallback lists!) while changing which anchor element each is referring to.
Its values are identical to the <anchor-element> term in anchor() and anchor-size().
.anchored{ position : absolute; position-fallback : --under-then-over; } @position-fallback --under-then-over{ @try { // Nospecified , // so it takes from'anchor-default' . top:calc ( .5 em +anchor ( auto)); bottom : auto; } } .foo.anchored{ anchor-default : --foo; } .bar.anchored{ anchor-default : --bar; }
2.6. Anchor Queries
The anchor() and anchor-size() functions represent an anchor query: a request for the position of one or more sides of one or more anchor elements.
Anchor queries are valid only if all of the following conditions are true:
-
Their function is used on an element that is absolutely-positioned.
-
If representing an anchor() function, the function is being used in an inset property.
-
If representing an anchor() function and the <anchor-side> keyword is a physical keyword, it’s used in an inset property in the corresponding axis.
-
If representing an anchor-size() function, the function is being used in a sizing property.
-
There is a target anchor element for the element and the anchor name specified in the function.
Note: As specified in the definition of anchor(), an invalid anchor query causes the function to resolve to its fallback value instead.
3. Fallback Sizing/Positioning
Anchor positioning, while powerful, can also be unpredictable. The anchor element might be anywhere on the page, so positioning an element in any particular fashion (such as above the anchor, or the right of the anchor) might result in the positioned element overflowing its containing block or being positioned partially off screen.
To ameliorate this, an absolutely positioned element can use the position-fallback property to refer to a @position-fallback block, giving a list of possible style rules to try out. Each is applied to the element, one by one, and the first that doesn’t cause the element to overflow its containing block is taken as the winner.
3.1. The position-fallback Property
Name: | position-fallback |
---|---|
Value: | none | <dashed-ident> |
Initial: | none |
Applies to: | absolutely-positioned elements |
Inherited: | no |
Percentages: | n/a |
Computed value: | as specified |
Canonical order: | per grammar |
Animation type: | discrete |
Values have the following meanings:
- none
-
The property has no effect; the element does not use a position fallback list.
- <dashed-ident>
-
If there is a @position-fallback rule with a name matching the specified ident, then the element uses that position fallback list.
Otherwise, this value has no effect.
3.2. The @position-fallback Rule
The @position-fallback rule defines a position fallback list with a given name, specifying one or more sets of positioning properties inside of @try blocks that will be applied to an element, with each successive one serving as fallback if the previous would cause the element to partially or fully overflow its containing block.
The grammar of the @position-fallback rule is:
@position-fallback <dashed-ident> { <rule-list> } @try { <declaration-list> }
The @position-fallback rule only accepts @try rules. The <dashed-ident> specified in the prelude is the rule’s name. If multiple @position-fallback rules are declared with the same name, the last one in document order "wins".
The @try rule only accepts the following properties:
What exactly are the constraints that determine what’s allowed here? Current list is based off of what’s reasonable from Chrome’s experimental impl. We can make a CQ that keys off of which fallback was used to allow more general styling, at least for descendants.
The @try rules inside a @position-fallback specify a position fallback list, where each entry consists of the properties specified by each @try, in order.
If you have a bunch of elements that all use the same positioning and fallback, just relative to different anchor elements (like a bunch of tooltips), there’s no way to have them share @position-fallback rules; they each need a unique set. This sucks! Should figure out some way to address this.
Would be useful to be able to detect when your anchor(s) are fully off-screen and suppress your display entirely. For example, tooltips living outside the scroller holding the text they’re anchored to don’t want to just hover over arbitrary parts of the page because their anchor happens to have that position relative to the scrollport.
3.3. Applying Position Fallback
When an element uses a position fallback list, it selects one entry from the list as defined below, and applies those properties to itself as used values.
Note: These have to be applied as used values because we’re in the middle of layout right now; defining how they’d interact with the cascade would be extremely confusing *at a minimum*, and perhaps actually circular. In any case, not worth the cost in spec or impl.
This implies that the values can’t be transitioned in the usual fashion, since transitions key off of computed values and we’re past that point. However, popovers sliding between positions is a common effect in UI libs. Probably should introduce a smooth keyword to position-fallback to trigger automatic "animation" of the fallback’d properties.
To determine which entry is selected, iterate over the position fallback list, applying the properties of each entry in turn according to the standard cascade rules, and additionally shifting the element’s margin box according to its snapshotted scroll offset (if it has one), and determining whether or not the element’s margin box overflows its containing block.
Note: Descendants overflowing the anchored block don’t affect this.
The properties of the first non-overflowing entry (or the last attempted entry, if none succeeded), are taken as the final values for the specified properties.
Implementations may choose to impose an implementation-defined limit on the length of position fallback lists, to limit the amount of excess layout work that may be required. This limit must be at least five.
There are strategies to avoid this, but they’re not without costs of their own. We should probably impose a maximum limit as well, to avoid this.
However, since *most* usages won’t be problematic in the first place, we don’t want to restrict them unduly just to prevent weird situations from exploding. Perhaps a complexity budget based on the branching factor at each level? Like, accumulate the product of the fallback list lengths from ancestors, and your fallback list gets limited to not exceed a total product of, say, 1k. Get too deep and you’re stuck with your first choice only! But this would allow large, complex fallback lists for top-level stuff, and even some reasonable nesting. (Length-five lists could be nested to depth of 4, for example, if we did go with 1k.)
More thought is needed.
#myPopover{ position : fixed; position-fallback : --button-popover; overflow : auto; /* 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 ; } @position-fallback --button-popover{ /* First try to align the top, left edge of the popover with the bottom, left edge of the button. */ @try { top : anchor ( --button bottom); left : anchor ( --button left); } /* Next try to align the bottom, left edge of the popover with the top, left edge of the button. */ @try { bottom : anchor ( --button top); left : anchor ( --button left); } /* Next try to align the top, right edge of the popover with the bottom, right edge of the button. */ @try { top : anchor ( --button bottom); right : anchor ( --button right); } /* Finally, try to align the bottom, right edge of the popover with the top, right edge of the button. Other positions are possible, but this is the final option the author would like the rendering engine to try. */ @try { bottom : anchor ( --button top); right : anchor ( --button right); } }
4. Security Considerations
No Security issues have been raised against this document.
5. Privacy Considerations
No Privacy issues have been raised against this document.