CSS Cascade Control

A Collection of Interesting Ideas,

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

Abstract

A proposal to improve the handling of list-value and set-valued properties when one wants to alter them in multiple independent ways, without overriding each other or having to explicitly handle all combinations.

1. Introduction

A number of CSS properties are list-valued (background, transition, etc) or set-valued (will-change, etc). These can be difficult to manage in an application written by several authors (or partially styled by indepedent libraries), or even when there’s only a single author that just wants to style an element in multiple independent ways.

For example, a particular class might do some transform work, and thus want to set will-change: transform, while another class does some opacity changes, and thus wants to set will-change: opacity. Done naively, if these two classes get set at the same time, one will win over the other, and will-change will only reflect one of the values.

Currently, the only way to get around this is to explicitly handle the collision, manually applying will-change: transform, opacity when the element matches both classes. This sort of explicit collision-handling is unmaintainable, and can quickly grow out of control if more cases are added, as each one increases the number of combinations to be handled combinatorially. (A third class means you need to explicitly handle the 1+2, 1+3, 2+3, and 1+2+3 cases. A fourth class means handling 11 different combinations!)

In this spec we propose a few possible mechanisms to handle these situations more elegantly.

2. Cascade Declarations

During the cascade process, multiple declarations of the same property on a given element are sorted in specificity order to produce the output of the cascade, and then only the last value (the one with highest specificity) is actually used as the value of that property on the element.

A cascade modifier is a modifier on a property name that makes the property’s value depend partially on the previous cascaded value: the value for that property that comes before the current value (that is, is lower specificity) in the output of the cascade.

For any set-valued property with a name set-prop, the following cascade modifiers exist:

set-prop+

Represents the union of its value with the previous cascaded value.

set-prop-

Represents the difference of the previous cascaded value with its value. (That is, the previous cascaded value, minus the current values.)

set-prop{}

Represents the intersection its value with the previous cascaded value.

This involves defining the "unit" of each set-valued property (for example, in will-change each keyword is a "unit"), and ensuring that all of them have a "null" value (like none).

For any list-valued property with a name list-prop, the following cascade modifiers exist:

list-prop+

Represents the current value appended to the end of the previous cascaded value.

+list-prop

Represents the current value prepended to the start of the previous cascade value.

The "unit" of list-valued properties are much easier to define; for all but some legacy properties like counter-reset, it’s just splitting on commas.

Define that, for all of these, we interpret the property as normal first, then split it into units for merging; this isn’t variable-style concatenation.

That is, the following does not define a 2px-offset blue shadow:

.foo {
  box-shadow+: 2px 2px;
}
.foo {
  box-shadow+: blue;
}

As each property is still interpreted as a property, the first is interpreted the same as box-shadow: 2px 2px; (specifying a single drop-shadow set to currentcolor), while the second is simply invalid.

2.1. Managing Order Explicitly with Variables

Using cascade modifiers directly can achieve a number of useful, simple effects, but direct usage doesn’t allow a number of common use-cases. For example, you can’t override a particular cascade modifier with a more specific rule.

For example, take the following:
div {
  background-image: url(base.jpg);
}
div.foo {
  +background-image: url(one.jpg);
}
div.foo.bar {
  +background-image: url(two.jpg);
}

This does *not* override the one.jpg with two.jpg; for an element matching all three rules, it produces an equivalent effect to background-image: url(two.jpg), url(one.jpg), url(base.jpg);.

In other words, so long as the element matches the div.foo rule, the value will contain url(one.jpg).

(You can override the entire thing with a higher-specificity declaration that doesn’t use a cascade modifier, but that overrides the base.jpg too. There’s no way to directly override just one of the modified declarations.)

The preferred pattern to achieve this is to use a custom property:

To fix the previous example, so base.jpg always applies but one.jpg and two.jpg apply based on specificity, you can write:
div {
  background-image: url(base.jpg);
  +background-image: var(--upper-background) !important;
}
div.foo {
  --upper-background: url(one.jpg);
}
div.foo.bar {
  --upper-background: url(two.jpg);
}
Why not just use variables by themselves? Why the extra complexity?

The above example could instead be written only using variables, with no cascade controls:

div {
  background-image: var(--upper-background, none), url(base.jpg);
}
div.foo {
  --upper-background: url(one.jpg);
}
div.foo.bar {
  --upper-background: url(two.jpg);
}

While this works in simple situations, it’s less useful as more things interact. It requires that background-image never be disturbed; if anything else attempts to set background-image, it’ll wipe out the variable use. The cascade modifier approach, on the other hand, maintains the images set by the variables even if other code sets the background-image property.

3. cascade() Function

More directly but slightly more complex, we coudl add a cascade() function that’s accepted by all set-valued and list-values properties as a whole "unit". By default it subs in the previous cascaded value for itself, but for set-valued things it needs to offer more functionality to do difference/intersection.

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-BACKGROUNDS-3]
CSS Backgrounds and Borders Module Level 3 URL: https://drafts.csswg.org/css-backgrounds-3/
[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-COLOR-4]
Tab Atkins Jr.; Chris Lilley. CSS Color Module Level 4. 5 July 2016. WD. URL: https://drafts.csswg.org/css-color/
[CSS-LISTS-3]
Tab Atkins Jr.. CSS Lists and Counters Module Level 3. 20 March 2014. WD. URL: http://dev.w3.org/csswg/css3-lists/
[CSS-TRANSFORMS-1]
Simon Fraser; et al. CSS Transforms Module Level 1. 26 November 2013. WD. URL: http://dev.w3.org/csswg/css-transforms/
[CSS-VARIABLES-1]
Tab Atkins Jr.. CSS Custom Properties for Cascading Variables Module Level 1. 3 December 2015. CR. URL: http://dev.w3.org/csswg/css-variables/
[CSS-WILL-CHANGE-1]
Tab Atkins Jr.. CSS Will Change Module Level 1. 3 December 2015. CR. URL: http://dev.w3.org/csswg/css-will-change/
[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
[SELECTORS-4]
Selectors Level 4 URL: https://drafts.csswg.org/selectors-4/

Issues Index

This involves defining the "unit" of each set-valued property (for example, in will-change each keyword is a "unit"), and ensuring that all of them have a "null" value (like none).
The "unit" of list-valued properties are much easier to define; for all but some legacy properties like counter-reset, it’s just splitting on commas.
Define that, for all of these, we interpret the property as normal first, then split it into units for merging; this isn’t variable-style concatenation.

That is, the following does not define a 2px-offset blue shadow:

.foo {
  box-shadow+: 2px 2px;
}
.foo {
  box-shadow+: blue;
}

As each property is still interpreted as a property, the first is interpreted the same as box-shadow: 2px 2px; (specifying a single drop-shadow set to currentcolor), while the second is simply invalid.

More directly but slightly more complex, we coudl add a cascade() function that’s accepted by all set-valued and list-values properties as a whole "unit". By default it subs in the previous cascaded value for itself, but for set-valued things it needs to offer more functionality to do difference/intersection.