Anchor Positioning Exploration

Elika J. Etemad, Jen Simmons, Miriam Suzanne

Abstract

An exploration of ways to express anchor positioning in CSS, with a goal of adapting CSS Anchor Positioning into a layout system that is not only capable of fulfilling its use cases, but delightful for authors to use.

Table of Contents
  1. 1 Introduction
    1. 1.1 Design Goals
    2. 1.2 Use Cases
  2. 2 Anchor-based Positioning
    1. 2.1 Identifying the Anchor
    2. 2.2 Positioning Relative to an Anchor Box
    3. 2.3 Choosing the Positioning Anchor
      1. 2.3.1 Anchor Lookup and Name Scoping
    4. 2.4 Choosing the Positioning Area
      1. 2.4.1 Grid-based Compartment Syntax
      2. 2.4.2 Track-based Compartment Syntax
      3. 2.4.3 Relational Compartment Syntax
    5. 2.5 Insets and Margins
    6. 2.6 Default Alignment
      1. 2.6.1 Alignment Safety
  3. 3 Overflow Fallback Positioning
    1. 3.1 Area-specific Styling: the @position-style at-rule [Proposal A]
    2. 3.2 Area-specific Styling: the :position-area() pseudo-class [Proposal B]
    3. 3.3 Area-specific Styling: the @position-area at-rule [Proposal C]
  4. 4 Anchoring Indicators: Tethers
    1. 4.1 Tether Positioning
    2. 4.2 Default Tether Styling
  5. Conformance
  6. Index
    1. Terms defined by this specification
    2. Terms defined by reference
  7. References
    1. Normative References
    2. Informative References
  8. Issues Index

1. Introduction

This is a rough sketch of ideas. We hope it inspires Google to improve Anchor Positioning before it ships in Chrome.

1.1. Design Goals

1.2. Use Cases

2. Anchor-based Positioning

Anchor-based positioning allows positioning a box relative to another box. It builds on the absolute positioning model.

2.1. Identifying the Anchor

The anchor can be identified by name with a new property, anchor-name. This allows it to be used as a positioning anchor for another element.

anchor-name: none | <dashed-ident>

2.2. Positioning Relative to an Anchor Box

A box can be positioned in reference to another box, called its positioning anchor, in addition to its containing block by identifying a positioning anchor and choosing either the absolute or fixed positioning scheme.

The core properties for defining anchor-based positioning are:

Alternative Namings
position-* Scheme anchor-* Scheme Value Space Notes
position-type position fixed | absolute | etc Choose positioning scheme
position-anchor-name anchor-target-name <dashed-ident> Identify positioning anchor box
position-anchor-box anchor-target-box <layout-box> Choose positioning anchor reference edges
position-anchor anchor-target <'x-name'> || <'x-box'> Shorthand
position-area anchor-area (see below) Choose the positioning area
position [ <'position-type'> || <'position-anchor'> ] <'position-area'>? Shorthand available in position-* variant

In addition to these properties, the inset properties, margin properties, and self-alignment properties play an important role in anchor positioned layout.

2.3. Choosing the Positioning Anchor

The positioning anchor rectangle for an absolutely positioned box is specified using the position-anchor properties, which identify a positioning anchor box and which box edges to use (margin/border/etc.)]

2.3.1. Anchor Lookup and Name Scoping

The positioning anchor for an absolutely positioned box is the principal box of the first (in tree order) element in scope that has a matching anchor-name.

Note: Using first rather than last improves two things: 1) It biases towards ancestors rather than descendants and siblings, and 2) It’s less janky during incremental page loads

An element is in scope for an absolutely positioned box’s positioning anchor search if:

Note: If a suitable positioning anchor cannot be found, the box is laid out without anchor-based positioning, using the normal absolute positioning rules.

anchor-scope: none | all | <dashed-ident>#

The anchor-scope property allows positioning anchor name lookup to be scoped, much the same way timeline-scope allows timeline name lookup to be scoped.

2.4. Choosing the Positioning Area

Once the positioning anchor and containing block have been identified for an absolutely positioned box, the positioning anchor rectangle is used to divide the containing block into a 9-grid by extending lines coinciding with its edges until they intersect the edges of the containing block.

┌───┬─────────┬─────────┐
│ 1 │    2    │    3    │
├───╆━━━━━━━━━╅─────────┤
│ 4 ┃    5    ┃    6    │
├───╄━━━━━━━━━╃─────────┤
│ 7 │    8    │    9    │
│   │         │         │
└───┴─────────┴─────────┘

The position-area property is used to indicate which cell(s) of the resulting grid the absolutely positioned box should be laid out into, i.e. its anchor area.

position-area: <compartment>#
initial: center

By default, the anchor area of the absolutely positioned box coincides with the positioning anchor rectangle.

This makes it easy to use absolute positioning with a more explicitly-chosen containing block: simply name that containing block using anchor-name and choose it with position-anchor.
.card {
  anchor-scope: all;

  .photo {
    anchor-name: --photo;
  }
  .banner {
    position: absolute --photo;
    place-self: start end; /* top right corner in LTR-TB */
  }
}

However, authors can choose to position into any of the other areas of the 9-grid using the <compartment> syntax, which is resolved using the writing mode of the positioning anchor.

Add in Jen’s proposal for a grid-like track-based logical+physical syntax!!

2.4.1. Grid-based Compartment Syntax

In this variant, <compartment> is equivalent to <'grid-area'>, and is able to reference the following automatically-defined line names:

Note: These imply the existence of container and anchor grid areas, which are therefore also valid values for <compartment>.

The grid-based syntax re-uses existing syntax and allows for 2x2 areas as well as 1xN areas, but can only express logical values,

Expand the line names so that logical/physical works? Is that possible?

2.4.2. Track-based Compartment Syntax

In this variant, <compartment> can specify the anchor area using logical keywords, physical keywords, or a combination.

<compartment> =
    <single-keyword-compatment>
  |
    [ [[ top | bottom ] || [ y-center | center]] | [[ start | end ] || center] | all ]
    /
    [ [[ left | right ] || [ x-center | center]] | [[ start | end ] || center] | all ]

<single-keyword-compartment>
  center | block-start | block-end | inline-start | inline-end | top | bottom | left | right

If a <single-keyword-compartment> is specified, the other axis defaults to all.

Otherwise, the keywords before the slash represents specified tracks in one axis, and the keywords after the slash represent specified tracks in the other axis. If neither axis uses a physical keyword, the keywords before the slash represent the block axis, while keywords after the slash represent the inline axis.

When combined with a physical keyword, the center keyword computes to y-center or x-center, whichever matches the corresponding physical axis. (The ambiguous center keyword is allowed in these combinations as an author convenience; it is otherwise interpreted logically.)

2.4.3. Relational Compartment Syntax

Remove this in favor of Jen’s syntax (above)?

In this variant, <compartment> is a keyword combination describing the desired area.

<compartment> =
    center
  |
    [ block-start | inline-start | block-end | inline-end | block-center | inline-center ]
    [ [ start || center ] | [ center || end ] ]
  |
    [ top | bottom | y-center ]
    [ [ left || x-center ] | [ x-center || right ] | [ start || center ] | [ center || end ] ]
  |
    [ left | right | x-center ]
    [ [ top || y-center ] | [ y-center || bottom ] | [ start || center ] | [ center || end ] ]

The first keyword specifies the primary desired row/column; the subsequent keywords specify the desired cell(s) within that row/column.

For example, the following keyword combinations map to the 9-grid as follows:
Value LTR RTL TTB-RL
top left 1 1 1
left top
block-start start 1 3 3
inline-start start
right center 6 6 6
y-center right
top 1+2+3 1+2+3 1+2+3
inline-start 1+4+7 3+6+9 1+2+3
y-center 4+5+6 4+5+6 4+5+6
top start 1 3 3
bottom center 4 4 4
block-end center 8 8 4
bottom start center 7+8 8+9 8+9
block-start center end 2+3 1+2 6+9
inline-end center end 6+9 4+7 7+8

The center keyword by itself is a shortcut that computes to inline-center center.

This relational syntax only allows for 1xN areas, but can express both physical, logical, and physical-logical combo values.

2.5. Insets and Margins

The inset properties measure from the edges of the anchor area, i.e. 'inset: 0' creates an inset-modified containing block coinciding with the designated anchor area.

Percentage values are relative to the size of the single track on the corresponding side of the anchor area (not to the entire anchor area).

For example, if a box occupies the right and center tracks, then left: 50% will pin the left edge of the inset-modified containing block halfway through the center track; and right: 50% will pin the right edge of the inset-modified containing block halfway through the right-side track.

Additionally, when positioned into an anchor area, the auto values of inset resolve to zero.

This relies on alignment for positioning. We could otherwise make them resolve smartly, like alignment? What’s better?

2.6. Default Alignment

The self-alignment properties apply in the context of anchor-based positioning just as they do to all absolutely-positioned boxes, aligning the box’s margin box within its inset-modified containing block.

Note: See CSS Box Alignment 3 § 6.1.2 Absolutely-Positioned Boxes and CSS Box Alignment 3 § 6.2.2 Absolutely-Positioned Boxes.

However, the normal value maps to the positional alignment that aligns the box towards its positioning anchor rectangle in each axis, with “anchor-center” representing preferred alignment over the center of the anchor to the extent that it fits within its inset-modified containing block:

Row/Column Alignment
start end
start + center end
center anchor-center
bottom + center top
end start

Note: Non-replaced absolutely positioned boxes normally use stretch sizing when the self-alignment property is normal; however since it maps to a positional alignment keyword, they default to fit-content sizing when a positioning anchor is in effect. stretch sizing can be requested by specifying stretch on either the appropriate self-alignment property or the appropriate sizing property.

2.6.1. Alignment Safety

Unless the unsafe keyword is specified, the self-alignment properties attempt to maintain the absolutely positioned box within the intersection of the inset-modified containing block and the actual containing block.

For example, if a center-aligned box is placed on an anchor that is too close to the edge, it will shift off-center in order to stay within the containing block, even if it was given negative inset values in order to allow an overhanging effect.

3. Overflow Fallback Positioning

Many use cases for anchor-based positioning desire positioning close to an anchor, but have a higher priority on avoiding overflow than on occupying a specific anchor area. Authors can specify alternative layouts by providing a list of fallback positions in the position-area property.

In the following example, the tooltip is preferentially placed just below the anchoring element, but if there isn’t enough space, it is placed just above instead.
.tooltip-text {
  position: fixed --tooltip-owner block-end, block-start;
}

Because the initial value of the self-alignment properties automatically adapts to the anchor area, the most basic uses of anchor-based positioning will not need additional tweaking. However, area-specific styling can also be applied to the element.

The <compartment-selector> takes the same values as <compartment>, and additionally allows the keyword any (as an alternative to all) to represent any track or track combination in that axis. It matches when the chosen anchor area matches the <compartment> (even if that anchor area was chosen by a different <compartment> syntax). It is used to select the appropriate area-specific styling as described below.

3.1. Area-specific Styling: the @position-style at-rule [Proposal A]

Note: This variant is based on the @position-fallback rule in the current spec. It doesn’t work well with the cascade.

The @position-style at-rule allows associating additional styles with each fallback position by declaring them with @area at-rules. Their syntax is defined as follows:

<@position-style> = @position-style <dashed-ident> {
  <@position-style-area>#
}
<@position-style-area> = @area <compartment-selector># {
  <declaration-list>
}

The <dashed-ident> associates a name with that set of @area rules, which can be referenced in the position-fallback property to associate its @area declarations with an absolutely positioned box. If multiple @position-style sets are declared with the same name, the last one wins.

Once a set of @area rules are associated with the absolutely positioned box, all declarations associated with a matching <compartment-selector> are cascaded together. They are applied in the animation origin, prior to any rules applied by any actual animations.

3.2. Area-specific Styling: the :position-area() pseudo-class [Proposal B]

Note: This probably is the most natural way to express conditional styling, but might not be reasonable to implement.

The :position-area() pseudo-class represents an element while its principal box is placed in a particular anchor area. Its argument is a <compartment-selector>.

In order to avoid cycles, the :position-area() pseudo-class can only be specified on the subject of the selector; it otherwise matches no element. Furthermore the display, content, and all position-* properties are invalid when specified inside a rule applying to any selector containing :position-area().

3.3. Area-specific Styling: the @position-area at-rule [Proposal C]

Note: This variant doesn’t directly style the box, it just defines the bounds of new named areas.

The @position-area at-rule allows declaring custom named areas. Such named areas can be used in position-area.

<@position-area> = @position-area <dashed-ident> {
  <declaration-list>
}

Declarations allowed within an @position-area rule are for the following two descriptors

Allow sub-variants of each named area? Maybe using a functional notation? Maybe by matching up list values?

4. Anchoring Indicators: Tethers

Designers often use visual cues, such as an arrow, to connect an anchored element to its anchor. When an absolutely positioned box is placed into any anchor area other the center or all slot, the absolutely positioned box automatically generates a ::tether() pseudo-element to represent this cue. The pseudo-element’s selector accepts a <compartment-selector> as its argument to allow area-specific styles to be applied.

4.1. Tether Positioning

The ::tether() pseudo-element has a computed position-type matching its originating element (and is thus always out of flow). Its containing block is formed as follows:

If the anchor area is a corner slot

The containing block is the rectangle anchored by the positioning anchor border box corner closest to that anchor area, and the absolutely positioned box’s border box corner closest to the positioning anchor.

Otherwise, if the anchor area occupies one or more cells in a track to one side of the positioning anchor

The containing block is the rectangle formed by

Otherwise, if the anchor area occupies cells in both a side track and the center track

The containing block is the rectangle formed by

in each axis that it occupies both the center track and a side track:
in the axis (if any) that it occupies only the side track:

Note: Authors can use negative insets to make the tether box overlap the positioning anchor and/or the absolutely positioned box.

4.2. Default Tether Styling

The following rules must be applied by the UA in the UA origin.

These default rules make it easy to style a simple triangular tether: adding a background-color makes it visible, adding a positive border-width shows the border on the appropriate sides, and adjusting the width, height, insets, and alignment allow sizing and shaping it as desired.
::tether() {
  corner-shape: angle;
  border: solid 0;
}
::tether(left /  any) {
  border-radius-top-right: 100%/50%;
  border-radius-bottom-right: 100%/50%;
  border-left: none;
}
::tether(right /  any) {
  border-radius-top-right: 100%/50%;
  border-radius-bottom-right: 100%/50%;
  border-right: none;
}
::tether(top /  any) {
  border-radius-bottom-left: 50%/100%;
  border-radius-bottom-right: 50%/100%;
  border-top: none;
}
::tether(bottom /  any) {
  border-radius-top-left: 50%/100%;
  border-radius-top-right: 50%/100%;
  border-bottom: none;
}

Conformance

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.