w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.35k stars 641 forks source link

idea: CSS non-multiplicative opacity-override property #10214

Open trusktr opened 2 months ago

trusktr commented 2 months ago

Sometimes we'd like to make a parent/ancestor have opacity less than one, but would like some child not to be affected or to have a different opacity.

This can sometimes be achieved with background-color: rgba() when it is specifically the background content that should have different opacity than children, but it solves the problem only part of the time.

It would be nifty to have a way to override the opacity of a child such that it is not multiplied with any ancestor opacity, but instead starts with a value as if ancestor opacities are all 1.

For example, currently, the following results in an ancestor having visual opacity 0.5, and a descendant having visual opacity of 0.4:

.some-ancestor {
  opacity: 0.5; // we see 0.5 for ancestor content
}

.some-descendant {
  opacity: 0.8; // except for this descendant we see 0.4
}

With a new property named opacity-override (or something) we could force the descendant to have a specific opacity not derived from multiplying with ancestor opacities:

.some-ancestor {
  opacity: 0.5; // we see 0.5 for ancestor content
}

.some-descendant {
  opacity-override: 0.8; // except for this descendant we see 0.8
}

This would be very useful for cases when we'd like to dim all content except to highlight a particular piece of the content, without having to resort to drastic changes to DOM structure (someone may only be editing CSS, and does not want to have to refactor code spread across UI components, for example). In this use case, we might want to use opacity override 1 so that all content in some ancestor is faded out except for some highlighted descendant:

.some-ancestor {
  opacity: 0.5; // we see 0.5 for ancestor content
}

.some-descendant {
  opacity-override: 1; // except for this descendant, it visually has full opacity (focused or highlighted)
}
cdoublev commented 2 months ago

.some-ancestor *:not(.some-descendant) { opacity: 0.5 } is quite straightforward. Besides, opacity applies on a flat rendering of the ancestor and its children, so I fear opacity-override could not be applied:

Conceptually, after the element (including its descendants) is rendered into an RGBA offscreen image, the opacity setting specifies how to blend the offscreen rendering into the current composite rendering.

trusktr commented 2 months ago

.some-ancestor *:not(.some-descendant) { opacity: 0.5 }

That's not the same, that will apply to anything that is nested, which can totally break the desired effect.

Besides, opacity applies on a flat rendering of the ancestor and its children, so I fear opacity-override could not be applied:

I'm sure it is possible to update the spec so that opacity overrides are applied within the offscreen image in a step prior to compositing that offscreen ancestor, etc.

tabatkins commented 2 months ago

The issue here is that descendants' opacities aren't "derived from multiplying with ancestor opacities". A descendant with opacity: 1 has opacity 1 - it's fully opaque. But the partially-transparent ancestor draws all of its descendants onto itself, as a single graphics layer, then applies opacity to that single resulting image. This is very very different from each element painting itself partially-transparent and then all stacking together (which is roughly what happens with @cdoublev's example code).

Because everything is composited together into a single image, if you want something to not be composited with the group, it has to exist fully outside of the group - either completely above everything or completely below everything. Outside of some rare exceptions (top layer, mostly), the DOM's painting model implies that the element isn't a descendant of the partially-transparent ancestor at all.

Fundamentally, what's going on here is the difference between group opacity and item opacity. You can find tons of tutorials explaining the same thing (and people asking this exact same question) for graphics programs.

LeaVerou commented 2 months ago

Just a quick note that while I'm not sure if this is the best solution, big +1 for addressing the pain point: I've definitely had this problem numerous times.

ydaniv commented 2 months ago

Adding to what @tabatkins mentioned above, the only way this could be achieved, I think, is if we could instruct the browser to somehow scope elements by isolating them into separate layers, kind of like @scope does for styles, and then the browser should somehow "merge" these layers, instead of compositing them, kind of like feMerge does.

But this is just complete theoretically sci-fi talking.

xiaochengh commented 2 months ago

Another sci-fi talking: allow multiple layer trees / paint property trees with non-similar structures. For example, tree 1 for transforms where the .some-descendant is in .some-ancestor's layer, and tree 2 for opacity where .some-descendant breaks out of .some-ancestor's subtree.

trusktr commented 2 months ago

@tabatkins yeah I understand the spec specifies that, but it is something I bet most people don't know, and the behavior most people see most of the time is basically the equivalent of a simple multiplicative opacity in terms of mental model.

Is there a way to make an element break out of the graphics layer into its own (while the parent has opacity < 1)?

proposal

How about updating the spec so that at least transform-style:preserve-3d content is not flattened (so it actually preserves 3D like it says!) by breaking out into a new layer, and is also not affected by ancestor opacity (the layer opacity starts over at 1)?

This would solve the OP because to "highlight" an element you could break it out in 3D space (even just a small amount that is not noticeable by the naked eye) and it would be its own new graphics layer with its own opacity, without having to change the shape of the DOM tree and using only CSS styling.

This would also be more like how actual 3D engines work. Flattening, f.e. as if setting a 3D node's scale.z to zero, when the tree node has opacity < 1, is nonsensical in all 3D engines that I know of. This is very surprising behavior for 3D in CSS.