Skip to content

Instantly share code, notes, and snippets.

@carlosame
Created March 17, 2021 16:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carlosame/d71832c68a4b446b397738f5cdb222b5 to your computer and use it in GitHub Desktop.
Save carlosame/d71832c68a4b446b397738f5cdb222b5 to your computer and use it in GitHub Desktop.
Compact CSS media rules: the 'assign' mechanism

Compact CSS media rules: the @assign mechanism

Justification

As mentioned in my gist about a CSS switch-like function, authors would like to have a mechanism for less verbose CSS @media rules, as expressed in CSSWG's issue 5009.

In that issue a possible new syntax (with a few variants) is suggested, with authors specifying stacked property values that are chosen when they match a media feature value from a given set.

Although no detailed specification proposals were initially given (it all started from a tweet), most of the suggested syntaxes involved widths or lengths (range features), and in those cases one either assumes an ordered set of feature values (with the opposite behavior being required for e.g. max-width and min-width, which is problematic) or uses some kind of comparison operators (like other proposals posted later in that thread did).

This proposal presents a mechanism of a similar kind, but more general and more formally specified. It is not based on the usage of comparison operators, although the media features themselves may contain them.

Although the level of detail of some parts of the specification may seem a bit of overkill, that is necessary to offer a consistent solution with general validity.


The @assign rule

The basic idea is to assign a value to a custom property, based on the medium matching one or more of a list of media queries. If more than one media query (MQ) is matched, then a new concept of media query specificity (defined later) is introduced so the value corresponding to the most specific, matching MQ is used. If the medium matches more than one of those 'most-specific' MQs, then the last one is used (as typical in CSS).

The syntax has semicolon separators, like in one of the variants for my switch-like proposal:

@assign <custom-property-name> (<declaration-value> [; <declaration-value>]{0,});

so comma-separated values can be assigned.

If the medium matches a most-specific MQ located at position N in the MQ list, then the <declaration-value> at position N is used to set the custom property. If N is greater than the number of values in the assign, the last value is used.

Example:

@assign --my-font-size (14pt; 20pt; 25pt);
@assign --my-padding (10px; 20px; 50px);

Depending on where one puts those (potentially small) rules, regular properties could be assigned as well, although that would be a huge change in how regular properties are set.

So where do we put these rules? this proposal includes two initial variants for it.


Variant I: recycling @media rules

The most obvious path would be to use plain @media rules, for example:

@media (width >= 1200px), (700px <= width < 1200px), (width < 700px) {
  :root {
    @assign --my-font-size (25pt; 20pt; 14pt);
    @assign --my-padding (50px; 20px; 10px);
  }
}

or

@media (max-width: 700px), (min-width: 700px), (min-width: 1200px) {
  :root {
    @assign --my-font-size (14pt; 20pt; 25pt);
    @assign --my-padding (10px; 20px; 50px);
  }
}

and seeing that last example, one may think: "but a display may match more than one of those media queries". And here is where the concept of specificity is used. A device with a 1500px viewport width matches the last two, but the last is more specific. And a 700px-wide viewport matches the first two (which have the same specificity) so the second one wins.

Using @media rules allows to reuse an existing syntax but has a potential pitfall: if somebody decides to edit the media query list without being aware of the embedded @assign rules, they may break the style sheet. The second variant is less powerful but avoids that problem.


Variant II: the @select rule

Instead of using media rules, this variant introduces a new rule that has only one job: to enable @assign rules.

The syntax:

@select <media-query-list> {
  <assign-list>
}

where <assign-list> is obviously a list of @assign rules. Notice that there are no selectors in that rule, because all properties are assigned to :root; otherwise it would be confusingly similar to @media.


The specificity of media queries

So far, different syntaxes have been defined but an important part is missing: how to establish the specificity of media queries. And the first thing that we need to consider is that in a media query list like those shown above, one shall typically find MQs that apply different tests over the same media feature (width, for example). But they could also contain arbitrarily different features (unlikely, but needs to be specified).

First, let's define the specificity of MQs that test the same media feature, and then we shall define it for combinations of different features. In our context, the specificity is based on the concept that a MQ may be contained by another MQ, under the following definition:

If query A contains query B, then if a medium matches B it will also match A.

Naturally, the opposite may not be true (unless A is equal to B).

More definitions:

  1. For range features, if query A contains query B, then B is more specific than A.
  2. For discrete features, if query A contains query B, then A is more specific than B.
  3. If A does not contain B, nor B contains A (for example (max-width: 700px) and (min-width: 700px)), they have the same specificity.

Some clarifications:

  • If A contains B but B happens to contain A as well, then they have the same specificity (because A is equal to B).
  • If a MQ matches a feature through one or more of the items in a OR grouping, then only the most specific matching item in the OR is considered.

Examples:

  • (height > 500px) contains (720px < height <= 1080px). The latter is more specific.
  • (color-gamut: rec2020) contains (color-gamut: srgb). The former is more specific.

Typically, values of discrete features have all the same specificity (with the exception of color-gamut) because one value generally does not contain the others.


Mixed media features

In most cases the media query lists are only going to test a single feature, but the case of several different features being checked through a AND grouping needs to be specified. For example, (height > 500px) and (monochrome) is more specific than (height > 600px) but let's see how:

a) Some features have higher precedence than others, and a medium matching a feature with a higher precedence will always be more specific. That is, if a media query A matches a feature with higher precedence than (also matching) query B, A will always be more specific than B, even if B matches multiple other lower-precedence features through a AND grouping.

b) When a media query matches two or more features (via AND), it is more specific than a query that matches less features with the same precedence and specificity.

c) When query A matches two or more features of the same precedence (via AND) and one or more of those features is more specific in B, then B is more specific, regardless of the number of features being tested in A.

The following media features are ordered by precedence, higher first:

  1. forced-colors if active, and prefers-contrast when not evaluating to no-preference. Also grid, when evaluating to true.
  2. monochrome, when evaluating to true.
  3. width, height and aspect-ratio.
  4. inverted-colors.
  5. dynamic-range, color-gamut and color.
  6. resolution.
  7. Other features: they share the same lowest precedence.

Examples:

  • (forced-colors: active) is more specific than (monochrome), due to a).
  • (800px < width < 1200px) and (monochrome) is more specific than (width > 500px) and (monochrome) by virtue of c).
  • (width > 500px) and (aspect-ratio > 4/3) and (monochrome) is more specific than (width > 500px) and (monochrome) because of b).
  • (width > 500px) and (aspect-ratio > 4/3) and (monochrome) is less specific than (500px < width < 1000px) and (monochrome) because of c).

However, and as mentioned before, this feature precedence mechanism is unlikely to be extensively used, as authors are probably going to avoid the mix of different media features.


Implementation complexity

The implementation of media query specificity, albeit being based on concepts that are relatively simple, shall require a fair amount of coding and testing. To estimate that effort, it may be of help to look into an implementation that has done part of the work. My css4j library implements (in the Java language) the MediaQueryList.matches(MediaQueryList) method, which basically tests the 'contains' relationship defined above.

A non-exhaustive count of the lines of code involved with the functionality, across some of the relevant source files, yields a number around 1000 (not including tests, which add several times that figure). Not tremendous but not trivial either, with C++ implementations likely to be more verbose. And that's only part of the code that would have to be written, as then one has to implement the specificity on top of the 'contains', add support for the new syntax constructs, the value assignment...

The mechanism described in this document does the job for which it was devised, but the implementation costs would be relatively expensive.


Security considerations

This proposal would just allow a more compact form of specifying media-dependent properties, not introducing anything that could not be done previously. Therefore, it has no specific security implications.


Conclusions

Specifying this functionality requires a bit of complexity, but most authors should be able to use it nearly instinctively, without having to learn all the details. However, my opinion is that my other proposal about the switch-like function is simpler to implement and easy to understand by authors, looking like the simplest path to address their needs.

On the other hand, the proposal described in this document seems especially well suited for frequent use cases and, if that kind of approach is ever to be followed, provides a consistent framework that could be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment