1. Introduction
Introduction here.
2. Anchoring
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—
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( <dashed-ident>? <anchor-side>, <length-percentage>? ) <anchor-side> = top | left | right | bottom | start | end | self-start | self-end | <percentage> | center
The anchor() function has three arguments:
-
the <dashed-ident> specifies the anchor name of the anchor element it will be drawing positioning information from. See target anchor element for details. This name is a tree-scoped reference.
If omitted, the function uses an element’s implicit anchor element instead.
-
the <anchor-side> value refers to the position of the corresponding side of the target anchor element.
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.
An anchor() function representing a valid anchor query resolves 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.
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?
Per flackr/iank feedback, this should be able to respond to the scroll position of the anchor element, both for positioning and for fallback, in a way essentially identical to @scroll-timeline. Precise timing + details to be filled in.
.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.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( <dashed-ident>? <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. 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 name is not provided:
-
If query el has an implicit anchor element, return that element.
-
Otherwise, return nothing.
-
-
Otherwise, 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.
-
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
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.)
An element can also have an implicit anchor element, used when an anchor function doesn’t specify an explicit anchor name.
Note: The Popup API, for example,
defines an implicit anchor element for a popup—
Note: An element can have only one implicit anchor element. If we end up with multiple features defining potentially different implicit anchor elements on the same element, this specification will define a resolution order.
2.4. 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, popups 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 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.
#myPopup{ position : fixed; position-fallback : --button-popup; overflow : auto; /* The popup is at least as wide as the button */ min-width:anchor-size ( --button width); /* The popup is at least as tall as 2 menu items */ min-height:6 em ; } @position-fallback --button-popup{ /* First try to align the top, left edge of the popup 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 popup 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 popup 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 popup 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
Nil.
5. Privacy Considerations
Nil.