CSS Anchor Positioning

Editor’s Draft,

More details about this document
This version:
https://drafts.csswg.org/css-anchor-1/
Issue Tracking:
CSSWG Issues Repository
Inline In Spec
Editors:
Tab Atkins-Bittner (Google)
Jhey Tompkins (Google)
Ian Kilpatrick (Google)
Suggest an Edit for this Spec:
GitHub Editor

Abstract

This specification defines 'anchor positioning', where a positioned element can size and position itself relative to one or more "anchor elements" elsewhere on the page.

CSS is a language for describing the rendering of structured documents (such as HTML and XML) on screen, on paper, etc.

Status of this document

This is a public copy of the editors’ draft. It is provided for discussion only and may change at any moment. Its publication here does not imply endorsement of its contents by W3C. Don’t cite this document other than as work in progress.

Please send feedback by filing issues in GitHub (preferred), including the spec code “css-anchor” in the title, like this: “[css-anchor] …summary of comment…”. All issues and comments are archived. Alternately, feedback can be sent to the (archived) public mailing list www-style@w3.org.

This document is governed by the 2 November 2021 W3C Process Document.

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 element cannot easily express its size and position in terms of other elements on the page.

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:

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, 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.

For example, in .bar { top: anchor(--foo top); }, the anchor() will resolve to the length that’ll line up the .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.

Because the anchor() function resolves to a <length>, it can be used in math functions like any other length.

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(--x 50%);
  --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. Taking Scroll Into Account: the anchor-scroll property

Name: anchor-scroll
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

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.

<dashed-ident>

The scroll offsets of the target anchor element with the name given by the <dashed-ident> will be compensated for in positioning and fallback.

implicit

The scroll offsets of the implicit anchor element will be compensated for in positioning and fallback.

If anchor-scroll is not none on an absolutely-positioned element query el, and there is a target anchor element for query el with either the name given by anchor-scroll (if its value is a <dashed-ident>) or no name (if its value is implicit), and at least one anchor() function on query el refers to the same target anchor element, then query el has a snapshotted scroll offset, which is a pair of lengths representing a vertical and horizontal offset.

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.

To determine the target anchor element given a querying element query el and an optional anchor name name:
  1. If name is not provided:

    1. If query el has an implicit anchor element, and that element is an acceptable anchor element for query el, return that element.

    2. Otherwise, return nothing.

  2. Otherwise, return the first element el in tree order that satisfies the following conditions:

    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 el is a acceptable anchor element for an absolutely positioned element query el if all of the following properties apply:

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—the element that the popup is attached to.

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.5. 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:

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 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.

Nested anchors (an anchored element inside of another anchored element) present the potential for exponential blow-up of layouts when doing fallback, since the grandchild anchored element can cause scrollbars on an ancestor, changing the IMCB for the child anchored element, thus possibly causing the fallback choice to change for it.

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.

For example, the following CSS will first attempt to position a "popup" below the element, but if it doesn’t fit on-screen will switch to being above; it defaults to left-aligning, but will switch to right-aligning if that doesn’t fit.
#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: 6em;
}

@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.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Advisements are normative sections styled to evoke special attention and are set apart from other normative text with <strong class="advisement">, like this: UAs MUST provide an accessible alternative.

Tests

Tests relating to the content of this specification may be documented in “Tests” blocks like this one. Any such block is non-normative.


Conformance classes

Conformance to this specification is defined for three conformance classes:

style sheet
A CSS style sheet.
renderer
A UA that interprets the semantics of a style sheet and renders documents that use them.
authoring tool
A UA that writes a style sheet.

A style sheet is conformant to this specification if all of its statements that use syntax defined in this module are valid according to the generic CSS grammar and the individual grammars of each feature defined in this module.

A renderer is conformant to this specification if, in addition to interpreting the style sheet as defined by the appropriate specifications, it supports all the features defined by this specification by parsing them correctly and rendering the document accordingly. However, the inability of a UA to correctly render a document due to limitations of the device does not make the UA non-conformant. (For example, a UA is not required to render color on a monochrome monitor.)

An authoring tool is conformant to this specification if it writes style sheets that are syntactically correct according to the generic CSS grammar and the individual grammars of each feature in this module, and meet all other conformance requirements of style sheets as described in this module.

Partial implementations

So that authors can exploit the forward-compatible parsing rules to assign fallback values, CSS renderers must treat as invalid (and ignore as appropriate) any at-rules, properties, property values, keywords, and other syntactic constructs for which they have no usable level of support. In particular, user agents must not selectively ignore unsupported component values and honor supported values in a single multi-value property declaration: if any value is considered invalid (as unsupported values must be), CSS requires that the entire declaration be ignored.

Implementations of Unstable and Proprietary Features

To avoid clashes with future stable CSS features, the CSSWG recommends following best practices for the implementation of unstable features and proprietary extensions to CSS.

Non-experimental implementations

Once a specification reaches the Candidate Recommendation stage, non-experimental implementations are possible, and implementors should release an unprefixed implementation of any CR-level feature they can demonstrate to be correctly implemented according to spec.

To establish and maintain the interoperability of CSS across implementations, the CSS Working Group requests that non-experimental CSS renderers submit an implementation report (and, if necessary, the testcases used for that implementation report) to the W3C before releasing an unprefixed implementation of any CSS features. Testcases submitted to W3C are subject to review and correction by the CSS Working Group.

Further information on submitting testcases and implementation reports can be found from on the CSS Working Group’s website at http://www.w3.org/Style/CSS/Test/. Questions should be directed to the public-css-testsuite@w3.org mailing list.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-ALIGN-3]
Elika Etemad; Tab Atkins Jr.. CSS Box Alignment Module Level 3. URL: https://w3c.github.io/csswg-drafts/css-align/
[CSS-BOX-4]
Elika Etemad. CSS Box Model Module Level 4. URL: https://w3c.github.io/csswg-drafts/css-box-4/
[CSS-BREAK-4]
Rossen Atanassov; Elika Etemad. CSS Fragmentation Module Level 4. URL: https://w3c.github.io/csswg-drafts/css-break-4/
[CSS-CASCADE-5]
Elika Etemad; Miriam Suzanne; Tab Atkins Jr.. CSS Cascading and Inheritance Level 5. URL: https://w3c.github.io/csswg-drafts/css-cascade-5/
[CSS-DISPLAY-3]
Tab Atkins Jr.; Elika Etemad. CSS Display Module Level 3. URL: https://w3c.github.io/csswg-drafts/css-display/
[CSS-LOGICAL-1]
Rossen Atanassov; Elika Etemad. CSS Logical Properties and Values Level 1. URL: https://w3c.github.io/csswg-drafts/css-logical-1/
[CSS-OVERFLOW-3]
David Baron; Elika Etemad; Florian Rivoal. CSS Overflow Module Level 3. URL: https://w3c.github.io/csswg-drafts/css-overflow-3/
[CSS-POSITION-3]
Elika Etemad; Tab Atkins Jr.. CSS Positioned Layout Module Level 3. URL: https://w3c.github.io/csswg-drafts/css-position-3/
[CSS-SCOPING-1]
Tab Atkins Jr.; Elika Etemad. CSS Scoping Module Level 1. URL: https://w3c.github.io/csswg-drafts/css-scoping/
[CSS-SIZING-3]
Tab Atkins Jr.; Elika Etemad. CSS Box Sizing Module Level 3. URL: https://w3c.github.io/csswg-drafts/css-sizing-3/
[CSS-SYNTAX-3]
Tab Atkins Jr.; Simon Sapin. CSS Syntax Module Level 3. URL: https://w3c.github.io/csswg-drafts/css-syntax/
[CSS-TRANSFORMS-1]
Simon Fraser; et al. CSS Transforms Module Level 1. URL: https://w3c.github.io/csswg-drafts/css-transforms/
[CSS-VALUES-4]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 4. URL: https://w3c.github.io/csswg-drafts/css-values-4/
[CSS-WRITING-MODES-4]
Elika Etemad; Koji Ishii. CSS Writing Modes Level 4. URL: https://w3c.github.io/csswg-drafts/css-writing-modes-4/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119

Informative References

[CSS-SHADOW-PARTS-1]
Tab Atkins Jr.; Fergal Daly. CSS Shadow Parts. URL: https://w3c.github.io/csswg-drafts/css-shadow-parts/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/

Property Index

Name Value Initial Applies to Inh. %ages Anim­ation type Canonical order Com­puted value
anchor-name none | <dashed-ident> none all elements that generate a principal box no n/a discrete per grammar as specified
anchor-scroll none | <dashed-ident> none absolutely-positioned elements no n/a discrete per grammar as specified
position-fallback none | <dashed-ident> none absolutely-positioned elements no n/a discrete per grammar as specified

Issues Index

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.
Do we need to control which box we’re referring to, so you can align to padding or content edge?
Define the precise timing of the snapshot: updated each frame, before style recalc.
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.
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.
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.
Nested anchors (an anchored element inside of another anchored element) present the potential for exponential blow-up of layouts when doing fallback, since the grandchild anchored element can cause scrollbars on an ancestor, changing the IMCB for the child anchored element, thus possibly causing the fallback choice to change for it.

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.