CSS Cascade Control

Unofficial Proposal Draft,

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

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.

CSS is a language for describing the rendering of structured documents (such as HTML and XML) on screen, on paper, in speech, etc.

Status of this document

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 could 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

Document conventions

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.

Advisements are normative sections styled to evoke special attention and are set apart from other normative text with <strong class="advisement">, like this: UAs MUST provide an accessible alternative.

Conformance classes

Conformance to this specification is defined for three conformance classes:

style sheet
A CSS style sheet.
renderer
A UA that interprets the semantics of a style sheet and renders documents that use them.
authoring tool
A UA that writes a style sheet.

A style sheet is conformant to this specification if all of its statements that use syntax defined in this module are valid according to the generic CSS grammar and the individual grammars of each feature defined in this module.

A renderer is conformant to this specification if, in addition to interpreting the style sheet as defined by the appropriate specifications, it supports all the features defined by this specification by parsing them correctly and rendering the document accordingly. However, the inability of a UA to correctly render a document due to limitations of the device does not make the UA non-conformant. (For example, a UA is not required to render color on a monochrome monitor.)

An authoring tool is conformant to this specification if it writes style sheets that are syntactically correct according to the generic CSS grammar and the individual grammars of each feature in this module, and meet all other conformance requirements of style sheets as described in this module.

Requirements for Responsible Implementation of CSS

The following sections define several conformance requirements for implementing CSS responsibly, in a way that promotes interoperability in the present and future.

Partial Implementations

So that authors can exploit the forward-compatible parsing rules to assign fallback values, CSS renderers must treat as invalid (and ignore as appropriate) any at-rules, properties, property values, keywords, and other syntactic constructs for which they have no usable level of support. In particular, user agents must not selectively ignore unsupported property values and honor supported values in a single multi-value property declaration: if any value is considered invalid (as unsupported values must be), CSS requires that the entire declaration be ignored.

Implementations of Unstable and Proprietary Features

To avoid clashes with future stable CSS features, the CSSWG recommends following best practices for the implementation of unstable features and proprietary extensions to CSS.

Implementations of CR-level Features

Once a specification reaches the Candidate Recommendation stage, implementers should release an unprefixed implementation of any CR-level feature they can demonstrate to be correctly implemented according to spec, and should avoid exposing a prefixed variant of that feature.

To establish and maintain the interoperability of CSS across implementations, the CSS Working Group requests that non-experimental CSS renderers submit an implementation report (and, if necessary, the testcases used for that implementation report) to the W3C before releasing an unprefixed implementation of any CSS features. Testcases submitted to W3C are subject to review and correction by the CSS Working Group.

Further information on submitting testcases and implementation reports can be found from on the CSS Working Group’s website at http://www.w3.org/Style/CSS/Test/. Questions should be directed to the public-css-testsuite@w3.org mailing list.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-BACKGROUNDS-3]
Bert Bos; Elika Etemad; Brad Kemper. CSS Backgrounds and Borders Module Level 3. 17 October 2017. CR. URL: https://www.w3.org/TR/css-backgrounds-3/
[CSS-CASCADE-4]
Elika Etemad; Tab Atkins Jr.. CSS Cascading and Inheritance Level 4. 14 January 2016. CR. URL: https://www.w3.org/TR/css-cascade-4/
[CSS-COLOR-4]
Tab Atkins Jr.; Chris Lilley. CSS Color Module Level 4. 5 July 2016. WD. URL: https://www.w3.org/TR/css-color-4/
[CSS-LISTS-3]
Tab Atkins Jr.. CSS Lists and Counters Module Level 3. 20 March 2014. WD. URL: https://www.w3.org/TR/css-lists-3/
[CSS-TRANSFORMS-1]
Simon Fraser; et al. CSS Transforms Module Level 1. 30 November 2017. WD. URL: https://www.w3.org/TR/css-transforms-1/
[CSS-VARIABLES-1]
Tab Atkins Jr.. CSS Custom Properties for Cascading Variables Module Level 1. 3 December 2015. CR. URL: https://www.w3.org/TR/css-variables-1/
[CSS-WILL-CHANGE-1]
Tab Atkins Jr.. CSS Will Change Module Level 1. 3 December 2015. CR. URL: https://www.w3.org/TR/css-will-change-1/
[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]
Elika Etemad; Tab Atkins Jr.. Selectors Level 4. 2 February 2018. WD. URL: https://www.w3.org/TR/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 could 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.