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

[selectors][mediaqueries] :media() pseudo-class as a shortcut for one-off media queries #6247

Open
LeaVerou opened this issue Apr 27, 2021 · 16 comments

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Apr 27, 2021

Authors often need to duplicate rules across media queries and selectors. For example, this kind of thing is common:

@media (prefers-color-scheme: dark) {
	:root {
		/* dark mode rules */
	}
}

:root.dark {
	/* duplicated dark mode rules */
}

To my knowledge, there is no way to reduce this duplication with CSS, even if we take CSS nesting into account. Authors either write flimsy JS to toggle the class per the media query, use preprocessor mixins, or just live with the duplication.

Also, often media queries only contain a single rule, and could benefit from a more concise syntax.

With this proposal, the code above would be written as:

:root:is(.dark, :media(prefers-color-scheme: dark)) {
	/* dark mode rules */
}

Any :media() pseudos within a selector would desugar as media queries, joined with and and prepended with not accordingly (e.g. :media(foo):not(:media(bar)) should desugar to @media (foo) not (bar) {...}).

@LeaVerou LeaVerou changed the title [selectors][mediaqueries] :media() pseudo-class as a shortcut for media queries [selectors][mediaqueries] :media() pseudo-class as a shortcut for one-off media queries Apr 27, 2021
@tabatkins
Copy link
Member

I definitely agree with this functionality. Having some tests appear in an MQ vs others appearing in a selector means we have to do some awkward nesting and reduplication. This is also the motivation behind a generic conditional rule that combines @media and @supports (the @when proposal that I need to revive).

With that background, perhaps we should make the pseudo-class name more generic, so that it can handle support queries as well?

@LeaVerou
Copy link
Member Author

With that background, perhaps we should make the pseudo-class name more generic, so that it can handle support queries as well?

Agreed, I was wondering about it when I opened the issue, but decided against it as I thought it would complicate the syntax too much. But if you also think so, let's do that.

@argyleink
Copy link
Contributor

I ran into this last week and after asking Tab about it, have found myself here!

Here's a reduced case https://codepen.io/argyleink/pen/OJpmEWE where I would hope to eliminate the duplication between light and dark

@bkimmel
Copy link

bkimmel commented May 25, 2021

I love this idea. I kinda wish media queries just worked this way in the first place.

@mirisuzanne
Copy link
Contributor

mirisuzanne commented May 26, 2021

We discussed this as an approach to container queries, and it's probably good to consider them as part of this conversation. If the only syntax is selector-based, it makes larger changes (impacting multiple selectors) more repetitive – though nesting could help that problem:

.card:container(width > 30em) {
  & .card-image { … }
  & .card-content { … }
  & .card-footer { … }
}

With container queries specifically, this also introduces a different way to solve the "named container" problem – but it can be kinda confusing as well. Right now we always have the selector-target query its nearest container:

/* body and card are both containers */
body, .card { contain: inline-size style layout; }

@container (width > 30em) {
  .card { /* card queries the size of body */ }
  .card .content { /* content queries the size of card */ }
}

.card:container(width > 30em) .content {
  /* card queries the size of body, in order to update content */
}

I don't see it as a universally better solution than the at-rule syntax, but I do think it can provide some interesting trade-offs. Interested to see where this conversation goes.

@andruud
Copy link
Member

andruud commented May 26, 2021

Adding :container probably exceeds acceptable complexity limits. It makes invalidation much more annoying, since we would need to figure out if a given selector would have matched without :container pseudos, and in order do that correctly we would probably need to evaluate any non-matching selector with :container twice, which I'd rather not do.

Also, you know Emilio won't accept that it makes Element.matches depend on layout.

Also also, it's not compatible with other CSS things, e.g.:

body {
  width: 40em;
  contain: inline-size style layout;
}

body:has(.card:container(width > 30em)) {
  width: 20em;
}

@daKmoR

This comment was marked as duplicate.

@jakearchibald
Copy link
Contributor

Rather than make media queries & container queries work in selectors, would it be better to make selectors work in @ rules when nested?

I haven't thought hard about the parsing here, but something like:

:root {
  @media (prefers-color-scheme: dark),
  &.dark {
    /* dark mode rules */
  }
}

@romainmenke
Copy link
Member

romainmenke commented Sep 22, 2022

Might be more interesting to leverage @scope as mentioned in the examples.
https://drafts.csswg.org/css-cascade-6/#scoped-styles

@scope (.light-scheme) {
  a { color: darkmagenta; }
}

@scope (.dark-scheme) {
  a { color: plum; }
}

With @when it could become :

@when media(prefers-color-scheme: dark) or scope(.dark) {
  /* styles */
}

I do think it will have some weird side-effects to mix conditional at-rules and selectors:

@when media(prefers-color-scheme: dark) {
  @layer foo {
    /* this is fine */
  }
}
.dark {
  @layer foo {
    /* this is not fine */
  }
}
@when media(prefers-color-scheme: dark) or scope(.dark) {
  @layer foo {
    /* ?? */
  }
}

Same is true for other syntax proposals.

:root {
  @media (prefers-color-scheme: dark),
  &.dark {
    @layer foo {
      /* ?? */
    }
  }
}

@mirisuzanne
Copy link
Contributor

Could also consider a selector() function in the @when syntax? I'm not sure if that's been discussed.

I realized the other day that style queries help work around this issue, but with an unfortunate parent/child limitation. We can do something like:

@media (prefers-color-scheme: dark) {
  html { --mode: dark; }
}

.dark-mode { --mode: dark; }

/* the result of this query is based on the combined media-query/selector resolutions */
@container style(--mode: dark) {
  /* has to be a descendant of the element that sets the value 👎🏼 */
  body {
    background: black;
    color: white;
    /* the full list of colors only have to be defined in one place 👍🏼 */
  }
}

But it feels like a workaround, rather than a full solution.

@tabatkins
Copy link
Member

Putting selectors into MQs/etc has the wrong semantics; the selector isn't a query with a truth result, it's actually selecting elements, a completely independent action from the conditional itself. :media() has the correct semantics.

@jakearchibald
Copy link
Contributor

:media() has the correct semantics.

Well, not really. Pseudo-classes are query something about the element, whereas :media() is querying the viewport. It's a bit of a hack that it's classed to the element.

Fwiw, that's why I suggested:

:root {
  @media (prefers-color-scheme: dark),
  &.dark {
    /* dark mode rules */
  }
}

It's saying "this block of styles applies when this media query matches, or where this selector matches". It isn't putting MQs into element classes, nor is it putting selectors into MQs.

@tabatkins
Copy link
Member

Pseudo-classes are query something about the element,

My point is that the semantics of a pseudo-class (any selector, really) is "filter the currently-matched set of elements according to condition X". The fact that :media()'s condition isn't element-specific doesn't change this, it just makes it feel a little funny. (We could always define that the pseudo-class only matches on the root element, to make it feel more "global", but I think that would make things unnecessarily annoying in practice.)

Conditional rules, on the other hand, either match or not, and activate or deactivate the rules inside of them. They haven't previously had any effect on the set of matched elements. Obviously we could add this, changing their pattern of behavior, but it would be a fairly significant change. We could no longer imagine conditional rules as "turning off" rules, but rather as a type of selector in itself that filters the set of matched elements.

(Also a nit: putting selector syntax nakedly into MQ syntax would be a no-go; the grammar is too wide and would force us to be very careful evolving both selectors and MQs in the future. But a selector() function would work.)

@jonathantneal
Copy link
Contributor

Pseudo-classes are query something about the element, whereas :media() is querying the viewport.

I think context is something about the element. A similar API would be :has().

Here is a hypothetical example of context by target class or media query:

:root:is(.dark, :when(media(prefers-color-scheme: dark))) {
	/* dark mode rules */
}

Here is a hypothetical example of context by target class or style query:

:root:is(.dark, :when(style(--mode: dark))) {
	/* dark mode rules */
}

@NickGard
Copy link

Getting back to the problem of code duplication, could we do something similar to SASS's mixin?

Instead of setting a Custom Property flag and using container queries to style, you could add a block of code to a ruleset.

/* With flag */
.dark { --mode: dark; }
@media(prefers-color-scheme: dark) { --mode: dark; }

@container(style(--mode: dark)) {
  /* rules */
}

/* With mixin */
@mixin dark-mode {
  /* rules and nested rules that will map to the selector that includes the mixin */
}

.dark { @includes dark-mode }
@media(prefers-color-scheme: dark) {
  @includes dark-mode 
}

It doesn't have to be as full-featured as SASS's mixin, just the code reuse and dynamic scoping would be enough.

@brandonmcconnell
Copy link

Would this pseudo-class counterpart to the @media at-rule work directly on selectors or only inside :is() and :not()? I'd guess both, but I only see examples within other pseudo-class rules on this thread.

Assuming this applies to all applicable at-rules, this might also solve the issue/proposal I opened today in #10356.

This way, @starting-style rules can be applied inline with other selector-based rules to avoid redundancy. One of the spec's authors, @dbaron, also spoke into the discussion and voiced a similar sentiment.

I agree that we ended up with a solution that requires a bunch of repetition that I'm not happy about.

Having a way to use these at-rules inline as pseudos would be amazing.

I initially preferred the syntax proposed by @jakearchibald (above) and by @Jothsa in #8840, but that would also impose some limitations. In the case of @starting-style, this would still not allow the at-rule to mix with selector logic in the ways necessary to remove redundancy.

However, with a pseudo-class rule counterpart available, much of the redundancy in this example:

dialog {
  transform: translateY(-50%);
  &, &::backdrop {
    transition: all 0.25s ease-out allow-discrete;
    opacity: 0;
  }
  &[open] {
    transform: translateY(0);
    &, &::backdrop {
      opacity: 1;
    }
  }
  @starting-style {
    &[open] {
      transform: translateY(-50%);
      &, &::backdrop {
        opacity: 0;
      }
    }
  }
}

…can be consolidated, leaving us with this:

dialog {
  &, &[open]:starting-style {
    transform: translateY(-50%);
    &, &::backdrop {
      transition: all 0.25s ease-out allow-discrete;
      opacity: 0;
    }
  }
  &[open] {
    transform: translateY(0);
    &, &::backdrop {
      opacity: 1;
    }
  }
}

I think @jonathantneal's suggestion of introducing a :when pseudo for this could also work well, as long as starting-style would be valid within @when() rules :when() wouldn't pose a significant risk of confusion (between :where() and :when()).

If we did want a way to match the query syntax exactly, we could consider :matches(@media(…)), but I think that might be a step backward with the more flexible @when on the horizon.


It might help to organize a comprehensive list of all at-rules that could benefit from a pseudo-class counterpart like this.

  • @when
  • @media
  • @supports
  • @starting-style
  • any others?

Or should we introduce only one new pseudo-class, :when() which can be used to test for any of these and others?

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