CSS Constants Module

Working Draft [DATE: 17 October 2008]

This version:
http://fantasai.inkedblade.net/style/specs/constants/02
Previous version:
http://fantasai.inkedblade.net/style/specs/constants/01
Latest version:
http://fantasai.inkedblade.net/style/specs/constants/
Editor:
fantasai

[Here will be included the file "../copyright.inc"]


Abstract

This module defines parse-time constants with poweful scoping.

Status of this document

This is an unapproved working draft and has no official status at the moment.

Comments on, and discussions of this draft can be sent on the (archived) public mailing list www-style@w3.org (see instructions).

This is a working draft and may therefore be updated, replaced or rendered obsolete by other documents at any time. It is inappropriate to use Working Drafts as reference material or to cite them as other than "work in progress". Its publication does not imply endorsement by the W3C membership or the CSS Working Group (members only).

To find the latest version of this working draft, please follow the "Latest version" link above.

Table of contents

Introduction

Web authors have been requesting some form of constants or variables or macros for the past decade. These requests have always been met by the suggestion to use a preprocessor. However no such processor has become a serious solution for this problem.

Daniel Glazman (Disruptive Innovations) and Dave Hyatt (Apple Software) have put forward a proposal for CSS Variables to fill this gap. It is both complicated and limited by trying to keep the variables scriptable. The goal of this counter-proposal is to define a purely parse-time syntax for CSS macros, one that addresses several of the most common use cases without burdening the implementation.

Use cases include:

Implementation constraints include:

Declaring Named Constants

There are three types of named constants: value constants, style-set constants, and selector constants. A value constant represents a property value, and a style-set constant represents one or more property declarations. Similarly, a selector constant represents a selector. Constant names are case-sensitive, and each type of constant has its own name space (so a value constant name cannot conflict with a style-set constant name).

All three types of constant are declared with an @define rule. The syntax of an @define rule is a the @define at-keyword, followed by either the values keyword (for declaring value constants) or the style-sets keyword (for declaring style-set constants) or the selectors keyword (for declaring selector constants), followed by a block.

@define values { ... }
@define style-sets { ... }
@define selectors { ... }

The contents of the @define block depends on whether the block is defining value constants, style-set constants, or selector constants.

The contents of an @define values block looks very similar to a declaration block and has the same syntax: a semi-colon-separated list of declarations, each given as an identifier followed by a colon followed by a value. In this case the identifier is the author-defined name of the constant rather than a property name. The value must conform to the core grammar for values; otherwise, as for property declarations, the declaration is invalid and must be ignored.

@define values {
  textColor: black;
  bgColor: white;
  accentColor: silver;
  accentBorder: double silver 5px;
}

The contents of an @define style-sets block looks similar to a style sheet consisting solely of style rules where each selector is a tag name selector: the style-set constants are given as author-defined identifiers, each followed by a declaration block. The same parsing rules apply to these declaration blocks as apply to declaration blocks in style rules.

@define style-sets {
  noteBox {
    border-style: solid;
    padding: 1em;
    border-radius: 1em;
  }
  quoteBox {
    border-style: solid none;
    padding: 1em;
    margin: 1em;
  }
}

The contents of an @define selectors block have the same syntax as the @define values syntax except a selector appears after the colon in place of the value.

@define selectors {
  navli: .navigation > ul > li;
  nava:  .navigation > ul > li > a;
}

The selectors in a @define selectors must be valid selectors, and furthermore may not use the comma-separated grouping syntax. Just like the declarations in an @define style-sets rule, a selector constant declaration that is invalid according to this definition must be ignored.

@define selectors {
  datarow: table.data > tbody > tr;
  datacell: table.data > tbody > tr > td, table.data > tbody > tr > th;
  datagroup: table.data > tbody;
}

In the above example, the datacell declaration is invalid and will be ignored, but the datarow and datagroup declarations are valid and will be preserved.

I'm aware it would be nice if the datacell declaration were valid, but since selector constants can be expanded in the middle of a selector that would add functionality (or confusion) to selectors that is not already there. Selectors Level 4 should add some syntax, such as a :matches() pseudo-class, to group alternates within a given selector. With that, datacell could then be declared as below:

@define selectors {
  datarow: table.data > tbody > tr;
  datacell: table.data > tbody > tr > :matches(td, th);
  datagroup: table.data > tbody;
}

@define rules may appear both before and after @import rules.

Scoping of Named Constants

Named constants are in-scope from the point at which they are declared until overridden by a later declaration. If declared inside an @media rule, the scope of the declaration does not end with the @media block.

The scope of a named constant does not ordinarily cross @import boundaries. However this module extends the syntax of the @import statement to allow an additional keyword between the @import keyword and the style sheet URI, and a named constant's scope can cross @import boundaries if an appropriate keyword is present. This allows implementations to easily process style sheets asyncronously when there are no constants to import, and for the author to control the scope of the constants. The keywords are:

pull
Constants declared within the imported style sheet are pulled into the importing style sheet.
push
Constants declared above the @import rule in the importing style sheet are pushed to the imported style sheet.
sync
Constants declared above the @import rule in the importing style sheet are pushed to the imported style sheet, and constants declared in the imported style sheet are pulled into the importing style sheet.

In the following example, the pull keyword is used to pull in a standard color palette.

@import pull "corporate-colors.css";

In the following example, a site-wide style sheet is modified for each sub-section by modifying the colors only.

@define values {
  textColor: #003684;
  baseTheme: #0068FF;
  lightTheme: #8CBBFF
  paleTheme: #D2E4FF;
  accentColor: #FFBE00;
}
@import push "template.css";

Expanding Named Constants

Once declared, a named constant may be used to represent its value in an appropriate context. To do so, the constant's name must be prepended with a <insert your favorite punctuation character here> backquote (`). For example,

  `datarow > td, `datarow > th {
    `notebox;
    background: url(gradient-corner.png) top left `accentColor;
  }

Style-set constants should be followed by a semicolon, otherwise the declaration after the constant will be ignored in down-level clients. However UAs supporting named constants must not require the semicolon to parse the rules as valid.

Processing Model

Processing of named constants happens at parse time, after tokenization.

The processing model for constants is that the UA maintains a table of name—value mappings and adds or replaces definitions as it encounters @define blocks. The name—value tables for value constants, style-set constants, and selector constants are independent. Upon encountering a previously-declared constant reference as it parses the style sheet, the UA replaces the constant with its value and continues by parsing the newly expanded value in place. If a constant is used before being declared, then it is not expanded, and the normal rules for ignoring invalid constructs apply.

Constant references inside @define blocks are not expanded when the @define block is parsed, but instead are expanded when the constant is expanded. This means that if a constant is declared to map to another constant, and that other constant is defined (or redefined) in a later @define block before being used in a style rule, then it will pick up the definition in that later @define block.

@define values {
  accentColor: blue;
}
p {
  background: white;
}
@define style-sets {
  box {
    color: black;
    background: `altBG;
    border: solid `accentColor;
  }
}
.notebox {
  `box;
}
@define values {
  accentColor: orange;
  altBG: yellow;
}
.warningbox {
  `box;
}

In the above example, a p.notebox will have a blue border and a white background, but a p.warningbox will have an orange border and a yellow background.

When handling imported style sheets, the UA handles the name—value tables differently depending on the type of @import statement it encounters:

Note that this model means that if a style sheet is imported twice with at least one import using either a sync or push keyword, the style sheet may need to be parsed twice to handle the constants correctly.

Acknowledgements

Inspiration for this proposal comes from the many responses to the WASP's call for feedback on CSS3 and various other blog posts requesting 'variables", "macros", "constants", "directed inheritance", and all sorts of other less clearly-defined things. :)