[Updated July 29 2012 - the proposal in this email has been superseded by the official [CSS Variables spec. At this time, the spec is still under flux, but it's definitely past the "proposal" stage, and is being implemented by browsers.]]
[[Standard disclaimer - this is a personal draft, and is not endorsed by the CSSWG.]]
[[Also, this draft is in active flux as it gets commented on. It may change out from underneath you.]]
This is a draft proposal for CSS Variables, which allow you to store CSS values into variables for later use in properties.
Variables are useful for many things. They aid maintenance, as a site can define its primary colors in one place and then use the variables throughout the code, so that later changes to these colors can be made with minimal code editting. They aid theming in the same way - a template can be produced that references several theme variables, so that the theme itself can be distributed as a tiny stylesheet that just defines values for the variables. They can also help with the opposite type of theming, where a set of corporate colors should be used across multiple sites, by defining a single sheet of variables that can be included across all the sites. They aid organization, as a site can easily group related values together and give them significant names, even if their actual use in the stylesheet is scattered widely.
(This proposal is highly inspired by the variables proposal originally written by Dave Hyatt and Daniel Glazman, at http://disruptive-innovations.com/zoo/cssvariables/.)
Requirements
[[Mostly copied from http://disruptive-innovations.com/zoo/cssvariables/, as they all still hold. ]]
- The definition of a variable and a call to a variable should be simple enough so web authors don't have to drastically change their existing stylesheets to take advantage of CSS Variables. Use case: remove all existing occurrences of a given value in a given stylesheet to replace them by a call to a single variable.
- The definitions of variables should cross @import boundaries so variables can be contained in a standalone stylesheet and imported by all stylesheets needing them. Use cases: company-wide graphical charter for a set of corporate web sites; easy theming on a single template
- The value of variable should be modifiable by script. Such a modification should instantaneously update the value of all properties calling the corresponding variable's value, possibly triggering changes in the rendering of the document.
- Manipulating a variable via script should be doable both through a CSSOM extension allowing direct manipulation of the @var rule in a stylesheet (so editors and similar can manipulate them within a stylesheet), and through a simple method more convenient for authors.
- Calls to a variable in the assignment of the value of a property should make the corresponding CSS declaration invalid in a non-conformant legacy browser, the CSS error handling rules allowing then a fallback to a regular CSS Level 2 rule.
Variable Syntax
The syntax for variables is very simple. Variables are defined in an @var
rule anywhere in the sheet (though it is recommended that they be defined at the top of a sheet for organizational purposes):
@var $main-color blue;
The syntax is @var
, followed by whitespace, followed by the variable name, followed by whitespace, followed by an arbitrary stream of CSS tokens with balanced (), [], and {}, capped by a semicolon at the end.
(I want to impose stricter requirements on variables values than this, but it's not immediately clear how to do so. See the "Stricter Requirements on Variable Syntax" section later in this document.)
Variable names must start with the $ glyph, then follow the IDENT production.
Using variables is very easy too - they can be used anywhere you could use a component value:
p { color: $main-color; background: url(foo) $main-color no-repeat; list-style-image: radial-gradient($main-color, $secondary-color); }
Note: You can't use a variable within a url() expression, like p { background: url($foo); }
, because $ is a valid url character. Code like the preceding is interpreted as the url "$foo", not whatever value you'd stored into the variable name $foo. You can, of course, simply make the variable itself be a url() instead, like @var $foo url(bar); p { background: $foo; }
.
Variables Availability and Scope
Variables exist in the global scope. @import'ing a stylesheet makes any variables contained within it available to all other sheets. Similarly, linking in a stylesheet makes any variables contained within it available to all other sheets. Disabled or alternate stylesheets don't contribute their variables until they're made "active".
Scoped stylesheets (those created with a <style scoped>
element in HTML) have their own nested global scope. Variables created or imported within a scoped stylesheet are only available within the scoped stylesheet; variables created in the outer global scope are still available in a scoped stylesheet.
(It's expected that we'll solve name collisions through some kind of module system giving us lightweight namespacing.)
Variables declared in an media-specific sheet or @media block are only active while the media query is true.
Multiple Variable Declarations
If the same variable name is declared in multiple @var rules, the last valid declaration wins. For this purpose, UA-defined variables come before all author-defined rules, which come before all user-defined rules. Within each category, the ordering is document order. (This is intentionally identical to normal CSS precedence rules, except that there's no tag/class/id/important bits.)
Changing the Variables in a Document
If new variables are added to the document, such as through dynamically linking in a new stylesheet, they are added to the set of variables, possibly changing the value of existing variables or adding new variables. If a new variable is introduced, any declarations that referenced that variable's name, and were thus invalid, are now valid. Similarly, removing stylesheets might remove @var rules from the document, which can change the value of variables used in a stylesheet or, if the removed @var was the only definition of a particular variable name, make declarations which reference that variable name invalid.
Invalid or Undefined Variables
If a variable is used without being defined (or it was "defined", but the definition was invalid and so no variable was created), the variable must be treated as containing a generic value that is invalid for whatever context it was used in. (In other words, it's guaranteed to invalidate the property.)
Note that CSS, being a declarative language, does not in general care where something is defined. It's okay to use a variable in a rule before it's defined, as long as the variable is defined at some later point in the stylesheet or in another sheet entirely.
The use of variables does not change the basic processing model of CSS. Given code like the following:
p { color: red; color: $foo; }
If $foo is defined to have a value that's invalid for color
(like, frex, 12px
), or if it's not defined at all (and thus computes to a guaranteed-invalid value), then <p>
doesn't get a color at all, at least not from this declaration block. [[This still requires keeping around information about the declaration stack that you currently don't have to, so you can do fallback to a previous rule. Possibly instead an invalid variable will kick a property into its initial state.]]
Variables Referring to Variables
It's valid to create a variable that depends on the value of another variable, like so:
@var $foo red; @var $bar linear-gradient(transparent, $foo);
When producing the set of variables, browsers must track which variables depend on which. If a dependency cycle is detected, all the declarations that contributed to the cycle instead become invalid variables (see above for treatment of invalid variables). For example, in this code:
@var $foo red; @var $bar $foo; @var $foo $bar;
The latter two declarations form a dependency cycle, and so are invalid. Both $foo
and $bar
are created, and contain generic guaranteed-invalid values. If you then delete the third rule, the second is no longer part of a cycle, so both variables now contain the value red
.
Changes to CSS Grammar
To be completed with boring details.
Object Model
Same as http://disruptive-innovations.com/zoo/cssvariables/ for the low-level, stylesheet-iterating form.
For use by normal authors, I'll be proposing a new property on document
named css
. css.vars
will contain a map of vars. This map will reflect all valid vars in the global namespace, with the key being the variable's name (minus the $ prefix) and the value being the current value of that variable.
(I'd also like for window.css
to forward to window.document.css
, so it's super-easy to use it in the common case. The rest of the examples assume that this exists.)
Changing the value of a map entry changes the underlying value in the stylesheet for the declaration being used to produce that value. That is, given a stylesheet like this:
@var $foo red; @var $foo blue;
Executing a command like css.vars.foo = "yellow";
and then looking at the stylesheet again would produce this:
@var $foo red; @var $foo yellow;
Deleting a map entry either deletes the current declaration producing that value, or deletes all declarations defining that variable. The former is more symmetric with the underlying behavior of the "change" action, but the latter is more symmetric with the apparent behavior, as the complexity of multiple declarations is hidden away.
To add a new map entry, we first define css.stylesheet
, which implements the StyleSheet
interface. This stylesheet is treated as an author-level sheet placed after all other author-level sheets. Creating a new map entry creates a corresponding @var rule in this stylesheet. css.stylesheet
is a perfectly normal stylesheet in all concerns - it can be iterated over and modified by the CSSOM, will show up in document.stylesheets, etc.
(Note - if your variable is named appropriately, like "$foo", you can access it as vars.foo
. If not (for example, if it contains hyphens), just use the array method of accessing keys on an object, like vars['foo-bar']
to get the variable $foo-bar.)
Issue: It has been suggested that the vars map described in this section should instead work purely through overrides. If you make a change to an existing variable or add a new one, it creates/modifies an override value for that var. The override is what's actually used in the sheet. If you read .vars for something that hasn't been overridden, it returns the values defined in @var. Defining intuitive behavior for when the underlying @vars are changed is somewhat harder, though.
Serializing Variables
Variables appear as themselves in specified values. If the variable is defined and valid, its computed value is the value of the variable. If not, its computed value is the variable name.
Typed Variables
There are many new improvements planned to the CSSOM to allow authors to edit CSS values in more intuitive ways, such as exposing .red
on colors to allow direct manipulation of the red component. When editting a property value, the property gives context to the values, so you can assign the correct interfaces to the component values based on their implied type. We'd like to expose the same abilities on variables, but they lack the context that property values have, so it's difficult to tell what interfaces to offer.
There are two approaches we've considered for dealing with this - optional typing, late typing, and "omni-typing".
(Before continuing, I'll note that the obvious answer of "inferred typing" appears to be a non-starter. There are many ambiguous cases where you can't tell ahead of time what type a value would be.)
(The definition of a 'type' is intentionally somewhat loose right now. At minimum, every primitive value is a type, as is every property. We may also want some complex component types, like <position>
.)
Optional Typing
Optional typing is just an amendment to the declaration syntax that allows the author to specify the variable's type directly. It would look like this:
@var color $foo red;
Untyped variables would just expose the legacy string-based CSSOM interface, while typed ones would expose the appropriate new interfaces based on the type.
Late Typing
The previous suggestion seems to put the typing in the wrong place. Typing doesn't help the CSS developer in any way, as CSS can figure out types as necessary all by itself. It's only useful for the JS developer. Perhaps, then, the burden of typing should be on the JS dev?
In this approach, variables are untyped by default, but JS authors can "cast" them into particular types to expose the appropriate interfaces:
css.vars.foo.asColor.red = 50; var x = css.vars.bar.asLength; print(x.em);
The object returned by a cast has all the same functionality as the original variable object, just with extra interfaces on it. If a variable's value would be invalid when interpreted as that type (for example, if the variable's value was "5px" and you called .asColor
on it), an error is thrown.
Omni-typing
The previous suggestion is somewhat cumbersome, as it requires the dev to type the variable every time they want to use an interface, or store a reference to an already-typed object. The UA still has to handle the possibility that the dev tries to cast it to a bad type. We could instead just expose all interfaces, and throw an error if the dev tries to use an interface from a type that is incompatible with the variable's value.
css.vars.foo = "red"; css.vars.foo.blue = 255; // Good print(css.vars.foo); // "rgb(255,0,255)" css.vars.foo.em = 6; // error, the above value can't be interpreted as a length
This would only work if the OM interfaces were carefully designed in such a way that there is never ambiguity, or the ambiguity is harmless. For example, a variable containing "red" could expose a .l property, which could potentially correspond to it being interpreted as a background-color list, a font-family list, or some other values, but the ambiguity doesn't affect anything here.
Possibly this could be mixed with late typing, so that we auto-expose interfaces associated with primitive types, but require casting for property-types and other complex types so the potential for ambiguity is minimized.
Stricter Requirements on Variable Syntax
The syntax for a variable is problematic. The value should be a meaningful unit of CSS, whatever that means. For example, we want to disallow code like this:
@var $foo blue, 5px; p { text-shadow: 3px 3px $foo 5px teal; }
That sort of code is simply unreadable, so it would be nice to make it an error somehow.
A lesser case of bad code is something like this:
@var $foo url(bar) left; p { background: red $foo 5px; /* equivalent to "background: red url(bar) left 5px" */ }
This is also unreadable, though it's harder to immediately see how to prevent it.
On the other hand, I'd like to allow things like this:
@var $foo 5px 5px red, 7px 7px orange; p { text-shadow: 3px 3px darkred, $foo; }
It seems that the types of values that we'd potentially like to allow fall into three categories:
- Single values, like
5px
or#ff0
orlinear-gradient(white, silver)
. I think this can be qualified as "functions, or values without spaces or separators". I should be able to give an explicit grammar for this. - Whole property values. This also includes whole chunks of list-valued properties like
text-shadow
orbackground
. It shouldn't be hard to qualify this, though it's something that you can only distinguish when a variable is used, not when it's defined. - Certain complex values, like a position, which can be 1-4 space-separated values. A position is a whole property value for
background-position
, but it's also a component of thebackground
shorthand, and an argument to the *-gradient() functions.
Restricting variable usage to just #1 and #2 shouldn't be too difficult. Variables without any separators can be used anywhere. Ones with separators must be either the sole value of a property, or be a whole chunk(s) within a list-valued property.
(3) is a bit more difficult, because you want to avoid the second invalid example above. A possible approach to take is values with spaces may be used as whole arguments to functions, and values with commas may be used as whole series of arguments to functions. This doesn't allow you to use a position in the background
shorthand, but that's a minor loss.
So, in sum, we'd be looking at three levels of usage restrictions:
- Variables which contain only a single value can be used anywhere.
- Variables which contain space-separated values can only be used as whole property values, whole chunks of list-valued properties, or whole function arguments.
- Variables which contain any other separator can only be used as whole property values, whole chunks of list-valued properties, or whole series of function arguments.
[[I need a good name for a single whole value in a list-valued property, like a background layer in the background shorthand, or a shadow in the text-shadow property. We already have "property value" for the whole thing, and "component value" for the smallest units of organization, but a single layer/shadow/etc. is another useful unit of organization that needs a name.]]