Tab Completion

I'm Tab Atkins Jr, and I wear many hats. I work for Google on the Chrome browser as a Web Standards Hacker. I'm also a member of the CSS Working Group, and am either a member or contributor to several other working groups in the W3C. You can contact me here.
Listing of All Posts

Accumulating List-Valued Properties in CSS

Last updated:

Several properties in CSS (and more every day) are "list-valued"; that is, the property accepts a comma-separated list of an arbitrary number of identical (or nearly identical) values. Examples of this are the background property (and all of its subproperties), the text-shadow and box-shadow properties, and the animation and transition properties (and all of their subproperties).

Properties like margin are not list-valued in the same way. The fact that they're space-separated is irrelevant (so is counter-increment, and it's list-valued) - the issue is that they don't accept an arbitrary number of layers. These types of properties could instead be thought of as "map-valued", if you like.

All of these have a common problem - you can't adjust one layer of them without redefining the entire thing. This is very inconvenient. Here are a few suggestions on how we can solve this.

Using Variables

Variables allow a pretty simple hack around the issue. It's not a perfect solution, but once Variables are supported, it's at least a solution until we solve it properly.

Just write your code like this:

.foo {
  transition: transform .5s;
  transform: var(one, rotate(0deg)), var(two, scale(1));
}

.foo.a {
  var-one: rotate(30deg);
}

.foo.b {
  var-two: scale(1.2);
}

Now .a and .b can independently apply transforms to the same element without having to worry about clobbering each other. Adding a third class that wants to apply a transform is easy, too - just change the original transform values to include a third var, and then manipulate that var in the third class.

This isn't ideal, of course, for several reasons:

  1. It "uses up" a variable name on the element.
  2. It requires you to know a good "neutral value" for the var()'s fallback argument. This may not always be obvious (or even possible, if we don't keep this use-case in mind when designing new properties) (however, dealing with transitions pretty much requires us to define a neutral value if the property is transitionable).
  3. If you're using this on a shorthand, and you want to set the longhand of a particular layer, this gets way more complicated and annoying to use. (You have to build that layer out of multiple variables, and can't set the whole layer at once. (Well, it might be possible by putting more variables in the fallback. Still annoying.))

Numbered Longhands

A better alternative would require minor changes in CSS, but would be much friendlier. Basically, just make every list-valued property a shorthand for an infinite number of numerically-indexed longhands.

This way, the above example could instead be written as:

.foo {
  transition: transform .5s;
}

.foo.a {
  transform-1: rotate(30deg);
}

.foo.b {
  transform-2: scale(1.2);
}

Identical result, but simpler to work with, because you don't have to "plan ahead" and fill out a master version of the property with a bunch of defaults. Missing layers are just an appropriate neutral value.

This also solves problem #3 from the Variables-based solution. If you want to tweak, say, just the background-position of the third layer, just use the background-position-3 property. This works even if other rules are setting the full background-3 property.

Other Solutions?

A remaining problem with numbered longhands is that you still have to coordinate within your stylesheet so that a particular numbered layer is used for a particular purpose, and not accidentally clobbered by something else.

Francois Remy tried to solve this with named shorthands for the layers, where the names were author-defined: http://lists.w3.org/Archives/Public/www-style/2012Apr/0256.html. This then runs into the problem of what order the names are applied in. Francois' suggestion just sorts them in asciibetical order, but that seem arbitrary (and prone to making people name their styles with letter prefixes to adjust the ordering). I'm not sure how to solve this in a way I'm happy with, though.

(a limited set of Markdown is supported)

It's a pitty you missed the smile I did when reading the title of your blog post :-)

In fact, I never stopped working on the CSS List-Valued Properties proposal, I was just collecting more (and more) use cases for it, and waiting the "good moment" to post it to www-style. Maybe that moment arrived...

One can find my latest "draft" here: http://fremycompany.com/TR/2012/ED-css-list-properties/

For example, I propose there a better way to order indexes (ordinal or identifer); see section 2.7 at the end of the document.

However, I've no strong opinion on the syntax. As I said to Alan Stearns earlier: If I could solve one issue in CSS, it would be that one, really...

Reply?

(a limited set of Markdown is supported)

What about a token along the lines of !important that marks the definition as being "accumulated"? Something like:

.foo { transition: transform .5s; }

.foo.a { transform: rotate(30deg) !accumulate; }

.foo.b { transform: scale(1.2) !accumulate; }

The order would be defined by document order (which solves the numbering/alpha issue) and by the cascade. It does mean that you can't arbitrarily "turn off" one of the values, but I think that that's a fair compromise...

Reply?

(a limited set of Markdown is supported)

Re #1:

Ah, cool! I didn't realize you'd kept working on it.

I'm still not sure I like the sorting, though. I don't think that specificity-sorting is much less arbitrary than asciibetical sorting. I think if I were going to get on board with named values, I'd want an explicit declaration of ordering declared somewhere.


Re #2:

That syntax has been suggested before. Unfortunately, it doesn't allow multiple rules to manipulate the same layer, so it's kinda dead. :/

Reply?

(a limited set of Markdown is supported)

Re #1:

To be more specific, take this example stylesheet:

foo { bar[a]: a; }
foo { bar[b]: b; }

This is equivalent to the following vanilla sheet, since you sort them with higher-specificity ones coming later:

foo { bar: a, b; }

Now say we want to override the [a] one, and change our stylesheet to:

foo { bar[a]: a; }
foo { bar[b]: b; }
foo.a { bar[a]: BIG-A; }

This is now equivalent to the following vanilla stylesheet:

foo { bar: b, BIG-A; }

In other words, simply changing the value of one of the longhands had the unintended effect of also reordering the components.

I don't think this is very good. :/

Reply?

(a limited set of Markdown is supported)

I really like the numbered-longhand solution. It makes loads of sense to me.

I don't think having to plan out layers is such a big deal. CSS users are probably already used to dealing with z-index, where having a plan is extremely important.

How many real-world projects will need more than a few layers? It would be trivial to document your plans with a few lines of comments.

I'm actually considering taking this idea for a non-CSS project I have. This is a pretty elegant solution to any case where you have an initial definition that needs to be partially inherited.

Reply?

(a limited set of Markdown is supported)

@Tab: Not exactly. If you have a closer look at the ordering algorithm, you'll notice that the order is chosen in function of the specificity of the lowest rule applying the named index on the element.

So in your sample:

foo { bar[a]: a; }
foo { bar[b]: b; }
foo.a { bar[a]: BIG-A; }

is now equivalent to:

foo { bar: BIG-A, b; }

which is what the author expect.

Reply?

(a limited set of Markdown is supported)

(I've taken the liberty of correcting the markup of your comment. I now allow some markdown, but definitely not any bbcode. ^_^)

Ah, I see what you mean. That doesn't change my point, though. :/ It's still easy for simple edits (moving a segment to a different, simpler selector) to cause unintentional reordering.

Like I said, I just don't think I'll be happy with anything less than an explicitly-specified ordering for the named segments. Anything else has the potential for confusion.

Reply?

(a limited set of Markdown is supported)

Re #7: (Well, time for me to learn Markdown, I guess)

There's still the possibility to define a constant somewhere mapping a name to a number. But I find that ugly.

Or we can revert to Alphabetical sort and leave the possibility to people who like it to use prefixes to sort things out. I'm in favor of this solution because you can easily have an understandable name and do a CTRL+H without too much risk of collisions.

Alternatively, we can leave that out and suggest authors to use numbers and comment their code. However, by experience, this has zero chance of hapening. Plus this is ugly, too.

Reply?

(a limited set of Markdown is supported)

#9 - Aryeh Gregor:

Why not allow dot-separated indexes, like software sometimes uses for version numbers? So if you specify

.a { background-1: foo; } .b { background-2: bar; }

and later want to add something in between, you can do:

.c { background-1.1: baz; }

or 1.5, etc. More specifically, let an index be a dot-separated list of nonnegative integers, with at least one of the integers in the list nonzero. (Thus "0" and "0.0.0" are invalid, but "0.0.0.0.0.0.1" is valid. We don't want to allow "0" because then you couldn't put things before it.) Two indices are compared by dictionary order component-wise, so for instance, 1 < 1.1.0 < 1.5 < 1.10 < 1.10.0.0.0.1 < 1.10.0.0.0.2 < 2. 1 is the same as 1.0, 1.0.0, etc. This way you can always insert things in between existing indices.

If libraries want to play nice, they could say that they reserve a specific range of values, e.g., 100.1 through 100.2 or something, so authors that use the library know how to put things before or after library values if they want. It would be easy to make it fairly sure that nobody puts things in the middle of your library's range by chance if you reserve an improbable range, like 123.0.0.26.4.9 through 123.0.0.26.4.10. Of course, two different libraries that don't know about each other will have their respective values ordered arbitrarily, depending on which ranges they happen to use, but that doesn't seem readily avoidable.

So I don't think anything is going to solve the need for coordination if you really want to control the order. But if you choose your indices to be fairly long, this system will at least stop arbitrary unintentional clobbering.

Reply?

(a limited set of Markdown is supported)

Re #9: Damn, that's a good idea. Gives you most of the benefits of an arbitrary name, but still gives you a trivial, easy ordering.

I like it.

Reply?

(a limited set of Markdown is supported)

Re #7:

I'm all in for the accumulating list-valued properties.

I like most the bar[a] syntax, andI think that alphabetical order is a must, especially in case numbers would be allowed, so it would be obvious that the bar[1] would go before bar[2].

Speaking of randomness and prefixing for ordering, we could allow extra properties, like bar-order, where you could write down the preferred order of the items. So you could do this:

foo { bar-order: b, a; }
foo { bar[a]: a; }
foo { bar[b]: b; }

and get

foo { bar: b, a; }

and all the properties that were not declared in the -order, would go after all that been declared. Or there could be some placeholders that would tell where all those props would go, like

foo { bar-order: b, yield, a; }
foo { bar[a]: a; }
foo { bar[b]: b; }
foo { bar[c]: c; }
foo { bar[d]: d; }

and that would render to

foo { bar: b, c, d, a; }

or something like that.

Having an extra property for -order would give us a way to swap it easily:

foo { bar-order: b, a; }
foo { bar[a]: a; }
foo { bar[b]: b; }
foo:hover { bar-order: a, b; }

that can be useful for some effects in backgrounds, for example.

Also, we could go deeper and treat the -order properties as other list-valued ones, so…

foo { bar[a]: a; }
foo { bar[b]: b; }
foo { bar[c]: c; }
foo { bar[d]: d; }
foo { bar-order[1]: c;
      bar-order[3]: b; }

and that would render to

foo { bar: c, b, a, d; }

And in that case it become obvious that you shouldn't have a way to redeclare the numeric ones, so the bar[1] would always point to the first one etc.

Those were some rough thoughts on the problem, hope they would be useful, feel free to correct me :)

Reply?

(a limited set of Markdown is supported)