CSS Variables Draft Proposal

I've finished the first cut on a draft proposal for CSS Variables.
You can find it at <http://www.xanthir.com/blog/b4AD0>.

The current state of the post is reproduced below in Markdown.

~TJ



CSS Variables Draft
===================

**[[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 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
------------

**[[Copied from <http://disruptive-innovations.com/zoo/cssvariables/>,
as they all still hold. ]]**

1. 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.

2. 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

3. 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.

4. 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 (which must start with a $, and then follow the IDENT
production), followed by whitespace, followed by an arbitrary series
of CSS tokens, capped by a semicolon at the end.

(I want to impose stricter requirements on variables values than just
"a token stream", but it's not immediately clear how to do so.  See
the Typed Variables section later down, though.)

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);
}
~~~~

Using a variable that hasn't been declared is a syntax error.  (It's
valid to use a variable that hasn't been declared *yet* - the
declaration may appear later in the stylesheet, or in another sheet
entirely.)


Variables and @import, etc.
---------------------------

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.

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 block are only available if the media
declaration 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.


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 are invalid.  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.  A single variable named `$foo` is created, with the value
`red`.  If you then delete the third rule, the second is no longer
part of a cycle, so the `$bar` variable is valid and contains the
value `red` as well.


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 global object 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, suitably transformed (see below), and the
value being the current value of that variable.

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.  (Should this exist in `document.stylesheets`?  If so,
where?)

To map a variable name to a key in the variables map, follow these steps:

1. Remove the $ prefix from the name.
2. Lowercase the remaining letters.
3. Uppercase any letters that immediately follow a hyphen.
4. Remove any hyphens.

Given this algorithm, a variable like `$main-color` will be accessed
as `css.vars.mainColor`.


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.

Received on Thursday, 10 February 2011 01:23:23 UTC