Csswg-drafts: [css-positioning] position: popover proposal

Created on 5 Nov 2020  Â·  9Comments  Â·  Source: w3c/csswg-drafts

As a custom element author, it's not uncommon to create an element that utilizes some sort of dynamic "popover." There are many examples of this, but a few common ones include:

  • Dialogs
  • Dropdowns
  • Combo boxes
  • Tooltips

In most cases, such components require a "panel" element that "pops over" the page content. The problem is the panel is subject to containment by its ancestors. For example, a containing <div> with overflow: hidden will [correctly] cause unwanted clipping.

CleanShot 2020-11-05 at 09 20 46

Traditional workarounds for this problem include:

  • "Hoisting" the panel element to another position in the DOM (e.g. before </body>)
  • Using a fixed position strategy to "break out" of the containing element

Neither of these solutions are elegant, nor do they guarantee the panel will be positioned as intended. The former relies on DOM modifications and arbitrary z-indexes while the latter requires that no ancestors have transform, perspective, or filter properties:

It is positioned relative to the initial containing block established by the viewport, except when one of its ancestors has a transform, perspective, or filter property set to something other than none (see the CSS Transforms Spec), in which case that ancestor behaves as the containing block. Source

In the world of web components, particularly when a shadow root is attached, the panel is commonly contained in the shadow root. As a result, it cannot be "hoisted" to another location in the DOM because its styles are encapsulated and slotted content will no longer work. Hoisting also makes accessibility difficult since id attributes can no longer be referenced once they're removed from the shadow root (not to mention potential conflicts with ids in the global scope). This leaves us with only one option — the fixed position strategy.

With the fixed position strategy, there's no way to guarantee a panel's position. You can try to identify its containing block by traversing the DOM and checking for relevant properties, but that's arduous. And if the containing block isn't viewport, "fixing it" will involve altering properties that may cause visible changes to unrelated ancestors.

The Proposal

I suspect this use case will become more and more common as web components become ubiquitous. After many weeks of experimentation, I've come to the conclusion that this could better be solved at the CSS level. Therefore, I would like to propose a new position property:

.my-panel {
  position: popover;
}

Just like position: fixed, an element with position: popover will be removed from the normal document flow, and no space is created for the element in the page layout. Unlike fixed, the element will be positioned relative to the viewport, not its containing block. This makes sense since popovers are pseudo-modal and seldom appear off-screen.

And since naming is hard, a few alternatives might be position: overlay, position: anchored, or position: viewport.

css-position-4

All 9 comments

I remember wanting a solution for use cases like this at least a few times. But is it really a new positioning scheme? I'd prefer something that works orthogonally to position. The goal is to allow the element to be hoisted for some parts of styling as much as possible, breaking from ancestors' context. I see two main differences from regular behaviour:

  • painting order: we want it on top of other stuff, even that in other stacking contexts;
  • overflow: the element should overflow its ancestors even if they normally don't allow it.

Of course this also needs working out the details. Probably contain should trump hoisting. Maybe there should also be a property preventing descendants from being hoisted further. Not necessarily a binary one – perhaps named hoisting contexts would be useful.

I think it makes sense and will be easier to implement as a positioning scheme. The way I see its behavior working is almost identical to position: fixed except without the containing block behavior.

I'm interested to get some feedback from others in the community.

This would introduce the same issues that making transform / filter being fixed-pos containing blocks fixes. Transformed / filtered content should be painted atomically.

Should position: popover nodes be affected by ancestor transforms and filters? If so, how is that supposed to work?

Ideally, they wouldn't be affected. Opting in to this positioning scheme will effectively "hoist" an element visually for the purpose of breaking it out of overflows and avoiding all transforms/filters. Perhaps it makes sense to think of it as a new stacking context that gets drawn above everything else.

While this mostly makes sense in the context of an element living in a shadow DOM, it's not exclusive to that use case. The same behavior can benefit any sort of utility that needs to hoist elements programmatically for the purpose of breaking out of overflows.

Thinking about this more, an alternative might be a property that allows an element to "escape" any overflow that affects it.

.my-modal-thing {
  escape-overflow: x | y | all | none;
}

With this, we could rely on existing positioning schemes and achieve a similar effect. But personally, a positioning scheme feels more appropriate here.

Either way, I wanted to start the discussion and gather some feedback. I'm very open to alternative ideas that solve the same problem. 😄

I second that some things should not be escapable, such as contain or things like clips/bounds on iframes.

I'm worried that a feature that says "i escape all of my ancestors clips" can break some optimizations that we may have. For e.g. strict containment essentially says that the elements bounds contain all of the pixels of its contents, which means that if the element is off-screen then you don't need to paint any of its subtree. If something can escape it though, this is no longer true.

Did you have a specific set of things in mind that you think this property should ignore / escape?

I'm worried that a feature that says "i escape all of my ancestors clips" can break some optimizations that we may have.

I'm not sure it would break optimizations, but it's a fair point to consider. In the case of position: popover, the affected element would be virtually hoisted so we can safely ignore it when ancestors are drawn. Similarly, changes to how the popover is drawn won't affect ancestors. Both can be drawn independently.

Did you have a specific set of things in mind that you think this property should ignore / escape?

I'm debating with myself whether display: none in an ancestor would cause a position: popover element to be hidden or not. I don't think I have a preference here.

However, it should break out of overflows and clips. Overflows are the most common cause of frustration, but I've seen clips used cleverly to achieve similar results and it would be difficult to guarantee the popover element is visible if they didn't break out.

The main reason why I don't consider it a positioning scheme is that you provided no description of how any positioning properties would be interpreted differently for popover. And indeed in the considered examples, including the one in your picture, authors would rather like to relate to the element's context in the box tree in specifying its position.

We considered requirements for interactions and for lack thereof with different, as yet quite vague sets of properties. Let me take a stab at a simple spec taking into consideration only the most important ones: overflow (without separating the axes for now), clip-path, z-index and contain. I propose we start from there and see what's needed and achievable in the first level.

Name: popover (to bikeshed)
Value: # || icb | none
_Edit:_ There is <selector> before the hash but GitHub swallows it.
_Edit:_ Replaced && with ||, that's what I meant.
Initial: none
Applies to: all elements
Inherited: no
Computed value: as specified (or with <selector>s canonicalized)
Animation type: discrete

If the value is not none, let B be the nearest ancestor of the subject matching any of the given selectors. If there is none and the value contains the keyword icb, let B be the initial containing block. If B is still undefined or there is an element with contain other than none (but cf. ISSUE 3) between the subject (exclusive) and B (inclusive) in the subject's ancestor chain, the used value is none. Otherwise, disregard overflow and clip-path on the subject's ancestors which are descendants of B when rendering the subject. Additionally, if the subject is not a child of B, take the subject out of the stacking context in which it would participate and insert it into the stacking context in which A participates as if it were the following sibling of A, where A is the subject's ancestor which is a child of B.

Open issues

  1. A syntactic nit to iron out: icb and none are valid selectors. (Use parentheses?)
  2. Is there a need for a viewport value? Probably not, given this property's orthogonality to position.
  3. Add more refined (and potentially controllable) interaction with contain instead of fully trumping in all cases?
  4. Consider potential interactions with transform, filter, perspective, mask, mask-image and mask-border.

I stumbled over the same problem just now while thinking about converting our angular combobox to a web component.

I think that dropdowns/tooltips and dialogs are different scenarios:

  • Dropdowns/tooltips are usually positioned relative to the trigger (like a position relative wrapper around both and position absolute on the dropdown/tooltip).
  • Dialogs on the other hand are usually positioned in the middle of the viewport.

So in my opinion it shouldn't be a new css position value. It would be more flexible for the developer to have a new css property which can be combined with position absolute/fixed.

About the transform question: At least for dropdown/tooltip implementations it would probably make sense if the transforms are applied.

For my usecase (dropdown of a combobox) it would be best, if it would behave the same as the dropdown of the native <select>.

Perhaps this issue should be renamed to reflect the use case, not my proposed solution. I'm happy it's got the discussion going, but I'm definitely open to alternative ideas whether it be a new property or an extension of an existing one. 😄

Was this page helpful?
0 / 5 - 0 ratings