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-conditional] Applying styles based on an elements size (@media for elements, container queries) #3852

Closed
mtom55 opened this issue Apr 19, 2019 · 17 comments

Comments

@mtom55
Copy link

mtom55 commented Apr 19, 2019

For: https://drafts.csswg.org/css-conditional-3/

Although it's possible this might need to be applied to a different part of the specification.

PROBLEM:
It is possible to apply styles to an element based on the viewports size, but not to the elements size.

Take the following example:

<!doctype html>
<html>
    <body>
        <div class="container">
            <div class="menu">
                MENU CLOSED
            </div>
            <div class="main">
                SomeText
            </div>
        </div>
    </body>
</html>
* { 
    box-sizing: border-box; 
    margin: 0;
}
html, body { height: 100%;}

body {
    display: flex;
    flex-direction: column;
}

.container {
    flex: 1;
    
    display: flex;
}

.menu {
    width: 100px;
    border: 1px solid red;                 
    margin: 20px;
    padding-top: 20px;
    color: red;
    font-size: 18px;
    text-align: center;
}

.main {
    flex: 1;
    
    margin: 20px;
    padding: 20px;

    border: 1px solid blue;
    text-align: right;
    font-size: 80px;
}

@media (max-width: 500px) {
    .main {
        color: green;
        font-size: 20px;
    }
}

image

Using a media query I can change it so that as the width of the viewport shrinks we can reduce the size of the text so that it fits like so:

image

but if the we expand or contract the menu (i.e. if the user presses a button and we use javascript to change the width of the menu), there is no way currently in CSS to change the style based on an elements size.

image

Currently we have to use ResizeObserver (or mutation observer in browsers that don't support it) to monitor the size of the element and then remove and add a CSS class which we can then use to apply the style.

POSSIBLE SOLUTIONS:

A possible solution would be to add a CSS selector like:

.main::conditional(max-width: 500px) {
    color: green;
    font-size: 20px;
}

As a minimum viable spec, it would be nice to support max-width, min-width, max-height, min-height but it could be expanded to include all the current media options.. for example:

.main::conditional((min-width: 500px) and (orientation: landscape)) {
    color: green;
    font-size: 20px;
}

.main::conditional(min-width: 500px) .childElement{
    color: pink;
}

CONSIDERATIONS:
The widths and heights of each element would need to be calculated first. Then the conditional arguments would need to be run through altering the widths and heights of the elements.

The issue with running it in javascript, is that you can get a flickering as the ResizeObserver will get triggered on every change and the css you are applying can break the condition (i.e. by changing the width).

By moving this into CSS it's not longer an issue because the resizing only happens once.

USAGE:
I've discussed this in depth with a large group of web-developers and we think this would be very useful, however the syntax and mechanics are not quite obvious.

@tabatkins
Copy link
Member

Thanks for the issue!

Container queries / element queries have been discussed in this group for quite a while. The fundamental issue with them is that they're selectors that depend on layout, and so they're fundamentally cyclical in a way that can't be supported without complicated hacky business.

ResizeObserver, by placing itself outside of the CSS layout engine and putting the responsibility of handling cyclic changes on the author, is the current best solution to this sort of thing.

@gregwhitworth
Copy link
Contributor

gregwhitworth commented May 18, 2019

@tabatkins I understand this, can we possibly use this issue to take our twitter thread and start thinking of a way to make this happen. Especially since I'm digging further into components - this will be needed (eg: just look at the video element in Chromium 😉). I think there are a few approaches to potentially solving this (there may be more, haven't thought this aspect through too far). Feel free to correct me where I may have things wrong.

  1. @dbaron approach (which I think you mentioned was yours as well) which is is to base this off of containment thus we know that the children can't impact its parent and thus we can resolve styles against layout once it is resolved. This has the positive side of being synchronous and thus works as an author would expect CSS to work today. This would however require an additional cascade/layout pass since we can't compute and cascade the appropriate styles until after layout has completed. This also has the unfortunate benefit of not being able to adjust the layout due to the containment necessity to establish the contract for completing cascade/layout cycle.

  2. We add an async keyword to CSS, which authors are used to and would actually just hook into ResizeObserver under the hood. This could take on any type of naming but I personally would lean towards just keeping the resize-observer name by making it an at-rule and taking a dimension property with computer length, similar to the JS method. So @resize-observer(calc(width >= '500px')) {...}. This would allow the UAs to detect the cycle issues and abandon based on the ResizeObserver specification. I realize I also just introduced some additional logic notations that would need to be added to syntax and calc() (or we can say that these only work in @resize-observer but I can see them being beneficial within calc in general scenarios.

  3. Both. I think there is value to 1 and 2 for different reasons. I'm also open to other ideas but I think the moderate success of ResizeObserver and constant desire for this feature we should finally spec out something that makes sense. I'm fine adding it to RO L2 and we can hash it out later?

Thoughts?

ccing: @dbaron @FremyCompany

@dbaron
Copy link
Member

dbaron commented May 18, 2019

An issue with (2) is how to do the cycle detection. If you separate the ResizeObserver work that triggers this from other ResizeObserver JS code (in separate flushes of style and layout), then it seems plausible, but doing that separation seems like it would have a substantial performance cost if both are in use. (Or are you referring to an existing cycle-detection mechanism? I don't see the word "cycle" in the ResizeObserver spec.) Without that separation it's harder to see how you distinguish cycles from JS code doing things.

@gregwhitworth
Copy link
Contributor

Currently ResizeObserver already adds a microtask to the queue when layout completes and the observer matches. The looping issue is covered in 3.6.1. I'll also cc in @atotic since he did the implementation of this work to possibly add clarity on this further if necessary.

@mtom55
Copy link
Author

mtom55 commented May 18, 2019

The main way I see this being used to style the contents of an individual "component" so that it fits based on the size of other components.

For example, in a real life example we have 10 different components in a row on a page (with 2px solid blue).
image

When the menu opens thought, the images need to switch to display vertically, quite often with style changes to hide some elements, and change the spacing:
image

If this were able to just trigger changes to the css that only applied within the target element border edge and within I think this would solve all the use-cases that I can think off (i.e. it doesn't have to be able to apply styles that would affect other elements).

In an example where we have to apply different css once a component (element) goes below 900px, Currently our solution involves using using ResizeObserver to insert a class onto that component once the box goes below 900px, but because one change in one component can affect another component (i.e. they trigger multiple ResizeObserver events) we can only remove the class once the element resizes back over 930px (+30px) to prevent "flickering".

@eeeps
Copy link
Contributor

eeeps commented May 18, 2019

The best explanation of RO’s depth-based loop-breaking is probably here: WICG/resize-observer#7

Note that it successfully breaks out of loops within frames, but not across sequences of successive frames. So while it does prevent you from bricking a thread/never painting again, it does not prevent flickering between a cycle of states.

(Also interesting to note that it’s basically what @ausi implemented in his cq-prollyfill library https://mobile.twitter.com/ausi/status/873464855843524608)

@FremyCompany
Copy link
Contributor

FremyCompany commented May 18, 2019

Yeah, and it's very concerning that this kind of loop might be so subtle you would not notice it on the screen but it would basically kill the performance of your site and drain your user's battery by causing a new layout and a new paint every frame at 60fps. That's why I would not recommend to standardize the RO-based pipeline.

(Especially concerning is that this might only happen in a specific scenario which might not happen on the developer's device and might be very difficult to diagnose after the fact, because it might need the resize to happen at a certain pair of points between two frames, which might only happen when you rotate the screen of a specific device, or resize your window quickly, or when the touch keyboard appears on certain OSes depending on the screen height, etc etc etc you got the idea)

@mtom55
Copy link
Author

mtom55 commented May 19, 2019

Three thoughts:

1. Element can not Resize
Would one solution to fix the looping would be for the elements that you have added this, in the layout stage apply an exact width and height to the element (which can not change) which would stop the contents pushing it out (causing loops, flickering) which means a change to the css of the contents would not cause a re-layout / loop / flickering.

Some thoughts though, obviously this works for items that have box-sizing: border-box; but not for the standard box model.

~ sibling selectors couldn't be supported by this

2. Secondary Layout is last and only occurs once
The other solution that occurs to me is that the secondary layout / paint stage is the last and only happens once, even if it breaks the conditions of the original css.

For example, lets say you have the box and below 900px you are going to apply a border: 2px solid pink;

After the element drops to 899px, a 2px border is applied increasing size to 901px breaking the condition but because condition on the first run through it does not then do a third run through.

The reason I'm not sure about this particular method is that if you have multiple conditional css based on size on nested children.

3. Ranges
If you go below 900px it triggers the change, but you have to go above 930px to trigger it back, this stops loops and flickering and this our current solution in javascript and works for nested children.

@gregwhitworth
Copy link
Contributor

gregwhitworth commented May 20, 2019

@FremyCompany While I get the worry, I'm not sure if this is something that should block on - especially since it is already available in JS, not only in RO but also using window.onresize where this same scenario can occur. I'm fine trying to hone in some type of capability to further restrict RO so that it avoids un-necessary firings, such as recommended in #3663 which I believe is similar to what @mtom55 is stating regarding ranges. This is also similar to threshold's in intersection observer; again something an author can have firing numerous times depending on the setup of the page, how many observers are created, etc - and positioning which can adjust based on the same things you've denoted.

Finally, a synchonous version - which we had in msElementResize cause us numerous performance issues, as you know, so I think having the flash is a solid trade off to keeping async.

I'm personally kind of tired (as you know since we worked right next to each other for so long) of this being punted on, especially since we have this in JS and it's effectively syntactic sugar at this point if you put this in CSS and build it on top of resizeObserver. I'm open to suggestions so if you have some please let me know.

@mtom55
Copy link
Author

mtom55 commented May 20, 2019

Matthew Dean has a very nicely written proposal which touches on a lot of interesting points: WICG/container-queries#12

@FremyCompany
Copy link
Contributor

FremyCompany commented May 20, 2019

a synchonous version - which we had in msElementResize - caused us numerous performance issues

It is not because it was synchronous that it was causing issues, it is because the only way to use this was to trash layout, and you need to trash layout O(n^2) for the amount of breaks you have in your code, so with just two nested layers of observation you already make layout 4 times slower. Plus it is linear to the amount of observations you have, and many used this for children of a list so layout was thrashed dozens of times as all the items would get notifications and trash the layout before the next one did. So, sure, if you trash layout dozens of times, synchronicity is then problematic because layout then blocked the main thread.

Async doesn't make things more performant, tough, it actually makes things way slower (now you are also trashing paint on top of layout), but at least the page stays responsive because the main thread isn't busy looping: it has some breath of fresh air between the layout wastes. This isn't a solution, the solution is to not waste layout, and we have all the tools needed to accomplish that goal cleanly today, so I see no reason why we would settle for less.

I think having the flash is a solid trade off to keeping async

This very thread is a testimony that this is not true. I'm quoting here:

The issue with running it in javascript, is that you can get a flickering as the ResizeObserver will get triggered on every change and the css you are applying can break the condition (i.e. by changing the width).

By moving this into CSS it's not longer an issue.

Using the same pipeline as RO to achieve the desired results will not solve any of the problems people are facing with RO today and which make them say this isn't the solution they want (while still admitting it is the solution they need, because there isn't any better yet).

@gregwhitworth
Copy link
Contributor

It is not because it was synchronous that it was causing issues, it is because the only way to use this was to trash layout, and you need to trash layout O(n^2)

Yes, I know that, but the synchronous version made this a terrible end user experience, thus compounding the issue of doing layout numerous times.

Using the same pipeline as RO to achieve the desired results will not solve any of the problems people are facing with RO today and which make them say this isn't the solution they want (while still admitting it is the solution they need, because there isn't any better yet).

Again, I am not opposed with a longer term solution that is both synchronous and avoids the layout thrashing. Assuming that can be done and I also stated that I was for having an async & synchronous version so we won't need to worry about webcompat for the synchronous version. That said, the ways in which people have proposed to solve this issue, in the past, makes it so the use cases that web developers need, not work which defeats the purpose entirely. Seeing as how we haven't come up with a solution that both solves the pain points of web developers (bringing CQ to CSS, rather than in JS alone), and end-users, by keeping the main thread from being blocked; I think bringing RO into CSS is a step in that direction. At the very least it's gotten us talking about it further 😉

Finally, as denoted above - and I just want to repeat for emphasis, if you (or anyone) have any suggestions on how we can both solve web developer's use cases while also avoiding layout thrashing and thus allowing it to be synchronous; I happily welcome them.

@ausi
Copy link

ausi commented May 20, 2019

if you (or anyone) have any suggestions on how we can both solve web developer's use cases while also avoiding layout thrashing and thus allowing it to be synchronous; I happily welcome them.

A large portion of use cases, for which web developers want container queries for, can be solved – I think – by CSS conditions that work with custom properties (aka CSS variables).

I proposed such a feature on WICG Discourse two years ago and wrote a short article about how I believe this can be a solution to most container query use cases. It doesn’t require layout thrashing and cannot result in endless loops.

This year I discovered that CSS animations can be (mis)used to build something very similar. It is far away from being useful in production, but it shows that browsers and their CSS engines are capable of handling such a feature (in contrast to container queries). A short demo of this technique is available on CodePen and shows that CSS conditions are perfectly suitable for the main use case described in https://wicg.github.io/cq-usecases/

UPDATE: @gregwhitworth I just saw that you’ve already read my proposal in WICG/container-queries#5 (comment)
Sorry for double-posting ☺️

@imkremen
Copy link

As I understand, cycling issue can be resolved with adding contain: size to element.

@eeeps
Copy link
Contributor

eeeps commented Jun 1, 2019

@imkremen my understanding is that there are two proposals that just might work:

  1. Something based on contain: size’s behavior. This is pretty restrictive; we would certainly want single-axis size containment in order to allow elements to grow in the non-queried dimension to fit whatever content is placed within them.

  2. Querying the space available for the element, rather than its actual size.

This lets you query any element without restricting or modifying its layout, but is, I think, harder for authors to "see," and reason about. There may be other fundamental issues with how this strategy would restrict layout performance, which @bfgeek discussed in the Twitter thread, and which I won't pretend to understand.

Basically: an author writes a potentially-circular container query. Do we want that query to change (or require changes to) the actual layout of the queried element, to remove the potential of infinite loops? Or do we want to query something other than the element's size, in order to remove that potential?

@dbaron dbaron changed the title [css-conditional] Applying styles based on an elements size (@media for elements) [css-conditional] Applying styles based on an elements size (@media for elements, container queries) Jun 2, 2019
@fantasai fantasai added css-conditional-5 Current Work and removed css-conditional-4 labels Dec 25, 2021
@mirisuzanne mirisuzanne added css-contain-3 and removed css-conditional-5 Current Work labels Feb 8, 2022
@mirisuzanne
Copy link
Contributor

I think we can safely close this issue, now that a Container Query spec is officially in Working Draft, and development is underway in multiple browsers?

@mtom55
Copy link
Author

mtom55 commented Feb 8, 2022

Container Queries thoroughly solves all the issues in this ticket. Thank you.

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