Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-position] Ability to set a positioned element's containing block to another element #5952

Closed
LeaVerou opened this issue Feb 8, 2021 · 21 comments

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Feb 8, 2021

The need to anchor a positioned element based on another element's position comes up very frequently in things like menus, popups, tooltips etc, and the current solutions are rather messy, involving copious amounts of flimsy JS code to monitor the element's position, scrolling, resizes etc and adjust the popup accordingly.

There is even a recent proposal for a <popup> element whose main feature is that its position can be "anchored" to another element.

In my opinion this is something that needs to be addressed in CSS, not by introducing new "magic" baked in to specific HTML elements.

Would it be feasible to set a positioned element's containing block to an arbitrary (positioned) element in the tree?
If not feasible in the general case, what constraints would make it feasible?

An obvious constraint is that the containing block cannot be a descendant of the element. What others are there?

We'd also need some way to prevent cycles, e.g. where A uses B as its containing block and B uses A as its containing block. A good way to deal with cycles is to prevent them altogether, by only allowing elements that come before the positioned element in source order to be used as its containing block. This also has the advantage of limiting reflows during incremental rendering. If this proves insufficient for covering the use cases, I suppose there's always cycle detection.

Also, does this make sense for other positions beyond absolute?

Syntax ideas

Assuming this is indeed doable, with certain constraints, a few rough thoughts on syntax.

There are three orthogonal questions:

  1. How to specify the element?
  2. How to set the containing block to that element?
  3. How to handle errors?

1. How to specify the element?

First, I think we will ultimately need a new <element> type in CSS, as features that require element references have come up before, and will come up again. We should not repeat mistakes like element(), which defined its own ad hoc querying logic. However, we can for now create ad hoc microsyntax for this feature and port it to an <element> type later when the syntax needs to be shared with another property (as we've done with other types before, e.g. <position>). However, keeping that future direction in mind should influence the syntax, i.e. we should not design it in a way that would be incompatible with being included in other properties. For example, we should use functional notation(s) instead of including a bare selector which would be impossible to disambiguate from other values.

The only (?) precedent here is element(), which only accepted a bare <id-selector>. We would definitely need some way to refer to a single element by id, not only because this is easiest to implement, but also because it offers an escape hatch into JS when the rest of the syntax is not sufficient to express author intent. Perhaps a function like first(<compound-selector>) would work for this.

We'd also need syntax to refer to the default containing block, though that could just be auto.

Ideally, we need some kind of relative syntax which would resolve to an element based on the currently matched context. Something based on relative selectors (rel: #5745) is probably one's first thought. However, if we decide to go with the constraint that containing blocks need to come before the positioned element in source order, this means we cannot use (complex) selectors here, as we need to go up the tree and selectors only go down.

I think as long as we can match arbitrary ancestors and arbitrary previous siblings of the current element or any of its ancestors, this covers most use cases.

How about a set of functions, such as

closest(<compound-selector>) | sibling(previous <compound-selector> [of <element>])

Combined with first(<compound-selector>), these provide a lot of expressivity, with a pretty small vocabulary.

2. How to set the containing block to that element?

If we decide that this only makes sense for position: absolute, then specifying the containing block could be an extra parameter after absolute (e.g. absolute from <element>), or even a functional notation: absolute(<element>).

Otherwise, I think it makes sense as a separate property:

position-reference: <element> | auto;

3. How to handle errors?

Once we have arbitrary element references for <element>, but elements specified in position-reference need to follow certain constraints, we need to decide what happens when valid elements are specified that do not match these constraints.

I think the easiest route to go is probably IACVT, which would make position-reference the same as if auto was specified.


Related: #5699 though it doesn't address anchoring, but mainly allowing an element to spill out of an overflow: hidden container. However, changing the containing block was discussed there as a possible solution as well.

Also related: #5304

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 8, 2021

ping @emilio @dholbert per @fantasai's suggestion.

@emilio
Copy link
Collaborator

emilio commented Feb 8, 2021

How does this relate to the top layer that <dialog> / :fullscreen / etc uses? That is already-existing magic, and seems like it would allow arbitrary elements to escape their containing block (but not choose an arbitrary container, which avoids having to deal with these issues)

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 9, 2021

@emilio This is not about allowing arbitrary elements to escape their containing block, that's what #5699 is about. it's about positioning elements relative to another element, so that popups can display relative to the element they are anchored to regardless of where that is, via common top/right/bottom/left properties, and without the author having to write complicated buggy JS code for this.

In fact, these containing blocks could often be smaller than the popup that is positioned relative to them. For example, take a look at this popup for editing an <a> element's source:
image

@emilio
Copy link
Collaborator

emilio commented Feb 9, 2021

Well, yeah, I ask because being at the top layer doesn't prevent your position being anchored to another element I guess, and the use cases look similar in terms of whether you want stuff like overflow clips to apply or what not.

Anyhow, so... some amount of this can already be done by abspos + hypothetical position (so inset:auto, display:inline + margin) right? That has the issue of needing the "anchored" element being next in the DOM to the anchor, but that seems off hand like a workable solution (if a bit ugly I guess)? E.g. something like:

<!doctype html>
<style>
.editor {
  display: none;
  position: absolute;
  width: min-content;
  border: 1px solid black;
  background: white;
  padding: .3em;
  margin-left: -3em;
}

a:hover + .editor,
.editor:hover {
  display: inline;
}
</style>
Doesn't really matter much where this text wraps, here's some <a href="#" title="Hover to edit URL">cool link.</a> <span class="editor">Url: <input type=text></span>

Of course that's not great in lots of ways (it doesn't guarantee it ends up in the viewport, etc etc), but "choose an arbitrary containing block" also has this issue, doesn't it?

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 9, 2021

@emilio This does work for some cases, but not all (or even most), and currently the complexity of the required solution explodes once you need to go past that. Also, this comes up a lot in web components, in which case the popup might be in Shadow DOM and the anchor element in light DOM, and thus they cannot be siblings.

I should have elaborated when pinging you that I was primarily asking about which constraints would make this implementable, so I could propose a syntax with those in mind. @fantasai thinks it should be implementable with the constraints listed in the original post, but suggested I confirm with you and @dholbert.

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 9, 2021

I edited the first post to add some rough thoughts on possible syntaxes.

@dholbert
Copy link
Member

dholbert commented Feb 9, 2021

One possible constraint / concern here: the "search" here might need to be blocked from crossing boundaries of elements that form containing blocks for their fixed-position descendants (e.g. an element with filter or transform set to a non-default value).

These properties (filter, transform, etc) are typically painted as a single layer, and are animated as a unit (e.g. if the transformed element is bouncing around the page). This proposed positioning feature could make that sort of optimization difficult, if e.g. an element with animated transform had a descendant that was positioned relative to some other element (particularly if that other element is also changing its position dynamically).

Similarly, it's worth thinking about how this would interact with / traverse overflow:[hidden|scroll|auto] and contain:paint.

@dholbert
Copy link
Member

dholbert commented Feb 9, 2021

(Though... is the intent here that the actual box-tree parenting of the positioned thing would be changed? i.e. does it just entirely "escape" any filters, transforms, overflow clipping, etc. that one of its nearby ancestors might otherwise impose on it? If so: then that would somewhat clarify how this would work in my previous comment. Though I suspect that reparenting would introduce other complications that I haven't thought through yet.)

@Crissov
Copy link
Contributor

Crissov commented Feb 9, 2021

Would it help if one box, foo, could declare itself a named anchor, foobar, at its live position relative to the viewport with its box-sizing edges and another box, bar, could reference these edges in its positioning properties?

foo {
  anchor: "foobar";
}
bar {
  position: absolute;
  top: bottom("foobar");
  left: calc(left("foobar") - 10px);
}

@bfgeek
Copy link

bfgeek commented Feb 10, 2021

So I'll start out by saying there is a lot of complexity that the proposed <popup> element has avoided by placing in the top layer, e.g. you can snapshot the current (post-transform, post-scroll, etc) position of the "anchor" and position the popup relative to this.

(I believe that the popup is dismissed when something scrolls etc, which means that you don't need to synchronise this with composited scrolling).

@tabatkins & I wrote up some thoughts about something very similar to what @Crissov has just described a few months ago. And while this doesn't solve what <popup> is trying to achieve exactly it may still be useful.

(@tabatkins also has a blog post from 2010? with very similar ideas).

See here:
https://gist.github.com/bfgeek/60d4f57092eadcda0d4f32a8eb23b4c8

Effectively it is the same as proposed by @Crissov above. The "restriction" that makes this work from our perspective is that the "anchor" needs to be contained within the same containing-block subtree. E.g.

<div class="position: relative;"> <!-- containing block -->
  <div style="position: absolute; top: top(foobar) 10px;"></div>
  <div>
    <div style="position:  relative;">
      <div style="anchor: foobar;"></div> <!-- this is ok -->
   </div>
  </div>
</div>

<div style="anchor: foobar2;"></div> <!-- can't reference this block -->

The other restrictions are this needs to be "pre-scroll", "pre-transform" similar to how other things in abspos works today. (This is what makes it not immediately useful for the <popup> usecase).

Restricting to an <ident> instead of an arbitary selector, or complex function has nice side effects. But any solution will need to limit to the containing-block subtree.

However with layering with ScrollTimeline etc, you could achieve some smooth linked effects (there may be a way to make this "magically happen" but its super complex to get into right now).

@LeaVerou
Copy link
Member Author

@dholbert

One possible constraint / concern here: the "search" here might need to be blocked from crossing boundaries of elements that form containing blocks for their fixed-position descendants (e.g. an element with filter or transform set to a non-default value).

Do they need to be blocked when going up (i.e. the positioned element is a descendant of an element that forms a containing block for their fixed-position descendant) or also going down, i.e. when the positioned element is outside that subtree, but wants to be positioned relative to an element within that subtree?

Although, I seem to recall <dialog>'s positioning was an exception to this anyway.

Similarly, it's worth thinking about how this would interact with / traverse overflow:[hidden|scroll|auto] and contain:paint.

Yup, #5699 is about exactly that.
While these would definitely be used together frequently, I think they are ultimately orthogonal issues.

(Though... is the intent here that the actual box-tree parenting of the positioned thing would be changed? i.e. does it just entirely "escape" any filters, transforms, overflow clipping, etc. that one of its nearby ancestors might otherwise impose on it? If so: then that would somewhat clarify how this would work in my previous comment. Though I suspect that reparenting would introduce other complications that I haven't thought through yet.)

Ideally, yes. Whether it needs to or we can still cover enough use cases without this is yet unknown.

@LeaVerou
Copy link
Member Author

So I'll start out by saying there is a lot of complexity that the proposed <popup> element has avoided by placing in the top layer, e.g. you can snapshot the current (post-transform, post-scroll, etc) position of the "anchor" and position the popup relative to this.

Could you elaborate on what you mean by "placing in the top layer"?

(I believe that the popup is dismissed when something scrolls etc, which means that you don't need to synchronise this with composited scrolling).

This is unclear. It's not mentioned in the explainer nor in the Open UI definition of "light dismiss".
Furthermore, since <popup> is supposed to be able to emulate things like <select>, I don't think it's intended to be dismissed when scrolling.

@tabatkins & I wrote up some thoughts about something very similar to what @Crissov has just described a few months ago. And while this doesn't solve what <popup> is trying to achieve exactly it may still be useful.

(@tabatkins also has a blog post from 2010? with very similar ideas).

See here:
gist.github.com/bfgeek/60d4f57092eadcda0d4f32a8eb23b4c8

Thank you for putting this up, that's a very interesting proposal.
It's not clear to me what would happen in the (rather common) case of having multiple matches for the same id in the same containing block.
I'd change anchor's grammar to <ident>+, so that each element could associate itself with multiple anchors, to potentially "interface" with multiple separate bits of CSS which expect different anchor names.

One issue I see with the anchor declaring itself as an anchor instead of the popup referring to it via a selector is when multiple libraries want to position popups relative to elements in the host page. They can set anchor using a selector, but then each library overwrites the anchor value the previous libraries have set. Specificity wars ensue for which library will win. The libraries that lost don't get a nice fallback either, just popups randomly positioned (even with the fallback syntax, where would you place them?). There's no nice way to detect this from JS either, you'd need to fetch your intended anchor via JS, read its computed style to see what value the anchor property has, and if you've gone there you may as well just set the popup's style based on that computed style and be done with it.

The other restrictions are this needs to be "pre-scroll", "pre-transform" similar to how other things in abspos works today. (This is what makes it not immediately useful for the <popup> usecase).

Why would that make it not immediately useful for the <popup> use case?

Restricting to an <ident> instead of an arbitrary selector, or complex function has nice side effects. But any solution will need to limit to the containing-block subtree.

I think that could be workable, combined with a solution for visually escaping any clipping (discussed separately in #5699).

@Crissov
Copy link
Contributor

Crissov commented Feb 12, 2021

If each box could be known by many anchor names, but each anchor at any time only refers to a single box, assigning an anchor would not need to overwrite existing assignments of an affected box. To make that clearer, this could be split into two properties:

foo {
  anchor-set: "bar", "baz";
  anchor-clear: "quuz";
}

@chrishtr
Copy link
Contributor

Could you elaborate on what you mean by "placing in the top layer"?

This. In particular, top layer elements are hoisted to the top of the DOM and paint in their own stacking context, so all of the complexity of whatever filters, clips, scrolls, transforms and so on above the element that is placed in the top layer is avoided.

There are a whole lot of complexities of those containing block, stacking contexts, effects, compositing and threaded scrolls/animations which would explode in complexity as a result if the element was not in the top layer. And even if it is in the top layer, getting scrolling and animations right relative to something not in the top layer is complex.

For <popup>, I think there is a much simpler solution in the form of an html attribute. I also think the popup should be dismissed on scroll, for complexity reasons if not because of the UX pattern not needing/wanting it.

Here is an example: load this page in Chrome on ChromeOS, open the "pet" select element, and then scroll the page (doesn't repro on some other platforms, because complexity!). The popup will not remain anchored. This is a bug, but it's a tiny example of the kind of bug I want to avoid having to fix in all its gross generality if it's not truly needed for the UX pattern.

(And as you observe, Chrome is already set up to try to dismiss the select on scroll.)

@LeaVerou
Copy link
Member Author

@chrishtr I see, thank you for the detailed explanation. Essentially what we need here is to be able to specify an element to position relative to, I just thought having that element act as containing block would be easier to implement. If the target element needs to be a top layer element, that is somehow positioned relative to another arbitrary element, I think that addresses most use cases as well.

For <popup>, I think there is a much simpler solution in the form of an html attribute.

An HTML attribute might be a simpler solution to implement, but it violates separation of concerns, as well as the Extensible Web Manifesto (by adding new magic that cannot be specified in CSS), so I'm really hoping there is a set of constraints that would enable a CSS solution to this.

@chrishtr
Copy link
Contributor

An HTML attribute might be a simpler solution to implement, but it violates separation of concerns, as well as the Extensible Web Manifesto (by adding new magic that cannot be specified in CSS), so I'm really hoping there is a set of constraints that would enable a CSS solution to this.

If the anchoring state is ephemeral, which is what I am proposing, then separation of concerns is not violated. I view it as similar to how the <dialog> open state, or whether a <select> or <details> is open, is not in CSS; nor does CSS have direct access to the top layer.

Regarding the Extensible Web Manifesto: right now the developer doesn't have programmatic access to top layer functionality. Perhaps there are use cases for doing that, but before doing it we should have strong use cases, and not do it just because extensibility. The reason is that the top layer is an important way the browser can coordinate certain UX in order to guarantee visibility/accessibility to the user.

@melanierichards
Copy link

melanierichards commented Feb 25, 2021

Hey everyone, joining the party a little late here, not sure how I missed this thread until very recently. (cc @BoCupp-Microsoft @dandclark @ipopescu93, who I refer to here as "we")

For the <popup> proposal referenced in Lea's original post, our intent here was to use the anchor attribute as a marker/hook for a to-be-proposed, CSS-based anchored positioning scheme. How the popup (and other similar top-layer elements, future or otherwise) is positioned according to the anchor relationship would be handled in CSS. Note: we're heard "anchor" could be a little overloaded, so this is definitely bikesheddable terminology.

Original concepts we fleshed out created a net-new positioning scheme (position: anchor), with anchor-point (where on the button is a popup anchored to?) and anchored-origin (what point on the popup gets anchored to the button?) properties. There were also some *-adjust properties the author could use to express how the anchored element (popup) should be repositioned when there is insufficient space in the layout viewport. However, it's better to see what we can reuse in the platform, and we ended up setting that initial design aside in favor of using an absolutely positioned scheme, with an anchor() function used in existent positioning/box sizing properties. Here's a gist that captures some of this early thinking, not yet fully fleshed out into an explainer (text is in flux so please pardon any rough edges, heh): https://gist.github.com/melanierichards/42ffc11f18fd9c69e9bc9cfcc86f39cb

You'll notice it's very similar to what @bfgeek and @tabatkins shared. This direction was very much influenced by their ideas, so full props to them and their prior thinking in this space. :)

We think it's important that the anchor element is specified via markup or script instead of referenced in CSS, as these identifiers could be dynamic and unknown to the author as they are writing their styles.

Two items we haven't be able to solve yet with this direction are:

  • Can we emit JavaScript events (or provide a pseudo selector) when an anchored element is repositioned? This would help authors who have directionally-aware decorations on their anchored element, such as a pointer arrow on some teaching UI. Some interesting challenges here with having to wait until layout is done to understand how something was repositioned, but maybe you want to do something that impacts layout...
  • On MacOS, select listboxes are anchored to their button part according to the currently-selected option. In our previous designs, we solved this providing a delegate value on the anchored-origin property, which would tell the anchored element to delegate its positioning to a descendent (the checked option itself would then have the anchored-origin property declared). This is perhaps a more advanced use case and might be best left to script instead of CSS. Example below:

MacOS-Style select, where the listbox is centered over the button according to its second child

Sounds like there's a lot of interest in this topic, a few different ideas floating around, and some valid concerns about perf and such. What would best help the group explore anchored positioning on the web? Is this worthy of a breakout discussion?

@melanierichards
Copy link

Hi everyone, just wanted to let you know that we've put together some early ideas on anchored positioning for browser-managed, top-layer elements in this explainer: https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/CSSAnchoredPositioning/explainer.md

Both the primary proposal (using at-rules) and "Alternate A" (using a position-set() function in property values) are viable options to explore. Alternate A might actually be better from an implementation perspective, as that eases up on some of the dependencies on layout. We've invited authors to provide feedback on which syntax better meets their needs—or if there is another option that would be better suited.

We've just presented this to the Open UI CG for early feedback today; planning to discuss at the next telecon (June 17) which next steps we want to take w/r/t incubating a solution for anchored positioning. This proposal errs on the side of author control, but there might be some web devs who'd rather exchange some control in trade for a simpler syntax. I expect we'll chat about flexibility vs magic in upcoming discussions, and figure out whether to a) refine these ideas, b) go down a simpler path, or c) come up with a system where control and simplicity can more easily co-exist.

@nuxodin
Copy link

nuxodin commented Aug 5, 2022

In my frontend editing CMS i have these problems with the top layer concept.

I would like to be able to place UI elements above the dialog element.

The Wisiwig editor is not accessible as I can't position it above it.

I would therefore welcome a solution like the one described here.
Or can the pop-up API do this?

grafik
(I gave the dialog opacity 0.5 to make the ui-elements visible.)

@dk8dn
Copy link

dk8dn commented Feb 26, 2023

My opinion

We need something like anchor-name for the parent/target and anchor for the child.
The following is as short as i can describe what i really miss in the positioning module...

On the child:

  1. The position: anchor; is used to make the element "fixed" outside (or even inside ?) the target
  2. The anchor must not be used, and if so, it acts like position:absolute, to the next parent that is not static ...
  3. An targets overflow: hidden; is not applied, because anchored elements are always visible
  4. There should be an anchor-self to specify the corner that should match the targets corner
  5. There should be an anchor-target to specify the corner where the child will be placed with its corner
  6. The corners are specified using common keywords like top-left, right or even middle
  7. With that, yes, it can be possible to say anchor-self: middle and anchar-target: middle to center the box
  8. If anchor-self and anchor-target are not used, the browser decides how to place the element (default like tooltips?)
  9. Possible: If target is visible at the bottom-left in viewport, childs corner is bottom-left while targets corner is top-right. It is then placed right upper the target...
  10. Introduce the anchor-axis, where childs will flip to a specific side if they are not positionable
  11. Possible: If anchor-axis is e.g. horizontal or vertical (default) it will flip only on that axis
  12. Possible: If anchor-axis is both the element will flip to other side if not "positionable"
  13. Possible: If anchor-axis is none, it will never flip if not positionable (and therefor is display:none)
  14. Another option could be something like anchor-position: sticky where the childs sticks as long as possible, e.g. if the parent is visible.
  15. Nice would be an anchor-distance to define a "distance" margin between both elements
  16. The z-index can be used inside the childs like as in position absolute...

But the most importend thing for me is handling the "edge" cases, and this gives me the idea for new pseudos that could be used in the following ways:

:anchors or :anchors(min(3))

Used if the element has anchors (childs, and how many) attached to it.
Then we can handle most cases like draw only 3 Childs and not more than 3.

:viewport(inside bottom-left)

Check if an element is in the specific region of the viewport.
If region is omitted, check if it is inside or outside the viewport.

Example: all elements matching in that viewport definition can be colored:

*:viewport(middle) {
  color: red;
}

where viewport(middle) can be define as width:50vh;height:50vh;top:25vh;left:25:vw ...

Maybe it will be cool if viewport regions can be specified like:

@viewport middle {
  width: 50vh;
  height:50vh;
  top: 25vh;
  left: 25:vw
}

ex: :viewport(outside top)

not visible, but we knew it is at the top (we scrolled down).

What for? Yes, this can be used in Chat displays or even be used for "Scroll to bottom" buttons.

Example: Some chat openend, the new message is displayed at the bottom but the user scrolled up.

Some selector like .target:has(.child:viewport(outside bottom)) button.new-messages could be used to show a button?

really i need this!! :-)

@tabatkins
Copy link
Member

Ah, this issue can be closed at this point; the Anchor Positioning spec is handling this now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants