CSS Extend Rule

A Collection of Interesting Ideas,

This version:
tabatkins.github.io/specs/css-extend-rule/
Issue Tracking:
Inline In Spec
Editor:
Tab Atkins (Google)

Abstract

This module defines the @extend rule, which allows elements to act as if they matched other selectors. This makes it easier to "subclass" styling in a page, when some new type of element should act like an existing element, but with tweaks.

Table of Contents

1. Introduction

Sometimes, when designing a page, an author might create some styles for a given type of element, such as "error" messages. Later, they might realize they need to create a "subclass" of the first type, such as a "serious error" message, which is styled the same way as "error", but with a few tweaks to make it more distinctive. Currently, CSS does not have a good way to handle this.

If the author has control over the HTML, they can declare that every element with a class of "serious-error" must also have a class of "error". This, however, is error-prone-- it’s easy to forget to add the "error" class to an element, causing confusing styling issues, and any scripting that creates or manipulates error elements has to know to maintain the states properly (for example, any time they remove the "error" class, they have to remember to check for and remove "serious-error" as well).

Alternately, this can be handled in the CSS-- every time a style rule contains a .error selector, the selector can be duplicated with .serious-error replacing it. This, too, is error-prone: it’s easy for typos or inattention to cause the duplicated selectors to drift apart, and it’s easy, when adding new .error rules, to forget to duplicate the selector.

The @extend rule, defined in this specification, fixes this common issue. It allows an author to declare that certain elements, such as everything matching .serious-error, must act as if they had the necessary features to match another selector, such as .error.

For example, the following code declares that .serious-error elements should act as if they were .error elements as well:
.error {
  color: red;
  border: thick dotted red;
}

.serious-error {
  @extend .error;
  font-weight: bold;
}

Now an element like <div class=serious-error> will have red text and border, just like elements with class=error, but will also use bold text.

This allows authors to write simple HTML, applying either class=error or class=serious-error to elements as appropriate, and write simple CSS, creating style rules that just mention .error or .serious-error, secure in the knowledge that the former rules will also apply to serious errors.

2. The @extend Rule

The @extend rule declares that a matched element must act as if it had the necessary qualities to match another specified selector. Its syntax is:

@extend <compound-selector>;

The @extend rule is only allowed inside of style rules. In any other context, an @extend rule is invalid. An @extend rule modifies the way that selector matching works for the elements matched by the style rule the @extend selector is inside of, known as the extended elements for that rule.

The argument to @extend is the extension selector. The rule’s extended elements must, for the purpose of determining if selectors match them, act as if they had the necessary features/state/etc to match the extension selector, in addition to their pre-existing features/state/etc.

For example, in the following code:
.serious-error {
  @extend .error;
}

All elements matching the .serious-error selector must act as if they also had an "error" class for the purpose of matching selectors, regardless of what their actual set of classes is.

Should this only affect selectors in CSS, or should it affect all APIs using selectors? Dunno which is saner for browsers; probably all selector-based APIs. Do other query APIs, like getElementsByTagName(), rely on the same machinery? If so, should we generalize this to allow host languages to declare arbitrary querying APIs to be "selector-ish"?

The @extend rule only affects the extended elements as long as the rule it’s inside of matches them.

For example, if the rule containing @extend is in an @media block:
.error {
  color: red;
}

@media (width > 600px) {
  .serious-error {
    @extend .error;
    font-weight: bold;
  }

  .error {
    width: 100%;
  }
}

Then the .serious-error elements only act as if they have an error class when the page’s width is greater than 600px.

Note that the extension selector can specify more than classes. For example, in the following code:
.my-button {
  @extend button;
}

Any elements with class=my-button receive the same styling as actual button elements, as if they had a tagname of button in addition to their normal tagname.

Similarly, in the following code:

.perma-pressed-button {
  @extend .button:active;
}

Any .perma-pressed elements are styled as if they were :active, so that any styling applied to "pressed" buttons via :active rules applies to them as well.

The @extend rule effectively adds qualities to an element, so that it matches other rules. The selector used to apply the @extend rule has no effect on this. For example, in the following code:
.red-text { color: red; }
.blue-text { color: blue; }

#sidebar { @extend .red-text; }
div { @extend .blue-text; }

A naive author looking at the code and wondering how a <div id=sidebar> element would be styled might assume that it gets red text, as an ID selector is used to @extend the .red-text class, versus a much less specific tagname selector. However, this is wrong—the element gets blue text, as the .red-text and .blue-text rules have equal specificity, and the .blue-text rule appears later in the stylesheet. The specificity of the rules that caused the element to match .red-text or .blue-text are irrelevant here.

While this may in some cases be confusing, it can also be a great benefit in some cases. For example, an author can define a lot of styles with simple, one-class (or one placeholder selector) rules, effectively ignoring specificity entirely, then apply them via longer, much more specific selectors, using @extend to invoke the behavior of the simpler rules. This can allow an author to avoid many of the specificity problems of using IDs in rules, for example.

2.1. @extend Chaining

Multiple @extend rules can be "chained", with one rule adding certain qualities to an element, which cause another style rule containing an @extend to match.

Note: This falls out of the definition automatically. It is called out separately for clarity, not because it’s a separate feature that needs to be specifically defined.

For example, the following code using @extend:
.error {
  color: red;
}

.serious-error {
  @extend .error;
  font-weight: bold;
}

.super-serious-error {
  @extend .serious-error;
  animation: flashing 1s infinite;
}

is equivalent to the following code without @extend:

.error, .serious-error, .super-serious-error {
  color: red;
}

.serious-error, .super-serious-error {
  font-weight: bold;
}

.super-serious-error {
  animation: flashing 1s infinite;
}

3. The Placeholder Selector %foo

The @extend rule originates in CSS preprocessors, such as Sass. Experience with those tools shows that it’s often useful to define generic, "functional" sets of styles that don’t apply to any elements directly, then use @extend to give that behavior to semantic classnames which are more meaningful within their project.

For example, the "media block" is a common functional sort of styling, originating from OOCSS, that describes a box with a picture on one side and text on the other. It might be used like the following:
.media-block {
  overflow: auto;
}
.media-block > img {
  float: left;
}
...

.image-post {
  @extend .media-block;
  ... /* additional styles to tweak the display */
}

However, this also carries the possibility of confusion. In the above example, .media-block is just used to give a name to the pattern, so that other rules can @extend it. It’s not meant to be used in a document-- there shouldn’t be any elements with class=media-block-- but this isn’t obvious from the code. It’s easy for later maintainers of the file to accidentally use .media-block directly on an element, and modify it for their own uses (after all, if they search the codebase, they’ll find no elements on the page using it!), perhaps accidentally breaking elements using it in @extend.

To avoid situations like this, and make it more clear that one is developing a "generic"/"functional"/"structural" set of styles, the placeholder selector can be used. Its syntax is similar to a class selector, but is prefixed by a % (U+0025 PERCENT SIGN) rather than a period.

The previous example could be more clearly written using a placeholder selector:
%media-block {
  overflow: auto;
}
%media-block > img {
  float: left;
}
...

.image-post {
  @extend %media-block;
}

Host languages must not provide any way for an element to match a placeholder selector; the only way for an element to match one is by using an @extend rule. This ensures that no element will ever directly match the styles using one, even by accident, and it can’t be accidentally reused for an element directly.

Placeholder selectors have the same specificity as class selectors.

Or should they have slightly less, so concrete classes can reliably override? This would mean putting a fourth number into the specificity 3-tuple.

4. Acknowledgements

The editor would like to thank the following people:

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.

References

Normative References

[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

Index

Issues Index

Should this only affect selectors in CSS, or should it affect all APIs using selectors? Dunno which is saner for browsers; probably all selector-based APIs. Do other query APIs, like getElementsByTagName(), rely on the same machinery? If so, should we generalize this to allow host languages to declare arbitrary querying APIs to be "selector-ish"?
Or should they have slightly less, so concrete classes can reliably override? This would mean putting a fourth number into the specificity 3-tuple.