styletron icon indicating copy to clipboard operation
styletron copied to clipboard

Add support for css feature queries

Open stevenbenisek opened this issue 9 years ago • 10 comments

Feature Queries seem doable without adding to much extra code. @rtsao I could create a PR if this is something you think Styletron could benefit from.

stevenbenisek avatar Dec 06 '16 22:12 stevenbenisek

I've actually never really used feature queries, but this seems pretty interesting.

I wonder if this could somehow help solve https://github.com/rtsao/styletron/issues/6 where there's reusable atomic "fallbacks" based on feature queries.

Are there any other CSS-in-JS libraries that support feature queries? I'd be interested in seeing how they are used and what the syntax is like.

rtsao avatar Dec 07 '16 00:12 rtsao

Both cxs and Free-Style support feature queries. The syntax is equal to that of media queries. In the case of Styletron it would look like this:

import {injectStyle} from 'styletron-utils';

injectStyle(styletron, {
  '@supports (flex-wrap: wrap)': {
      display: 'flex',
      flexWrap: 'wrap'
  }
});

This is an excellent way to progressively enhance the experience. e.g: Android < 4.4 has support for display: flex but not for flex-wrap: wrap. Just applying both properties might break the layout of the component. Using feature queries to test if flex-wrap is supported mitigates this problem.

Feature queries could be used to provide value fallbacks as per #6. Free-Style has an interesting way of soving this: https://github.com/blakeembrey/free-style#overload-css-properties

stevenbenisek avatar Dec 07 '16 19:12 stevenbenisek

Would also love to have this to be able to conditionally style padding around the iPhone X notch. E.g.

@supports(padding: max(0px)) {
    .post {
        padding-left: max(12px, constant(safe-area-inset-left));
        padding-right: max(12px, constant(safe-area-inset-right));
    }
}

from https://css-tricks.com/designing-websites-iphone-x/

leyanlo avatar Feb 01 '19 06:02 leyanlo

I think supporting @supports this makes sense. That said, I think there are a few questions that need to be resolved, mostly around specificity/precedence.

For example, what should the text color be in the case of the style below?

const style = {
  "@supports (color: yellow)": {
    color: "yellow"
  },
  "@supports (color: aqua)": {
    color: "aqua"
  },
  color: "red",
}

Unlike media queries, there doesn't really seem to be an obvious way to sort these.

Leaving this to be non-deterministic defeats the intended purpose of @supports, so we have a few potential options for ordering, none of which sound particularly appealing:

  1. Object key iteration order (yielding red in the above example, given that latter declarations take precedence)
  2. First plain declarations, then @supports sorted by object key iteration order (yielding aqua in the above example)
  3. First plain declarations, then @supports sorted by Array.prototype.sort() (yielding yellow in the above example)

Thoughts?

I'm leaning towards #2, but I'm left wondering if there's a syntax/API that has a more obvious order as relying on object iteration order isn't a common pattern and might not be intuitive.

rtsao avatar Apr 10 '19 01:04 rtsao

Thanks for looking into this! My vote would be for 2 as well, since that is how one would write the CSS, at least for properties that one expects to get overridden given support.

E.g. when CSS grid was being introduced, the recommended pattern with @supports would have been something like

.maybeGrid {
  display: flex;
}
@supports (display: grid) {
  .maybeGrid {
    display: grid;
  }
}

And swapping the order of @supports would have been a legit bug. Iteration order after that seems ok to me since that’s how CSS overrides work in general.

Maybe we could show a warning if we have the same property (color in your example above) in two @supports blocks, similar to how styletron shows warnings for mixing shorthand/longhand properties?

leyanlo avatar Apr 10 '19 13:04 leyanlo

Keeping the discussion about atomic @supports here.

First plain declarations, then @supports sorted by object key iteration order (yielding aqua in the above example)

I don't think this would really help. Imagine having two components:

// component A styles
const style = {
  "@supports (color: yellow)": {
    color: "yellow"
  },
  "@supports (color: aqua)": {
    color: "aqua"
  },
  color: "red",
}

and

// component B styles
const style = {
  "@supports (color: aqua)": {
    color: "aqua"
  },
  "@supports (color: yellow)": {
    color: "yellow"
  },
  color: "red",
}

And now you have a race condition. If Styletron first runs into the component A, the component B will not have a chance to do the correct sorting according to the second rule.

We were able to fix this for media queries because there is a deterministic mobile-first sorting that can be used universally.

tajo avatar Jan 23 '20 02:01 tajo

Yeah, the object iteration example was mainly to shed light on how the semantics aren't all that clear (in isolation of how it is actually rendered).

As you point out, true atomic rendering will be problematic, but I think we could workaround this by avoiding globally shared atomic classes for @supports styles. I have some thoughts on how we could do this, but in the worst case, we could use a monolithic-style rendering approach purely for @supports styles (possibly a barebones implementation). Since the bulk of styles won't use @supports, I think that would be tolerable.

Another option for the atomic engine would be to simply have warnings in development for objects that have multiple @supports styles that reference the same properties. I think this would be relatively rare case, so this might be OK.

At the end of the day, I think we should be able to make things work regardless of the rendering engine, I'm mostly concerned with the semantics of this, which seem potentially confusing regardless of what we choose. I think option 2 is reasonably intuitive.

rtsao avatar Jan 23 '20 04:01 rtsao

I brought this idea in https://github.com/styletron/styletron/issues/334#issuecomment-562692804, but repeated selectors might be an elegant way of handling @supports.

The basic idea: when iterating through a style object, increment the specificity each time a @supports is encountered (starting with 1).

So suppose we have:

function Component() {
  const aqua = {
    "@supports (color: yellow)": { color: "yellow" },
    "@supports (color: aqua)": { color: "aqua" },
    color: "red"
  };

  const yellow = {
    "@supports (color: aqua)": { color: "aqua" },
    "@supports (color: yellow)": { color: "yellow" },
    color: "red"
  };

  const [css] = useStyletron();

  return (
    <>
      <div className={css(aqua)}>aqua</div>
      <div className={css(yellow)}>yellow</div>
    </>
  );
}

The rendered result would be:

<style media="">
@supports (color: yellow) { .a.a { color: yellow } }
@supports (color: aqua) { .b.b.b { color: aqua } }
.c {color: red}
@supports (color: aqua) { .d.d { color: aqua } }
@supports (color: yellow) { .e.e.e { color: yellow } }
</style>
<div class="a b c">aqua</div>
<div class="d e c">yellow</div>

So in this case, the precedence of the @supports styles is always correct for a given object (based on insertion order). Of course, re-use of atomic @supports styles would only happen if the iteration index of the @supports was the same as something already rendered.

The drawback is the rendering and hydration code would need some additional logic to take into account this concept of repeated selectors. But I think it could be lightweight.

Even more optimally, we could have two stylesheets for every media: one for regular styles and a latter one for @supports styles. This has two advantages: the first @supports would not need a repeated selector and the more complex hydration logic would only be needed for the @supports stylesheet, not the regular one.

In the prior example, the rendered example would be like:

<style media="">
.c {color: red}
</style>
<style media="" data-hydrate="supports">
@supports (color: yellow) { .a { color: yellow } }
@supports (color: aqua) { .b.b { color: aqua } }
@supports (color: aqua) { .d { color: aqua } }
@supports (color: yellow) { .e.e { color: yellow } }
</style>
<div class="a b c">aqua</div>
<div class="d e c">yellow</div>

rtsao avatar Feb 04 '20 00:02 rtsao

const aqua = {
  "@supports (color: yellow)": { color: "yellow" },
  "@supports (color: aqua)": { color: "aqua" },
  color: "red"
};

const yellow = {
  "@supports (color: aqua)": { color: "aqua" },
  "@supports (color: yellow)": { color: "yellow" },
  color: "red"
};

Just to be clear: If we used this order in plain CSS, the resulting color would be red for both components since supports doesn't increase specificity (interestingly, emotion always reorders these rules so support rules are defined last and "win").

Anyway, the repeated selector would work.

tajo avatar Feb 04 '20 21:02 tajo

If we used this order in plain CSS, the resulting color would be red for both components

Yeah, this is option 1 in https://github.com/styletron/styletron/issues/21#issuecomment-481498288, which I'm still open to. But I think for an object-based API, it "feels" like @supports styles should take precedence just like media queries do. It's probably a good sign that emotion does this so I think we're on the right track.

Part of me wonders if we should actually just preserve insertion order of media queries as opposed to actually sorting them... But I suppose sorting them also equally sensible and probably more helpful than not.

rtsao avatar Feb 04 '20 21:02 rtsao