-
Notifications
You must be signed in to change notification settings - Fork 756
Description
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?
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