- From: Tab Atkins Jr. <jackalmage@gmail.com>
- Date: Wed, 9 Feb 2011 17:22:30 -0800
- To: www-style list <www-style@w3.org>
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