1 of 40

Anchor Positioning Syntax Workshop

[OpenUI] #357

2 of 40

Goal: Assess Syntax for Anchor Positioning

If you have an idea for how you would like the developer experience for anchor positioning to look, please leave your proposed syntax after the image slides:

  • Image slides represent a UI interface
  • Please leave a numbered slide with your proposed syntax for how to achieve such an interface using anchor positioning. If something isn’t clear from the CSS syntax, please feel free to explain what would be happening from the browser-level.
    • Title your proposal with a number and use a different color header for each proposal to make them easier to parse through when discussing

Problems to resolve:

  • DevX
  • Styling anchors to a parent element
  • Handling window boundaries and logic

3 of 40

Assumptions to make

  • Assume that your stylesheet doesn’t always know what element to anchor to. Imagine that you are writing your styles for a reusable component.
  • Assume that you have an “anchor” HTML attribute available to you. You can put this attribute on the popup and have it point to the element it should be anchored to.
  • If you think the browser should automatically reposition/resize something, describe what you would expect it to do in that case.
  • You can provide your own additional constraints. If you see a use case in the deck that reflects one of your own, you can provide further detail on things like: min/max dimensions, whether the browser should optimize for largest dimensions / avoid overflow, etc. Tailor the use case as much as possible to the problems you have had to solve IRL (as applicable).

4 of 40

Demo A: Menubar

Use case: Style a popup based on its popup anchor

Keep menubar anchor on screen, inset 10px to the right of the menu button

#menu-popup

#menu-button

5 of 40

Proposed Syntax: Declarative Anchor Una

#menu-popup {

/* anchor: <parent pos> / <child pos> */

anchor: bottom right / top left;

margin-right: 10px;

}

The menu popup is always anchored to the bottom right of the menu button at the top right point of the anchored menu element, with a 10px right margin (you can also add margin-top / margin-block-start to give it vertical space away from the menu button).

anchor: <parent pos> / <child pos>

6 of 40

Proposed Syntax - Declarative Anchor #2 Rob

#menu-popup {

/* anchor: <anchor edge> <anchor align> */

anchor: bottom right;

margin-right: 10px;

}

The menu popup is always anchored to the bottom right of the menu button, with a 10px right margin (you can also add margin-top / margin-block-start to give it vertical space away from the menu button).

anchor: <anchor edge> <anchor align>

7 of 40

Proposed Syntax: anchor() function + @-rules Melanie/MS

#menu-popup {� position: fixed;� top: anchor(bottom);� right: calc(anchor(right) - 10px);� max-width: min(20rem, anchor(right));� max-height: calc(100vh - anchor(bottom));� overflow: auto;�}

  • Since this example only ever has one preferred position, @position-set rules are not needed, and we can use the anchor() function to reference edges of the anchor element, as ID’d by the anchor HTML attr.
  • The max width is the lesser of 20rem or the distance from the left-hand side of the viewport to the right-hand side of the button (to prevent overflow)
  • The max height is the distance from the bottom of the button to the bottom of the viewport (prevent overflow).

8 of 40

Proposed Syntax Andrico

#menu-popup {

anchor: bottom right;

anchor-offset: 0 -10px 0 0;

}

Explain:

I’m treating anchor-offset like top, right, bottom, and left to adjust the browser’s default placement of the popup, without needing to change the position attribute.

9 of 40

Proposed Syntax <proposer name>

// Code

Explain:

10 of 40

Demo B: Select

Use case: adjust where the popup is based on viewport position

If there is enough screen real estate for the select to drop down, open it underneath the input bar. If not, open it above to keep it on the page

Choose your own constraints:

  • Should the #select-popup always be the same width as #select-open? Or should it sometimes be wider? If wider, what happens to the width or position when there is not enough horizontal real estate?
  • Should there be a minimum preferred height for the #select-popup?

#select-open

#select-open

#select-popup

#select-popup

11 of 40

Proposed Syntax: Declarative Anchor Una

#select-popup {

anchor: center / center;

optimizeVisibility: viewport 1rem;

}

optimizeVisibility (name tbd) is a property that tells the browser to optimize for keeping the popup in the viewport. The 1rem value is the inset from the viewport.

optimizeVisibility values:

  • none (default)
  • viewport

12 of 40

Proposed Syntax: Declarative Anchor #2 Rob

#select-popup {

anchor: bottom left, top left;

}

  • Chooses first anchor which maximizes height of popup (e.g. if there’s enough room for full height on both sides chooses bottom)

13 of 40

Proposed Syntax: anchor() function + @-rules Melanie/MS

#select-popup {� position: fixed;� position-set: largestHeight(selectPos);� left: anchor(left);� width: calc(anchor(right) - anchor(left));� overflow: auto;�}��@position-set(selectPos) {� 1 {� top: anchor(bottom);� max-height: calc(100vh - anchor(bottom));� }� 2 {� bottom: anchor(top);� max-height: anchor(top);� }�}

  • The largestHeight() function selects the option in the position-set that yields the largest height.
  • The first position anchors the popup to the bottom of the button part, and constrains the height to the distance between the button and the bottom of the viewport.
  • The second anchors the bottom of the popup to the top of the button part. Likewise, its height is constrained to the distance from the top of the viewport to the top of the button.

14 of 40

Proposed Syntax: anchor() + position-set() Melanie/MS

#select-popup {� position: fixed;� top: largestHeight(position-set(anchor(bottom), auto));� bottom: largestHeight(position-set(auto, anchor(top)); � left: anchor(left);� width: calc(anchor(right) - anchor(left));� max-height: largestHeight(position-set(calc(100vh - anchor(bottom), anchor(top));� overflow: auto;�}

  • The largestHeight() function selects the option in the position-set that yields the largest height.
  • The first position anchors the popup to the bottom of the button part, and constrains the height to the distance between the button and the bottom of the viewport.
  • The second anchors the bottom of the popup to the top of the button part. Likewise, its height is constrained to the distance from the top of the viewport to the top of the button.

15 of 40

Proposed Syntax: anchor() + position-set() - alt Melanie/MS

#select-popup {� position: fixed;� position-optimize: largestHeight;� top: position-set(anchor(bottom), auto));� bottom: position-set(auto, anchor(top)); � left: anchor(left);� width: calc(anchor(right) - anchor(left));� max-height: position-set(calc(100vh - anchor(bottom), � anchor(top));� overflow: auto;�}

  • Same as the previous option, but introduce a new property to capture that we want to optimize for largestHeight first, instead of having to repeat the largestHeight() function.

16 of 40

Proposed Syntax: anchor() function + magic properties Melanie

#select-popup {� position: fixed;� reposition: flip-v;� reposition-optimize: � min-overflow, largest-height;� top: anchor(bottom);� left: anchor(left);� width: calc(anchor(right) - anchor(left));� max-height: calc(100vh - anchor(bottom));� overflow: auto;�}

  • The reposition property tells the UA what it’s ok to do in order to prevent this popup from overflowing. This example includes only “flip-v”, which will tell the UA it can invert positioning across the vertical axis. Note: might be expensive because the UA would have to compute inversions across properties (e.g. top: anchor(bottom); becomes top: auto; bottom: anchor(top). Unclear how UA might “flip” calc expressions: special logic? Some other hint from web dev?
  • reposition-optimize: tells the browser what to optimize for, in this case: first, minimal overflow, then largest height.

17 of 40

Proposed Syntax Andrico

#select-popup {

anchor: bottom, top;

anchor-arrow: hidden;

width: anchor(width);

min-height: 80px;

height: 120px;

}

Anchor takes comma-separated list of anchor positions in order of preference

Anchor-arrow specified the appearance of the arrow, in this case we hide it. This removes any gutter space between the popup and its parent.

Used melanie’s anchor function to specify the width

Height is the preferred height, but if height is less than 80px, the next anchor position will be used.

18 of 40

Proposed Syntax <proposer name>

// Code

Explain:

19 of 40

Demo C: Cards

Use case: Style an anchored element based on an ideal location but make adjustments for the viewport

Center the tooltip above the card when hovered/focused, but keep it within the viewport

#tooltip-popup

#tooltip-popup

#card

#card

20 of 40

Demo C: Cards

Use case: Style an anchored element based on an ideal location but make adjustments for the viewport

Center the tooltip above the card when hovered/focused, but keep it within the viewport

#tooltip-popup

#tooltip-popup

#card

#card

#tooltip-popup

#tooltip-popup

21 of 40

Proposed Syntax: Declarative Anchor Una

#tooltip-popup {

anchor: top center / bottom center;� optimizeVisibility: viewport 1rem;

}

#tooltip-popup:after {

content: ‘’; /* rest of tooltip triangle styling here ...*/� anchor: top center / bottom center;� optimizeVisibility: none; /* default so technically not needed */� /* might need a position: absolute too */

}

optimizeVisibility (name tbd) is a primitive that tells the browser to optimize for keeping the popup in the viewport

The 1rem value is the optimizeVisibilityOffset (offset from the viewport).

Anchor shorthand

22 of 40

Proposed Syntax: pseudo class Robert

#select-popup {

anchor: bottom left, top left;

}

#select-popup:anchor(0) {

anchor: none; /* bad*/

}

#select-popup:anchor(0) > .callout {

… /* bottom left style */

}

  • Add some selector of which anchor was matched
  • Probably change the name and values

23 of 40

Proposed Syntax: anchor() function + @-rules Melanie/MS

#tooltip-popup {� position: fixed;� position-set: tooltipPos;� top: calc(anchor(top) - .5rem);� max-width: min(10rem, calc(100vw / 2));�}��@position-set(tooltipPos) {� 1 {� --anchorMidpoint: calc(anchor(left) +� ((anchor(right)-anchor(left)) / 2);� left: var(--anchorMidpoint);� transform: translateX(-50%);� }� 2 {left: anchor(left);}� 3 {right: anchor(right);}�}

  • Per the use case, tooltip is always displayed above the element
  • Tooltip can be centered above the element, anchored to its left, or anchored to its right
  • A custom property for the anchor midpoint is introduced to make this a little more readable.

24 of 40

Proposed Syntax: anchor() + position-set() - alt Melanie/MS

#tooltip-popup {� --anchorMidpoint: calc(anchor(left) +� ((anchor(right)-anchor(left)) / 2);

position: fixed;� top: calc(anchor(top) - .5rem);� left: position-set(var(--anchorMidpoint),� anchor(left), anchor(right));� transform: position-set(translateX(-50%),� auto); � max-width: min(10rem, calc(100vw / 2));�}

  • Same as previous example, but using position-set() function syntax in left and transform properties.

25 of 40

Proposed Syntax: anchor() function + magic properties Melanie

#tooltip-popup {� --anchorMidpoint: calc(anchor(left) +� ((anchor(right)-anchor(left)) / 2);

position: fixed;� reposition: align-edges-h, resize-h;� reposition-optimize: � min-overflow, largest-width;� top: calc(anchor(top) - .5rem);� left: var(--anchorMidpoint);� transform: translateX(-50%);� max-width: min(10rem, calc(100vw / 2));� min-width: 8em;�}

  • The reposition property tells the UA that in order to keep the tooltip from overflowing, it can: align the edges of the popup to the anchor element, and/or resize the popup (in that order of preference).
  • Doing so would throw out transforms that modify the x coordinate, which may be problematic if the author wants that composed with repositioning for some reason other than aligning midpoints.

26 of 40

Proposed Syntax Andrico

#tooltip-popup {

anchor: top center;

}

Would there be any scenario where the tooltip overflowing outside of the viewport is the default behaviour?

Should the browser try and keep the popup within the viewport until it literally can’t?

If so, what could the inverse syntax be, when we don’t mind if the popup overflows?

27 of 40

Proposed Syntax <proposer name>

// Code

Explain:

28 of 40

SEPARATOR FOR ADDITIONAL EXAMPLES

29 of 40

Demo Use case D: Facebook like menu

Use case: <use case>

Above and left aligned (to edge of button) by default

Below and left aligned (to edge of button) when scroller meets edge of container.

30 of 40

Demo D: <Name>

Use case: <use case>

Explain Use Case

31 of 40

Demo E: Teaching UI

Use case: position teaching UI (with variable-length content) relative to the anchor element it describes. The teaching popup is repositioned both horizontally and vertically according to real-estate; the diagrams show the order in which you prefer to display the popup.

Assume that your stylesheet doesn’t know what element .teaching-popup will be anchored to.

Choose your own constraints:

  • Should there be a minimum preferred width or height? How about a maximum?
  • What logic should trigger position #2 instead of #1, for example?

#anchor-button

.teaching-popup

#anchor-button

.teaching-popup

#anchor-button

.teaching-popup

#anchor-button

.teaching-popup

#anchor-button

.teaching-popup

#anchor-button

.teaching-popup

#1

#2

#3

#4

#5

#6

32 of 40

Proposed Syntax: anchor() function + @-rules Melanie/MS

.teaching-popup {� --midpointH: calc(anchor(left) + ((anchor(right)-anchor(left)) / 2);� --midpointV: calc(anchor(top) + ((anchor(bottom) - anchor(top)) / 2));� --spaceRight: calc(100vw - anchor(right) - 1rem); � --spaceLeft: calc(anchor(left) - 1rem);� --spaceHMiddle: calc(min(midPointH, calc(100vw - midpointH)) - 1rem);� --spaceAbove: calc(anchor(top) - 2.5rem);� --spaceTop: calc(100vw - anchor(top));� --spaceVMiddle: calc(min(midpointV, calc(100vw - midpointV)) - 1rem);� position: fixed;� position-set: teachingPos;�}��@position-set(teachingPos) {� /* Set horizontal positions */� 1, 2 {left: calc(anchor(right) + .5rem); max-width: var(--spaceRight);}� 3, 4 {right: calc(anchor(left) - .5rem); max-width: var(--spaceLeft);}� 5, 6 {left: var(--midpointH); transform: translateX(-50%); � max-width: var(--spaceHMiddle);}�� /* Set vertical positions */� 1, 3 {top: var(--midpointV); transform: translateY(-50%); � max-height: var(--spaceVMiddle);}� 2, 4 {top: calc(anchor(top) - .5rem); max-height: var(--spaceTop);}� 5, 6 {top: calc(anchor(top) - 2rem); max-height: var(--spaceAbove);}�}

  • The position sets specify the positioning in the order shown by the diagrams.
  • Rem measurements assume you want .5rem space from the edge of the viewport.
  • Comma-separated selectors are used to define sets of horizontal positions, then vertical positions; these are combined by the UA. There are other ways to write this (e.g. could’ve just selected 5, 6 once and declared horizontal and vertical positioning in one pass).

33 of 40

Proposed Syntax: anchor() + position-set() - alt Melanie/MS

  • Same functionality as the previous. Definitely messier with this syntax.
  • Dropped duplication where applicable between position 5 and 6, in the event that if only 5 options were given in one property (vs 6 in another), position 6 would just use the 5th option.

.teaching-popup {� /* Assume same custom props as previous */� position: fixed;�� left: position-set((calc(anchor(right) + .5rem), calc(anchor(right) + � .5rem), auto, auto, var(--midpointH));� right: position-set(auto, auto, calc(anchor(left) - .5rem), � calc(anchor(left) - .5rem), auto);� max-width: position-set(var(--spaceRight), var(--spaceRight), � var(--spaceLeft), var(--spaceLeft), var(--spaceHMiddle));� transform: position-set(translateY(-50%), auto, translateY(-50%), auto, � translateX(-50%));�� top: position-set(var(--midpointV), calc(anchor(top) - .5rem), � var(--midpointV), calc(anchor(top) - .5rem), calc(anchor(top) - 2rem));� max-height: position-set(var(--spaceVMiddle), var(--spaceTop),� var(--spaceVMiddle), var(--spaceTop), var(--spaceAbove));�}

34 of 40

Proposed Syntax: anchor() function + magic properties Melanie

.teaching-popup {� --midpointV: calc(anchor(top) + ((anchor(bottom) - anchor(top)) / 2));� --spaceRight: calc(100vw - anchor(right) - 1rem); � --spaceVMiddle: calc(min(midpointV, calc(100vw - midpointV)) - 1rem);�� position: fixed;� top: var(--midpointV);� left: anchor(right);� margin: .5rem;� max-width: var(--spaceRight);� max-height: var(--spaceVMiddle);� transform: translateY(-50%);�� reposition: align-edges-right, align-edges-left, center-h /� align-edges-top, center-v;� reposition-optimize: min-overflow, largest-height, largest-width;�}

  • Reposition property say it’s ok to align right, left edges or center horizontally, in that order. In the vertical axis, it’s ok to align top edges or center vertically. This omits things like aligning bottom edges, or aligning to the top of the popup to the bottom of the anchor element. The order may not actually work out as in the design, but that may not be a dealbreaker.
  • Transform that impacts position is thrown out when repositioned.
  • We lose contextually-aware margins. We’ve provided .5rem space in all directions, when sometimes we want 2rem or -.5rem. The design won’t precisely match; the worst example is position #2 and #4, these will be .5rem from the top instead of -.5rem.

35 of 40

Proposed Syntax 1 <proposer name>

// Code

Explain:

36 of 40

Proposed Syntax 2 <proposer name>

// Code

Explain:

37 of 40

Demo F: sidenotes

Use case: center the sidenote of variable content length to the right of the element it describes. If the space to the right of the element is “too narrow” or “too short” (you decide when that is), the sidenote should be in a fixed position at the top-left corner of the document.

Assume that your stylesheet doesn’t know what element .teaching-popup will be anchored to.

#anchor-el

.sidenote

#anchor-el

.sidenote

38 of 40

Proposed Syntax: anchor() function + @-rules Melanie/MS

.sidenote {� --midpointV: calc(anchor(top) + ((anchor(bottom) - anchor(top)) / 2));� --spacevMiddle: calc(min(midpointV, calc(100vw - midpointV)) - 1rem);� --spaceRight: calc(100vw - anchor(right) - 1rem);� position: fixed;� position-set: sidenotePos;�}��@position-set(sidenotePos) {� 1 {� left: calc(anchor(right) + .5rem);� top: var(--midpointV);� transform: translateY(-50%);� max-height: var(--spacevMiddle);� max-width: var(--spaceRight);� min-width: 10em;� min-height: 10em;� }� � 2 {� top: .5rem;� left: .5rem;� width: calc(100% - 1rem);� width: height(100% - 1rem);� }�}

  • The sidenote will be anchored to the right of the element, only if it can be drawn 10em x 10em. If the available space is too small, it will be drawn to fill the viewport (with .5rem margin) instead.

39 of 40

Proposed Syntax 1 <proposer name>

// Code

Explain:

40 of 40

Proposed Syntax 2 <proposer name>

// Code

Explain: