Skip to content
This repository has been archived by the owner on Feb 16, 2023. It is now read-only.

Container queries via custom properties (aka CSS variables) #5

Open
ausi opened this issue Dec 13, 2016 · 11 comments
Open

Container queries via custom properties (aka CSS variables) #5

ausi opened this issue Dec 13, 2016 · 11 comments

Comments

@ausi
Copy link
Collaborator

ausi commented Dec 13, 2016

I’ve got a new idea for a solution to container queries 🤓

What we already have today

To describe the concept, lets first take a look at what we already have in browsers, imagine the following setup:

:root {
    --context-width: 100vw;
}
.one-third {
    width: 33.3%;
    --context-width: calc(parent-var(--context-width) / 3);
}
.two-thirds {
    width: 66.6%;
    --context-width: calc(parent-var(--context-width) / 3 * 2);
}

Now I can create a component that is in a 16:9 ratio to its context width as easy as:

.component {
    height: calc(var(--context-width) / 16 * 9);
}

No matter how nested the one-third and two-thirds containers are, a component inside them or at the root level would always be in the desired 16:9 ratio. This works already, take a look at this CodePen, although we need to hack a little because parent-var() is not a thing yet.

New concept for CSS conditions

If we take this idea further and introduce a concept for custom property conditions with this syntax:

if( <condition> , <value-if-true>, <value-if-false> )

we could do something like:

.component {
    float: if(var(--context-width) > 200px, left, none);
}

This way we told the component to only float left if it’s in a context (or container?) with more than 200px width, otherwise it should not float. Sounds pretty much like container queries to me 🎉

From the perspective of a browser

My knowledge for browser internals in very limited, but AFAIK one big problem with an implementation of container queries is, that the engine would have to jump between layout and style computation, which could result in unstable results and might be very bad for performance. I think the concept of simple CSS conditions would not suffer from that problem. Taking the example from above, the condition var(--context-width) > 200px can be worked out without the need for a layout. First the var() part would be resolved to something like calc((100vw / 3) / 3 * 2) and further to 22.22vw. This value can then be easily compared to 200px.

Flexibility

Another benefit I see with this solution is, that the CSS author can decide how to set and inherit the variables. If one creates a website which has bright and dark sections, he could set --context-dark to true or false and create components that just look perfect in every place. Such binary conditions already work in browsers today if you are very creative.

Downsides

The main downside of this solution is that we need to set variables for every container that changes the available width for its children. And we would sometimes not get the “exact” value if e.g. a scrollbar reduces the available width.

What do you think?

@tomhodgins
Copy link
Collaborator

The problem I'm seeing with the first example is that they're based on viewport-percentage units, but the elements you'll need to be styling may not scale perfectly with the browser's width and/or height.

What we need to do these kinds of calculations is a unit that is aware of the width and height of the element as it appears on the page (not as we want it to appear). With that information we could write the kind of code in the first examples. Consider the difference in syntax between the two following examples - the first uses JavaScript to check the offsetWidth which is wild, and the second uses ew units to represent 1% of an element's width which is a much nicer abstraction for writing CSS:

<div class=demo-1></div>
<style>
  @element '.demo-1' {
    $this {
      background: lime;
      height: eval('offsetWidth / (16/9)')
    }
  }
</style>
<script src=http://elementqueries.com/EQCSS.js></script>
<div class=demo-2></div>
<style>
  @element '.demo-2' {
    $this {
      background: orange;
      height: calc(100ew / (16/9));
    }
  }
</style>
<script src=http://elementqueries.com/EQCSS.js></script>

I think before a solution like this would be usable, we would need ew, eh, emin, and emax units so the element-based queries could be based on element-based conditions, not triggering element-based queries with browser-based conditions.

As for the conditional if() statements in CSS - I've experimented with this a lot and I think it works well for prototyping, and abstracting away JavaScript - but it's very limiting in that in many situations beyond building a prototype or demo where you would actually want to use this - you'd often find yourself using this sort of if() statement over and over in your CSS rules, leading to a lot of duplicate code that's hard to read or maintain.

Here's a version of your if() example that works in EQCSS syntax, and then some examples of hairier if statements I've written in EQCSS as well so you could see how people would try to use it and where usefulness lies in a feature like that:

<img src=http://staticresource.com/user.png class=component>
<style>
  @element 'html' {
    img {
      float: eval('innerWidth > 200 ? "left" : "right"')
    }
  }
</style>
<script src=http://elementqueries.com/EQCSS.js></script>

And then here are other ways I've used if() statements inside CSS. I think it's incredibly powerful, but very ugly, and the things we use this to try to solve would be better exposed as nicer syntax in CSS:

Min/Max font-size

@element '[data-min-font],[data-max-font]' {
  $this {
    font-size: eval('
      var vw = innerWidth/100*10, /* equal to 10vw */
          min = getAttribute("data-min-font"),
          max = getAttribute("data-max-font");
      if (min !== null && max !== null) {
        vw <= min ? min : max <= vw ? max : vw
      } else if (min !== null) {
        vw <= min ? min : vw;
      } else if (max !== null) {
        max <= vw ? max : vw;
      }
    ')px;
  }
}

Sniffing Element Orientation

@element 'div' {
  $this {
    background: eval("
      offsetWidth == offsetHeight ? 'blue'
      : offsetWidth > offsetHeight ? 'green'
      : offsetWidth < offsetHeight ? 'red'
      : null
    ")
  }
}

Faking :in-viewport

@element 'p' {
  $this {
    /* Color is red when partly in-viewport */
    color: eval("
      var top = offsetTop - innerHeight;
      var bottom = top + offsetHeight;
      if (
        (scrollY < offsetTop + offsetHeight)
        && (top < scrollY)
        && (bottom < scrollY + offsetHeight)
      ){
        'tomato'
      } else {
        'inherit'
      }
    ");
    /* Background is green when fully in-viewport */
    background: eval("
      var top = offsetTop - innerHeight;
      var bottom = top + offsetHeight;
      if (
        (scrollY < offsetTop)
        && (top < scrollY)
        && (bottom < scrollY)
      ){
        'darkgreen'
      } else {
        'inherit'
      }
    ");
  }
}

Centered or Overflow

This example below uses if() statements for the selectors, not even a property or rule:

@element 'div h2' {
  eval('offsetHeight <= parentNode.offsetHeight ? "$this" : ""') {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
  }
  eval('offsetHeight >= parentNode.offsetHeight ? "$parent" : ""') {
    overflow: auto;
    overflow-y: scroll;
    border-color: lime;
  }
}

Using If statement as selector

In this example we use JavaScript to check the background-image of an element and only write a selector (applying the rule) for those that return true.

<div style=background-image:url(http://staticresource.com/user.png)></div>
<div style=background-image:url(http://staticresource.com/nebula.jpg)></div>
<style>
  div {
    height: 200px;
  }
  @element 'div' {
    eval("window.getComputedStyle($it).backgroundImage.indexOf('nebula') !== -1 ? '$this' : '' ") {
      border: 10px solid lime;
    }
  }
</style>
<script src=http://elementqueries.com/EQCSS.js></script>

Based on these experiments and more I think that having container-style queries so we can write conditions once and affect styles for many elements is better than writing conditions many times in many rules.

Another takeaway is that having an if() kind of functionality would be amazing in CSS, but being too liberal with it, it's crazy and could lead to some really bad code. Too conservative with it and you'll miss the parts that make it useful and powerful.

Should the if() work as a value, as a selector, as a responsive condition for elements?

@ausi
Copy link
Collaborator Author

ausi commented Dec 18, 2016

The problem I'm seeing with the first example is that they're based on viewport-percentage units, but the elements you'll need to be styling may not scale perfectly with the browser's width and/or height.

I used the viewport units as an example, the CSS author could set the CSS variable to whatever suits best. An element with a fixed width like 200px can set the variable to the same value --context-width: 200px;.

What we need to do these kinds of calculations is a unit that is aware of the width and height of the element as it appears on the page (not as we want it to appear).

IMO that’s exactly the thing that is hard/impossible for browsers to implement, because it has to jump between layout and style computation.

For setting aspect-ratios we may get a new property in the future, this is not a topic for container queries IMO.

Here's a version of your if() example that works in EQCSS syntax, and then some examples of hairier if statements I've written in EQCSS as well

Your eval-based if() examples are different to what I want to propose here. They are all using values that rely on the layout process of the browser like offsetWidth or offsetTop.

Based on these experiments and more I think that having container-style queries so we can write conditions once and affect styles for many elements is better than writing conditions many times in many rules.
Should the if() work as a value, as a selector, as a responsive condition for elements?

Using if() only as the value for a property is the simplest way to start with IMO. If browser makers agree that this is a solution that can be implemented, we could think of more powerful versions like:

.component {
    if(var(--context-width) > 200px) {
        float: left;
        background: red;
    }
    else {
        float: none;
        background: green;
    }
}

or:

.component:if(var(--context-width) > 200px) {
    float: left;
    background: red;
}
.component:if(var(--context-width) <= 200px) {
    float: none;
    background: green;
}

@ausi
Copy link
Collaborator Author

ausi commented Mar 6, 2017

I proposed the CSS conditions feature on WICG Discourse:
https://discourse.wicg.io/t/css-conditions-with-variables/2048

And wrote an article on how they might be an alternative for container queries:
https://au.si/css-conditions-cq-alternative

@tomhodgins
Copy link
Collaborator

Hey @ausi, great idea putting it on WICG Discourse to gather comment, it'll be great to see what people have to say!

In your article you refer to the circularity problem with element/container queries, and today I recorded a video about this very issue: https://www.youtube.com/watch?v=QfM_JwSDdjo

Even though this is not as convenient as the original proposals for container queries, it should be much easier to implement, doesn’t suffer from the circularity problem and is still powerful enough to solve the RICG use-cases.

I think there's a way to handle circularity that makes sense to how CSS works already!

@ausi
Copy link
Collaborator Author

ausi commented Mar 6, 2017

I think there's a way to handle circularity that makes sense to how CSS works already!

I don’t think that’s true for container queries. I watched your video, but I don’t think :hover has the same circularity issue. As you mentioned, :hover only gets triggered when the mouse is moving, but container queries would get triggered every time the page gets reflowed/repainted. Triggering the container queries could then cause another reflow and, in the worst case, would end in an infinite loop. You can take a look at #3 (comment) and https://discourse.wicg.io/t/a-pseudo-class-for-when-an-element-is-stickily-spec-term-positioned/947/12 for information about the problems with implementing container queries.

@beep
Copy link
Collaborator

beep commented Mar 7, 2017

@ausi: I just gave https://au.si/css-conditions-cq-alternative a quick read, and found it really interesting. I’m ~offline this week, but give a quick 🙌; I’ll try to have more substantive comments in next week.

@ausi
Copy link
Collaborator Author

ausi commented Mar 7, 2017

@beep: Awesome thanks! I’m looking forward to it :)

@FremyCompany
Copy link

I had some thoughts about this today, and was thinking about how this could allow to choose among a set of differing layouts easily without need to extend this further, and wrote this: https://jsfiddle.net/35758bg7/1/ (basically you can duplicate all your layout paths and then from there choose one based on settings a small subset of properties on it based on --width)

Posted this here just not to loose it, I don't say this is very smart or relevant, just didn't know where to post it otherwise :)

@gregwhitworth
Copy link

When it comes to container queries themselves, there will be usecases this can solve, and others it won't. This actually hits on a common thing that I tell folks when they state, "I need element/container queries" - if you can tell me at a pixel dimension when you want to change the design then you can probably still devise a way to utilize media queries, although it won't be nearly as legible nor clean as your solution - but you don't NEED container queries.

That said, I do know of other desires to have conditionals that inspect variable values and apply different styles accordingly.

My only worry with this approach, is this will add yet an additional step in the cascade and this isn't fully defined as you can see in this issue I filed here and for us (Edge) this is a pain: w3c/csswg-drafts#411 (comment) and still should do a PR at this point (I haven't found the time).

We saw numerous interop issues as this can change the calculation of the variable that you're checking the condition on. And based on my worries for at-apply (which has been abandoned) you may end up in the same situation in this case where if we make it so that the condition is checked at, let's say, in between step 1 and 2 you'll result in some need to stop circularity within the cascade as well (just like we have with layout and style) which could be that you can't change a custom prop within animations that are used within a condition (you would need to do this anyway where you can't change the variable within the condition similar to what we do with animation tainting).

At any rate, I think it's something we should at least discuss as it would help out scenarios such as Roma's conditions using calc()

@ausi
Copy link
Collaborator Author

ausi commented Feb 12, 2018

@gregwhitworth Thank you for taking a look at this!

I agree that this feature would not solve all use cases for container queries, but I think it would solve many of them. I will reiterate on that once the use cases in wicg/cq-usecases are defined, as there is some work going on there currently.

Regarding circularity, I think this can possibly be solved by restricting the conditions to parent-var(). I wrote something about this in the WICG Discourse: https://discourse.wicg.io/t/css-conditions-with-variables/2048/15
Do you think this restriction could save the extra step in the cascade too, because it could be resolved directly in step 1 (apply cascaded values)?

@ausi
Copy link
Collaborator Author

ausi commented Jan 20, 2019

I just wrote another article regarding CSS conditions, including a nice hack which lets us use some form of CSS conditions today: au.si/css-conditions-today

I think this demonstrates that the groundwork for CSS conditions is already there and the general concept of such a feature is possible to implement in browsers.

I still believe that this can be the answer to most use cases of container queries and at the same time doesn’t create headaches for browser makers.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants