CSS3 Lists Planned Updates

Last updated:

I've taken over editorship of the CSS3 Lists module, which last saw some editor love eight years ago. I won't start active editting until mid-January, but I want to put up an outline of what I'll be doing so people have a heads up. There'll be one big change, and a bunch of smaller edits.

Cleanup and Harmonization

The Lists module is fairly old, and CSS 2.1 has changed out from underneath it a little bit. I'll be doing some cleanup in this area, to match the decisions the CSSWG has made regarding lists. This is mainly just the behavior of list markers, text direction, and text-align.

I also need to make the box-tree state of list markers more specific, so they're not underspecified magic. For example, inside list markers should be box-tree children of the list-item box, placed before the ::before pseudoelement (if it exists).

Internationalization Additions

Earlier this year I participated in the "Additional Requirements for Bidi in HTML" meetings, where we discussed what changes need to be made to the web languages to better handle rtl languages and bidirection (mixed direction) text. One of the things that came out of that was the recognition that the current list-style-position values don't work well for mixed-direction lists. The marker places itself according to the direction of the list item, which means you could potentially have markers on both sides of the list. This is confusing and weird, so we'll have a new position value that makes it determine what side to be on based on the list item's parent's direction. There's also some tweaking that needs to be done regarding how list markers interact with text-align in bidi contexts.

Strings as list-style-type

This was a really low-hanging fruit. Strings are now a valid list-style-type value, so you can just say, for example, ul { list-style-type: '▶'; }, and get the effect you want without having to fiddle around with ::marker and the content property (which won't inherit properly, like list-style will). (Note that, if you would prefer to refer to this style by a name (like triangle) rather than having to copypaste the unicode character or remember the escape every time, you can use @counter-style (below) to make a new named type.

display: marker ?

display:marker was introduced in CSS2 (it was removed in 2.1) before the ::marker pseudo-element was dreamed up. I think it still has uses, though - in legal documents, the counter style is part of the content and mustn't change just because the document was viewed without CSS. Right now, that just means you have to specify the marker inline and set list-style:none; on the lists. That removes some of the useful functionality surrounding marker positioning, though.

I think I'll retain display:marker for this use-case, so you can write the marker inline, wrap it in a span with display:marker, and then set list-style:inline; to make the first display:marker box move into the ::marker pseudo-element. Without CSS, you'll just see the marker inline with the content. With CSS, you'll have a proper list marker that can be affected by the various list properties.

@counter-style rule

The biggest change will be the abolishment of the gigantic list of new list-style-types, in favor of an @counter-style rule that lets authors define how list markers are rendered, along with a gigantic required UA stylesheet that reimplements all the types currently in the draft using @counter-style instead. These are general counter styles, so they can be used with the counter() function as well as in list-style-type. It'll look like this:

@counter-style upper-hexadecimal {
  type: numeric;
  glyphs: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F";
  suffix: ". ";

ol.hex {
  list-style: upper-hexadecimal;

Hopefully the properties above should be self-explanatory, but I'll go into some detail anyway. For completeness, the contents of @counter-style are a declaration block that accept a limited set of properties (this detail is necessary so that error handling is defined for things like specifying the same property twice). Properties other than the below are ignored without invalidating the @-rule, just like a normal declaration block. If multiple @counter-style blocks define the same name, author-defined counter-styles win over UA-defined ones, and user-defined counter-styles win over author-defined ones. Within each group, last wins.

type : The type of list, which determines how the counter uses its glyphs to create markers. The current Lists spec gives most of the basic categories. I'll go into detail about each type later. This is a required property - if it's not present or invalid, the @counter-style is invalid as a whole and ignored.

range : Many numbering systems are only defined over a finite range of numbers. For example, the basic Tamil numbering system can only represent numbers between 1 and 9999. This property takes two numbers which define the range over which the defined counter style is valid. Outside this range it instead uses the fallback counter style (see the fallback property below). Either number may instead be infinite, which means that the lower bound is negative infinity or the upper bound is positive infinity. If the counter style is defined for all numbers, a single infinite can be used; this is also the default value is the property isn't specified or is invalid.

fallback : When a counter with a value outside the counter style's range attempts to render, CSS will instead use the fallback counter style. This can be the name of any defined counter style. Fallbacks will chain until a counter style is found which includes the counter value in its range. If the fallback property isn't present, is invalid, or a cycle would be caused if the fallback was followed (two counter styles denoting each other as fallback, for example), it defaults to the decimal type, which is always defined and can't be overridden.

suffix : In ordered lists, the marker is usually a number followed a period and a space. Some languages use something other than a period, though, and unordered lists usually just have a space. suffix takes a string which is appended to the generated marker value. It defaults to " " (a space).

prefix : This is basically the same thing as suffix - it takes a string, which is prepended to the generated marker value. This is used, for example, to render counters that look like "(1)". It defaults to "" (the empty string).

glyphs : This property is where you specify how to create the marker value. It just takes a list of strings or <image>s. The treatment of this property depends on the type property - for numeric lists, the first glyph is the value for 0, the next is the glyph for 1, etc., and the number of glyphs determines what base the numbers are expressed in; for alphabetic lists, the first glyphs is the value for 1, the next is the value for 2, etc.. This property is required unless the type is additive or cjk - if it's not present or invalid, the @counter-style is invalid as a whole and ignored.

additive-glyphs : This is a variant of glyphs which is only used when the type is additive. It's a separate property because the syntax is different, and it would be kinda weird to have two completely different syntaxes in the same property. It takes a comma-separated set of pairs of glyphs and weights. The glyph comes first and is a string or <image> again, and is then followed by a positive (non-zero) integer for the weight. The last tuple may have a 0 for the weight. The tuples must be in descending order of weights or else the property is invalid. This property is required if the type is additive - if it's missing or invalid the @counter-style is invalid.

cjk-glyphs : Similarly, this is a variant of glyphs which only has meaning when the type is cjk, because it has a different syntax. It takes a space-separate list of 10 digits (strings or <image>s again), followed by a comma, then a space-separated list of 3 digit markers (strings or <image>s), a comma, then a space-separated list of (at least 3) group markers (ditto). This property is required if the type is cjk - if it's missing or invalid the @counter-style is invalid.

Counter types

There are several types of counters, which use different algorithms to convert a number (the counter value) into a marker value. Glyphs can be strings or <image>, and are combined as if they were merely adjacent inline boxes.

repeating : This counter style just cycles through its glyphs. At least one glyph must be provided in the glyphs property, or else the declaration is invalid. The first glyph is used for the value 1, and any other glyphs are used successively for 2, 3, 4, etc. When all the glyphs are used up, it starts again from the first glyphs. It also repeats backwards, so for the counter value of 0 it uses the last glyph as the marker value, for a counter value of -1 it uses the second-to-last glyph, etc. This is used for all the basic unordered-list types that contain only a single glyph.

@counter-style triangle {
  type: repeating;
  glyphs: "▶";

non-repeating : This counter style runs through its glyphs once, then automatically drops to the fallback type. At least one glyph must be specified, or else the declaration is invalid. By default, the first glyph is taken as the value for 1 - when you specify this type, you can optionally specify an integer after it (right in the type property value) that specifies a different value for the first glyph. For example, the circled-decimal type defined in the current draft starts at 0, and the decimal-leading-zero type starts at -9 (with a glyph of "-09"). Later glyphs are associated with the successive values. Regardless of the lower limit specified by the range property, if the counter value is less than the value of the first glyph, the list will drop down to the fallback counter style. Similarly, regardless of the ending point given by the range property, once all the glyphs have been used once, the list will drop down to the fallback counter style. The range property is still respected as normal if it specifies a tighter range than this implicit range, though it would be kind of useless for an author to specify that.

@counter-style circled-decimal {
  type: non-repeating 0;
  glyphs: "⓪" "①" "②" "③" "④" "⑤" "⑥" "⑦" "⑧" "⑨" "⑩" "⑪" "⑫" "⑬" "⑭" "⑮" "⑯" "⑰" "⑱" "⑲" "⑳" ;

numeric : This constructs its marker values as if they were numbers, just using a different set of digits or a different base. Significantly, there is a zero value that no value starts with except for 0 itself. At least two glyphs must be specified, or the declaration is invalid. Negative numbers are always prepended with a "-" (hyphen minus), between the value and the prefix string.

@counter-style upper-hexadecimal {
  type: numeric;
  glyphs: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F";
  suffix: ". ";

alphabetic : Similar to numeric, except there's no zero value and all numbers are positive. At least two glyphs must be specified, or the declaration is invalid. To construct marker values with this type, start by cycling through all the glyphs, with the first digit taken as the value for 1. When you exhaust all the glyphs, add an additional column to the front of the value which initially takes the first glyph and cycles through all the previous combinations, then takes the second glyph and cycles through all the combinations, etc. So, for example, the upper-alpha type goes "A, B, C, ... Y, Z, AA, AB, AC, ... AZ, BA, BB, ... ZZ, AAA, AAB, ...". Regardless of what the range property specifies as the lower range, if the counter value is 0 or negative, the list drops down to the fallback counter style.

@counter-style upper-alpha {
  type: alphabetic;
  glyphs: "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z";
  suffix: ". ";

symbolic : This style cycles through its glyphs repeatedly, doubling the glyphs on the second pass, tripling them on the third pass, etc. At least one glyph must be specified, or the declaration is invalid. To construct marker values, first cycle through the glyphs, with the first glyph taken as the value for 1. Once you've exhausted the glyphs, return to the first glyph, but double it. For the next value, double the second glyph, etc. When you've exhausted the glyphs again, start tripling each glyph, then quadrupling, etc. So, for example, the footnote type goes "*, ⁑, †, ‡, , ⁑⁑, ††, ‡‡, *, ...". Regardless of what the range property specifies as the lower limit, if the counter value is 0 or negative, the list drops down to the fallback counter style. This is sometimes used for legal documents, using the roman alphabet as glyphs.

@counter-style footnote {
  type: symbolic;
  glyphs: "*" "⁑" "†" "‡";

cjk : This is a somewhat more complex one. If this type is specified, the glyphs property is ignored in favor of the cjk-glyphs property. This produces numbers in accordance with the common rules used in Chinese, Japanese, Korean, and other east asian numbering system. The rules for handling this type are defined in the current spec, so I won't repeat them here. If the cjk-glyphs property is missing or invalid, the entire declaration is invalid. Regardless of the lower limit specified in the range property, if the counter value is negative, the list drops down to the fallback counter style (note that 0 is allowed in this type). Regardless of the upper limit specified in the range property, if there are more groups produced by the algorithm than there are group markers, the list drops down to the fallback counter style.

@counter-style japanese-formal {
  type: cjk;
  cjk-glyphs: "零" "壹" "貳" "參" "肆" "伍" "陸" "柒" "捌" "玖", "拾" "佰" "仟", "万" "億" "兆";
  range: 0 9999999999999999;
  suffix: ". ";

additive : The final counter type, this is used for several of the more complex counter styles, like Tamil and Roman Numerals. If this type is specified, the glyphs property is ignored in favor of the additive-glyphs property. If additive-glyphs is missing or invalid, the entire declaration is invalid. With this counter type, the marker value is constructed by starting with an empty string, then walking the list of tuples until you find the first one with a weight less than or equal to the counter value. If the counter value started at 0, and the last tuple in the list has a weight of 0, the marker value is the glyph in that last tuple. Otherwise, append the associated glyph to the current string, subtract the weight from the counter value, and start again until the counter value reaches 0, or you've looked through the entire list without finding a guard less than the current value. If you were able to finish and empty the counter, the resulting string (or rather, inline box, since it can have images in it) is the marker value. If you were left at the end with nothing left to match and a non-zero counter value, then drop down to the fallback counter style instead for that counter value. Note that it is impossible to represent negative numbers using this algorithm, so negative counter values will always drop down to the fallback counter style.

@counter-style upper-roman {
  type: additive;
  additive-glyphs: "M" 1000, "CM" 900, "D" 500, "CD" 400, "C" 100, "XC" 90, "L" 50, "XL" 40, "X" 10, "IX" 9, "V" 5, "IV" 4, "I" 1;
  suffix: ". ";
  range: 1 4999;

Both the symbolic and additive types have the potential to essentially DOS the user if a very large counter value is specified, as the length of their marker values increase linearly with the size of the counter value, while the length of the written-out counter value itself increases with the log of the counter value. That is, writing HTML like <ul start=1000000000000> only requires 13 digits to specify a value of one trillion, but if the footnote counter style is used, the marker value for the first marker will be 250 billion characters long. To guard against this, browsers may drop down to a fallback counter style if they determine the marker value would be too large to reasonably produce and display. The definition of "too large" is undefined and UA-dependent, and may vary from counter style to counter style.

Special Cases

A few list types are specially defined, rather than done through a @counter-style property. The hebrew and ethiopian-numeric types can't be represented by any of the above algorithms without special exceptions just for them, which I can't currently justify, so they'll be explicitly defined in the spec. (Their names can still be overridden by @counter-style blocks from the author or user.) ancient-tamil (just known as 'tamil' in the current spec) may fall into the same boat; I think the current spec algorithm is wrong, so I'll reserve judgement until I verify it.

The decimal counter style can't be overridden like all the other counter styles can. This is so it can serve as an ultimate fallback style no matter what.

Finally, a few names can't be used at all, because they'd conflict with other values in list-style, like none, outside, or inherit.

(a limited set of Markdown is supported)