CSS Anchor Positioning

Unofficial Proposal Draft,

More details about this document
This version:
http://tabatkins.github.io/specs/css-anchor-position
Issue Tracking:
CSSWG Issues Repository
Inline In Spec
Editor:
Tab Atkins-Bittner (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 section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the W3C technical reports index at https://www.w3.org/TR/.

Please send feedback by filing issues in GitHub (preferred), including the spec code “css-anchor-position” in the title, like this: “[css-anchor-position] …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.

This document was produced by a group operating under the W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent which the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy.

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.

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?

For this purpose, all scrolling containers are treated as if scrolled to their initial scroll position (but see anchor-scroll), and all transforms are ignored.

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. Anchoring to a Scrolled Position: the anchor-scroll property

The anchor() property does not take scrolling into account, calculating its values as if the positioned element and the target anchor element had all ancestor scroll containers set to the initial position. This enables the feature to work purely on information known during layout, since scrolling is often implemented "on the compositor", as a post-layout visual transform.

When responding to scroll position is necessary, however, the anchor-scroll property can be used to indicate this.

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

anchor-scroll allows a positioned element to respond to the scroll offsets of a target anchor element’s ancestor scroll containers, keeping the position of the two in sync as scrolls are performed. Values are defined as:

none

No effect.

<dashed-ident>

If there is no target anchor element with the given name, this value does nothing.

Otherwise, this element is translated by the accumulated scroll offsets of all scroll containers between the element’s containing block and the target anchor element.

This translation is treated identically to the translation caused by position: sticky.

Name TBD, this probably isn’t the final name.

2.3. 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.4. Determining The Anchor: the anchor-name property

Name: anchor-name
Value: none | <dashed-ident>
Initial: none
Applies to: all elements
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>

The element is an anchor element, with an anchor name equal to the <dashed-ident>.

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, find the first element el in tree order which satisfies the following conditions:

Note: There might not be any such element.

Note: Because positioned elements are laid out strictly after non-positioned elements in the same containing block, these conditions ensure that the anchor element is laid out strictly before the element using anchor() or anchor-size(), so there is no possible circularity between the position and/or size of the two elements.

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.

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.

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.

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://drafts.csswg.org/css-align/
[CSS-BOX-4]
Elika Etemad. CSS Box Model Module Level 4. URL: https://drafts.csswg.org/css-box-4/
[CSS-BREAK-4]
Rossen Atanassov; Elika Etemad. CSS Fragmentation Module Level 4. URL: https://drafts.csswg.org/css-break-4/
[CSS-CASCADE-5]
Elika Etemad; Miriam Suzanne; Tab Atkins Jr.. CSS Cascading and Inheritance Level 5. URL: https://drafts.csswg.org/css-cascade-5/
[CSS-DISPLAY-3]
Tab Atkins Jr.; Elika Etemad. CSS Display Module Level 3. URL: https://drafts.csswg.org/css-display/
[CSS-LOGICAL-1]
Rossen Atanassov; Elika Etemad. CSS Logical Properties and Values Level 1. URL: https://drafts.csswg.org/css-logical-1/
[CSS-POSITION-3]
Elika Etemad; Tab Atkins Jr.. CSS Positioned Layout Module Level 3. URL: https://drafts.csswg.org/css-position/
[CSS-SIZING-3]
Tab Atkins Jr.; Elika Etemad. CSS Box Sizing Module Level 3. URL: https://drafts.csswg.org/css-sizing-3/
[CSS-SYNTAX-3]
Tab Atkins Jr.; Simon Sapin. CSS Syntax Module Level 3. URL: https://drafts.csswg.org/css-syntax/
[CSS-VALUES-4]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 4. URL: https://drafts.csswg.org/css-values-4/
[CSS-WRITING-MODES-4]
Elika Etemad; Koji Ishii. CSS Writing Modes Level 4. URL: https://drafts.csswg.org/css-writing-modes-4/
[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

[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 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.
Do we need to control which box we’re referring to, so you can align to padding or content edge?
Name TBD, this probably isn’t the final name.
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.
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.