Csswg-drafts: [css-contain-2] do we need size containment in a single dimension to enable container queries?

Created on 14 Feb 2017  Â·  25Comments  Â·  Source: w3c/csswg-drafts

One thing that came up at the extensible Web summit in Boston on Friday was a discussion of container queries.

While I'd previously suggested tying container queries to CSS containment, one thing I realized is that there will be cases where developers want to fix the width from the outside but still allow an auto height, and then do media queries on the container's width.

I haven't thought this through very much, but it seems to me that this may require a concept of layout containment in a single dimension to be exposed from containment.

css-contain-2

All 25 comments

Specifically, what would be needed is 1d size containment.
https://drafts.csswg.org/css-containment/#containment-size

I think container queries require both size containment and layout containment.

I agree. What I meant is that you should be able to apply regular layout containment (which we already have) together with 1d size containment (which we don't) to get the effect you described.

So authors need both, but the WG only needs to add 1d size containment, since we already have the rest.

The CSS Working Group just discussed , and agreed to the following resolutions:

RESOLVED: Level 2

The full IRC log of that discussion

<fantasai> Topic:
<Florian> github topic: https://github.com/w3c/csswg-drafts/issues/1031
<fantasai> github topic: https://github.com/w3c/csswg-drafts/issues/1031
<fantasai> Florian: Next one from dbaron...
<fantasai> dbaron: This one is a big issue
<fantasai> dbaron: One of the things that a bunch of web devs really want is what they call "container queries"
<fantasai> dbaron: which basically addresses the sue case of teams that develop widgets or modules that are part of a bigger page
<fantasai> dbaron: Their developing some markup and script and whatever that gets included within a bigger page, and it will have some size
<fantasai> dbaron: The bigger page might use meida queries to e.g. switch from 3 columns to 2 colums, and widgte gets bigger though viewport got smaller
<fantasai> dbaron: If you're implementing the widget, you want to respond to the size that the widget is, not the size of the viewport
<fantasai> dbaron: Bunch of ppl want container queries that actually work, rather than do what ppl do right now which is do layout, flush, and set styles based on that
<fantasai> dbaron: Seems to me it should have some relation to containment
<fantasai> dbaron: that is, ability to do container queries should depend on some kind of ocntainment so that your insides dont depend on your outsides which depend on your insidees
<fantasai> dbaron: The next point is that sometime sppl want to do container query on their width, but have an autho height
<fantasai> dbaron: So, was thinking we want to have layout containment in only one dimension
<fantasai> dbaron: Beyond that haven't thought about it, so this is a "please design me a feature" issue
<fantasai> dbaron: This seems like a relatively high priority feature because ppl do this a lot, and do it by doing flush-restyle loops
<fantasai> Florian: If theyr'e willing to go that far, using 2D size containment is good, set it and then reset your height after you do layout
<fantasai> dbaron: Get ppl doing it for multiple parts of the page, so cycles multiple times
<fantasai> fantasai: why not just have -x -y keywords?
<fantasai> TabAtkins: Hard to define what htat means
<fantasai> eae: Maybe sets wrong expectations, that you wouldn't get same perf benefits
<fantasai> TabAtkins: True, but you do get the benefit that when your'e using resize observer you get predictable behavior and not loops
<fantasai> Florian: Seems like level 2, esp we don't have proposal yet
<fantasai> TabAtkins: This plus resize observer plus custom at-rules, I'm hoping will allow solving this use case
<fantasai> TabAtkins: Been my plan for like 10 years
<fantasai> s/10/2/
<fantasai> RESOLVED: Level 2

I would really like this feature to help container query scripts to solve the recursion issue. But is size/layout containment in one dimension even possible?

Take, for example, the following structure (assuming that size-x would be the value for containment across the X axis):

<style>
    .wrapper { height: 100px; overflow-y: auto }
    .component { contain: size-x }
</style>
<div class="wrapper">
    <div class="component">
        Content that grows in height via a container query script
        when the width gets wider.
    </div>
</div>

In an edge case this could still lead to an endless loop I think: The width of .component depends on the content-with of .wrapper and the content-width of .wrapper depends on the height of .component (because of the scrollbar). This means that the width of .component would still depend on its contents even though contain: size-x is set.

@ausi containment only works on the box that it is applied to; so, in your scenario, you're only containing size-x for .component. Thus whatever size-x is defined to do will occur for its content and will then propagate the resulting geometry to .wrapper. Regarding scrolling, it this geometry results in the height being over 100px then you re-layout .wrapper with the scrollbar in place (this is how scrollbars work today)

Regarding scrolling, if this geometry results in the height being over 100px then you re-layout .wrapper with the scrollbar in place

But wouldn’t this re-layout update the width of .component?

@ausi Yes - but that is how it works today because in scrollbar auto we don't know if we should show one or not, so if you need one we have to do a second pass. Now, based on the containment set you may NOT get to 100px so you won't have that relayout, but again this isn't new.

The relayout would change the width of .component, so ultimately the width of .component depends on it’s contents even though contain: size-x is set.

As far as I understand it, this means that implementing contain: size-x in a browser is impossible, because it cannot be assured that the size in one dimension doesn’t respond to changes to the size in the other dimension.

@ausi I mean it's a valid wrinkle, but personally, I think authors would be ok with this type of "two pass" layout as they normally would want the scrollbar to appear, and probably aren't even noticing (in most cases) that two passes are required. As a result, you can still honor the constraint but only once you've answered the question of "does it have scrollbars?" or not. @frivoal @dbaron thoughts?

I think the “two pass” layout for scrollbars is basically OK for authors. The problem arises if a container query script comes into play.

The spec for contain: size says:

Its primary benefit on its own is that tools which want to lay out the containing element’s contents based on the containing element’s size (such as a JS library implementing the "container query" concept) can do so without fear of "infinite loops", …

The same “no infinite loops” benefit would be required for contain: size-x, but this is impossible I think. And without this benefit the feature would be useless.

I wonder if this can be solved by making size-x work only when there's a specified width that does not depend on the parent (i.e. no auto, no available, no fit-content, no percentage). This certainly limits the situations in which you can use size-x, but if these are cases which would not work anyway, I think that leave us with cases where it does. Not sure it is still sufficiently useful though.

Alternatively, since I believe the problem is exclusively caused by an ancestor having auto scrollbars, maybe that's what we fix: if an overflow:auto element has a descendant with 1d size containment (2d also? not sure), then the scrollbar must be always visible, regardless of whether there is actual overflow or not (with an allowance for overlay scrollbars).

I wonder if this can be solved by making size-x work only when there's a specified width that does not depend on the parent (i.e. no auto, no available, no fit-content, no percentage). … Not sure it is still sufficiently useful though.

This would work I think, but it would not be useful for container query scripts anymore.

I believe the problem is exclusively caused by an ancestor having auto scrollbars

Unfortunately, I don’t think scrollbars are the only case this could happen. Take for example this code of a contain: size-y example (CodePen):

<div class="parent">
  <div class="inner">
    <div class="child"></div>
  </div>
</div>
<style>
  * { box-sizing: border-box } /*edit: this line was originally omitted, and was added back in later */
  .parent {
    float: left; /* this makes the width depend on its contents */
    height: 100px;
  }
  .inner {
    padding-top: 10%; /* this percentage is relative to the width */
    height: 100%;
  }
  .child {
    height: 100%;
    contain: size-y; /* would not work in this case */
  }
</style>

(2d also? not sure)

Two-dimensional contain: size is not affected because its specified behavior is “When laying out the containing element, it must be treated as having no contents.”

(edit: this comment was written before box-sizing: border-box was added to the example in the previous comment)

Interesting example.

The height of .inner depends on the height of the content area of .parent , which is fixed at 100px.
So the height of inner is also 100px, regardless of what happens to its padding-top.
The height of child is 100% of the content height of .inner, so 100px again.

The fact that padding-top changes when the non-contained x-axis changes means that the position of child will change depending on its width, but it's height will not.

contain:size-y means that size changes changes within .child that would affect its height don't, so the height of inner or up don't change because of that.

So we don't have a loop here: yes, width changes in .child do cause vertical layout changes in ancestors, but those don't affect their content height, and there's no impact on .child's content height…

However, if you add box-sizing: border-box on .inner, then there is an effect on .inner's content height, and we do have a problem. Damn.

Unlike scrollbars, I don't see any simple way to fix that one.

However, if you add box-sizing: border-box on .inner, then there is an effect on .inner's content height, and we do have a problem. Damn.

Yeah, I forgot to set the box-sizing in my example (it was only in the linked CodePen).

Unlike scrollbars, I don't see any simple way to fix that one.

Neither do I. I would really love this feature, but the more I think of it, the more impossible it seems to me.

@ausi Thinking again about your example, I think it might not be as much of a problem as I initially though: technically, it's still an issue, but:

  • this uses 1D block-direction size containment, but the thing that authors are mostly calling for for container queries is 1D inline-direction size containment
  • This example relies on how block direction percentage padding resolve against the inline dimension, but the opposite isn't true, so I don't think this example has an equivalent with axes flipped.

So, maybe axis agnostic 1D size containment isn't possible, but maybe 1D *inline-direction size containment is?

Well, we still have the problem of scrollbars, but that is a more tractable one. Or are there more situations where this breaks?

So, maybe axis agnostic 1D size containment isn't possible, but maybe 1D *inline-direction size containment is?

That would be great news.

Well, we still have the problem of scrollbars, but that is a more tractable one. Or are there more situations where this breaks?

I don’t know, margin/padding came into my mind when thinking about “what css size in one axis gets influenced by the other axis”. I can’t think of another one just now.

I can’t think of another one just now.

But with the property writing-mode you can flip how margin/padding works and can create elements whose width depends on the height of their content: https://codepen.io/ausi/pen/MWeqwNV?editors=1100

Maybe it would be possible to “disable” such features if single-axis containment is used?

I've been working on this with @andruud, and we have a few ideas for how we might work around the issues. I don't think these two solutions would necessarily be exclusive, but the first certainly feels "simpler" -- and might help us avoid solving the second in more detail.

The "pinky promise" (no containment)

Our favorite option is to avoid the need for size containment altogether, if we can. What if:

  • We evaluate the CQ against the size the container _would have_ if contain:size was specified.

    • A CQ then implies a _promise_ of containment from the author, at least for the axes present in the CQ expression.

    • Layout proceeds without size containment. The actual result of the layout has no effect on CQ evaluation.

    • In other words, if the author breaks their promise, the CQ will still evaluate as if they hadn't. (The CQs won't be evaluated again).

  • For the problematic "ancestral scrollbar" case, we'll automatically evaluate the CQ twice (since we're doing layout twice), so the second pass would have the CQ evaluate against the scrollbar-aware size.

    • However, if that second pass changes CQ evaluation such that the scrollbar wouldn’t be needed after all (e.g. by setting things to display:none when the container is below a certain width), then the scrollbar remains.

There is some danger that this makes it too easy for authors to stumble into "promise-breaking" behavior, where the container query reports a size significantly different from the final layout dimensions. But the advantages might be enough to offset that concern. It might be worth testing in a prototype.

Making 1D containment work

If 1D containment is needed, it looks to me like that would require making some hard decisions about how the cases above _fail consistently_ while maintaining containment. This isn't ideal, but may also be a worthwhile tradeoff for authors. It would take some more discussion, but something like:

  1. For the sake of determining auto scrollbars on ancestors, the container contributes an infinite cross-axis size (always trigger the scrollbar). This is probably the more common edge-case, but I also think auto-scrollbars _might often_ imply an element has containable size on the cross-axis – so authors could avoid this by using 2D size containment in those cases?
  2. For the sake of resolving percentage-padding on the contained axis, always resolve to auto. Maybe there's a better solution here, but I think this is the existing first-pass behavior in many cases where an element has unknown size, so it's a starting-point.

If we can do the "pinky promise" approach, that might be a nice way out: this would be something you could warn about in dev tools ("the size queried against was changed! don't do that!"), and wouldn't need us to introduce 1d containment at all.

I am afraid this is a pretty risky/complicated move though, because, applied naively, I think it would make layout stateful.

The CQs won't be evaluated again.

Taken at face value, that assumes that layout is only ever done once, for the whole page. But there are a bunch of case where something changes and the layout of the part of the page needs to be recomputed. You can get out of that by saying that even in those cases, you answer the query not from the current state of the page, but based on the size the element would get if the page was being laid out from scratch, as if the queried elements had been sized as if contained and had not had a chance to influence its ancestors.

That could also create different layouts depending on the order in which you evaluate & layout the container queries. You can get out of that by defining it so that all container queries in a single page are answered as if all queried elements had been sized as if contained (and had not had a chance to influence their ancestors).

However, either of these means that to answer container queries in relayouts or in the case of multiple queried elements, you cannot just use the layout of the page as it is, and you need to know, either through caching or through recomputation, what the page's layout would have been if none of them had been inserted yet, in case some of them do change their ancestors.

That looks solvable, but expensive (and bug prone).

As for "Making 1D containment work", assuming we've identified all the problematic cases, I think you're thinking along the right lines, and that something like that would solve it. But have we indeed identified all the problematic cases?

I agree there are likely more 1d containment problem-cases. But I think if we want to go that 1d containment rout -- addressing individual issues as they arise -- we can't be completest about it in the abstract. We'll never know if we have the full list until someone tries building it.

The CSS Working Group just discussed [css-contain-1] do we need size containment in a single dimension to enable container queries?.

The full IRC log of that discussion
<dael> Topic: [css-contain-1] do we need size containment in a single dimension to enable container queries?

<dael> github: https://github.com/w3c/csswg-drafts/issues/1031

<dael> miriam: The context is in thinking about container queries and starting from dbaron initial proposal a few years back

<dael> miriam: Easy to imagine how it works with full size containment, but doesn't work for most cases. Works in app-like cases but otherwise falls apart.

<dael> miriam: prop is 1d containment so can contain width of element and query against that but allow height to adjust.

<dael> miriam: Several cases where height of children changes width of ancestors. Scrollbars and % in padding

<dael> miriam: A lot of talk about that making it quite difficult

<dael> miriam: anders and I have been pushing on that.

<dael> miriam: Going through the ideas backwards b/c 2nd one is thinking through how to address issues as they arrise. If we want 1d can we always trigger scrollbar on ancestors, resolve % to auto

<dael> miriam: Not full proposals, but want to push conversation forward

<dael> miriam: That's a start on how might address issues as edge cases so 1d containment would work

<dael> miriam: anders proposed the pinky-promise approach. Idea here is what if we don't require containment on container queries and we resolve the query as if we have full size containment but allow you to make changes so you get different final size then reported

<florian> q+

<dael> miriam: trade offs between but not exclusive of each other. A lot more to resolve on both. Anders isn't here but that's basic context

<dael> florian: Author usibility I think pinky-promise works. Cases where children effect cross axis size are rare and easy to avoid.

<Rossen_> ack florian

<dael> florian: Concerned about implications on impl b/c makes layouts effectively stateful. if doing partial layout you have to do a relayout to figure out size if you hadn't instearted the children you did insert. Order becomes meaningful as well which brings us back to having to do a layout of the anti-page

<bkardell_> q+

<dael> florian: Not impossible, but could be expensive. I'm concerned. THe plug the leaks one by one seems more possible. We haven't come up with so many examples. We've so far had 2. Maybe can plus one by one

<dael> florian: Seems that by far dominant of 1d is query inline size, not block. The leaks for inline and block are not same. Easier to plug inline leaks. If we jsut worry about those maybe slightly easier problem

<dael> bkardell_: In the pinky-promise thing doesn't that wind up negative auto-height?

<Rossen_> ack bkardell_

<dael> florian: No, the pinky-promise case you do a container query against width and then you say I won't do anything in layout what would change the width of the parents. As long as you don't have the cases that is does change width of parents everything is fine

<dael> miriam: One other complication is inside of floats that shrink wrap, container query would return 0 but layout is quite a bit different

<dael> florian: Yeah. But for shrink-wrap this is fundamentally problematic.

<miriam> +1

<dael> florian: If there is a use case for that it's a whole different can of worms

<dael> Rossen_: We're at time. not sure if we have a resolution we could call. I would urge you to continue discussing on GH

<iank_> I'm somewhat not convinced the pinky-promise will yeild the results web-developers expect.

<dael> TabAtkins: Not looking for resolution yet, just general information

The CQs won't be evaluated again.

This poor wording is my fault. If we need to do another layout pass, which can be the case for e.g. the ancestral scrollbar problem, then the CQ _would_ be evaluated again with the scrollbar effects present. However, if the author then conditionally (based on the scrollbar appearance) restyles things such that a scrollbar would ultimately not be necessary after all, then too bad. You get a useless scrollbar. We're saved by the fact that (in Blink at least) we don't re-layout until we get something stable, we layout at most twice (for the scrollbar case anyway).

I don't think it's very practical to maintain an entirely different page-wide reality where some elements are size-contained. I've assumed that we can cheaply and "locally" compute the would-be-contained size for containers whenever they get a new (actual) size. If that's not true, or if we do have "repeat layout until stable" behavior somewhere in a way which can affect the would-be-contained size, then pinky-promise probably doesn't work.

I've assumed that we can cheaply and "locally" compute the would-be-contained size for containers whenever they get a new (actual) size. If that's not true, […] then pinky-promise probably doesn't work.

I believe that's indeed not true, because evaluating them cheaply and locally like that makes layout stateful, which is bad. The current state of the layout should not matter when (re)evaluating the layout. If this is true, when browsers evaluate the layout is an unobservable implementation detail, and they may do it as often or as rarely as they want to, recomputing it for part of the page or for the whole page, and nobody can tell the difference. This is an important quality to maintain, and it rules out the pinky-promise approach (unless we recalculate everything form a clean state every time, but that would be expensive).

Was this page helpful?
0 / 5 - 0 ratings