CSS When/Else Rules

A Collection of Interesting Ideas,

This version:
https://tabatkins.github.io/specs/css-when-else/
Issue Tracking:
GitHub
Inline In Spec
Editor:
Tab Atkins-Bittner

Abstract

A proposal to extend the concept of CSS conditional rules to arbitrary when/else chains, and supporing this, a proposal to unify the disparate conditional rules into a single grammar.

1. Introduction

Writing complex media queries or supports queries is difficult, particularly when you want to do Thing A in one circumstance, Thing B in another circumstance, and Thing C otherwise. This requires carefully crafting your conditional rules to exclude anything matching the other rules, while also making sure you’re not accidentally over-excluding things and leaving some situations unmatched.

This spec proposes two things to fix this problem. First, it proposes an @when rule, which generalizes the concept of a conditional rule. Anything you can express in an existing conditional rule can be expressed in @when, it just has to be wrapped in an appropriate function to declare what kind of condition it is. This allow authors to easily combine multiple types of queries, such as media queries and supports queries, in a single boolean expression. Without this, authors must rely on nesting separate conditional rules, which is harder to read and write, presupposes the conditions are to be conjoined with the "and" boolean relation (with no easy way to indicate anything else), and restricts their utility in the proposed conditional rule chains.

Second, it proposes the introduction of @else rules, which follow conditional rules and automatically qualify their conditions as you’d expect, such that at most one rule in an conditional rule chain is chosen as active.

2. Generalized Conditional Rules: the @when rule

The @when at-rule is a conditional group rule that generalizes the individual conditional group rules such as @media and @supports. It is defined as:

@when <boolean-condition> {
  <stylesheet>
}

Where <boolean-condition> is a boolean algebra a la Media Queries 4 §3 Syntax, but with media() and supports() functions as leaves.

Define "boolean algebra, with X as leaves" in a generic way in Conditional, so all the conditional rules can reference it directly, rather than having to redefine boolean algebra on their own.

The media() and supports() functions are defined as:

media() = media( [ <mf-plain> | <mf-boolean> | <mf-range> ] )
supports() = supports( <declaration> )

A media() or supports() function is associated the boolean result that its contained condition is associated with.

3. Chained Conditionals: the @else rule

Usually, conditional group rules are independent; each one has a separate condition, evaluated without direct reference to any other rule, and decides whether or not to apply their contained rules based solely on their condition.

This is fine for simple conditions, but makes it difficult to write a collection of conditionals that are meant to be mutually exclusive; authors have to very carefully craft their conditions to not activate when the other rules are meant to, and make sure the collection of conditionals don’t accidentally all exclude some situation.

The @else rule is a conditional group rule used to form conditional rule chains, which allow multiple conditional rules to be provided and guarantee that at most one of them will evaluate their condition as true. It is defined as:

@else <boolean-condition>? {
  <stylesheet>
}

@else is interpreted identically to @when. If its <boolean-condition> is omitted, it’s treated as having a condition that’s always true.

A conditional rule chain is a series of consecutive conditional group rules, starting with a conditional group rule other than @else, followed by zero or more @else rules. There cannot be anything between the successive conditional group rules other than whitespace and/or comments; any other token "breaks" the chain.

Should we require that only the last @else in a chain can have an omitted condition? It’s not uncommon for me, when debugging code, to short-circuit an if-else chain by setting one of them to "true"; I presume that would be similarly useful in CSS? It’s still pretty easy to see you’ve done something wrong if you omit the condition accidentally.

Within a conditional rule chain, the conditions of each conditional group rule are evaluated in order. If one of them is true, the conditions of all following conditional group rules in the chain must evaluate to false, regardless of what they contain.

An @else rule that is not part of a conditional rule chain is invalid and must be ignored.

For example, here’s a (somewhat silly) conditional chain:
@when media(width >= 400px) and media(pointer: fine) and supports(display: flex) {
  /* A */
} @else supports(caret-color: pink) or supports(background: double-rainbow()) {
  /* B */
} @else {
  /* C */
}

Exactly one of the preceding rules will be chosen, even tho the second rule doesn’t exclude large widths, fine points, or flexbox support, and the last rule doesn’t specify anything at all.

To achieve the same result without conditional rule chains, you’d need to write:

@media (width >= 400px) and (pointer: fine) {
  @supports (display: flex) {
    /* A */
  }
  @supports not (display: flex) {
    @supports (caret-color: pink) and (background: double-rainbow()) {
      /* B */
    }
    @supports not ((caret-color: pink) and (background: double-rainbow())) {
      /* C */
    }
  }
}
@media not ((width >= 400px) and (pointer: fine)) {
  @supports (caret-color: pink) and (background: double-rainbow()) {
    /* B */
  }
  @supports not ((caret-color: pink) and (background: double-rainbow())) {
    /* C */
  }
}

This is simultaneously impossible to read, requires significant duplication of both conditions and contents, and is very difficult to write correctly (I wrote it wrong twice while producing this example). If the conditions got any more complicated (which is not unusual in real-world content), the example would get significantly worse.

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-CASCADE-4]
Elika Etemad; Tab Atkins Jr.. CSS Cascading and Inheritance Level 4. 14 January 2016. CR. URL: http://dev.w3.org/csswg/css-cascade/
[CSS-CONDITIONAL-3]
CSS Conditional Rules Module Level 3 URL: https://drafts.csswg.org/css-conditional-3/
[CSS-SYNTAX-3]
Tab Atkins Jr.; Simon Sapin. CSS Syntax Module Level 3. 20 February 2014. CR. URL: http://dev.w3.org/csswg/css-syntax/
[CSS-VALUES]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 3. 11 June 2015. CR. URL: http://dev.w3.org/csswg/css-values/
[MEDIAQUERIES-4]
Florian Rivoal; Tab Atkins Jr.. Media Queries Level 4. 26 January 2016. WD. URL: http://dev.w3.org/csswg/mediaqueries4/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119

Issues Index

Define "boolean algebra, with X as leaves" in a generic way in Conditional, so all the conditional rules can reference it directly, rather than having to redefine boolean algebra on their own.
Should we require that only the last @else in a chain can have an omitted condition? It’s not uncommon for me, when debugging code, to short-circuit an if-else chain by setting one of them to "true"; I presume that would be similarly useful in CSS? It’s still pretty easy to see you’ve done something wrong if you omit the condition accidentally.