Skip to main content
Fear Goidte
Pronounced like “Far Gotcha”. The splendid online isolation of me, James Ó Nuanáin, wherein I gather together any loose cleverness that I manage to accumulate. This site is, primarily an exegesis on how it was made. For the moment, it is mostly beak and bone with very little feather and fur.
  1. Home
  2. Archives
  3. Copyright
  4. Access keys
  5. Web feed
  6. Colophon

Enumerate the ways

Nested ordered and unordered lists with hanging markers

James Ó Nuanáin

Estimated reading time: 12 minutes

Tagged with

The visual formatting of the ol and ul list elements and in particular; using generated content for the markers, setting them outside the list text, and having them ‘hang’ in the margin of the parent element.

Background

The styles I use are designed to produce certain outcomes and have specific limitations which might conflict with your own needs. The full stylings used here as well as list examples is available on the Codepen (Nested ordered and unordered lists with hanging markers).

Adaptability of styles

A list is nested when it is the direct descendent of another list as in the following stylesheet selector examples:

The number of direct-ancestor list elements a list has determines its nesting level.

Ordered lists

Ordered lists are numbered 1., 1.1, 1.1.1 &c.

These styles should able to nest ordered lists to an infinite level or at least beyond the point where the width of the numbering exceeds available screen width.

reversed="reversed"

Regrettably, at the moment, the only way I know of visually expressing the meaning of the reversed="reversed" attribute for ordered lists depends on knowing the total number of list items and using a style to ‘count down’ from that number. I have omitted that styling since it is not adaptive and therefore of limited use.

Unordered lists

Unordered lists are formatted to have different markers for each nesting depth up to a depth of five lists. This being (excluding the first level) the most that can be expressed using the mark-up h1 to h6 heading elements.

Hanging and outside markers

I find it easier on the eye, and easier to comprehend, when markers are outside of the list-item text and the markers for the top level of the list are hanging in the left-hand margin (unless the text direction is right-to-left) of its parent element.

The styles

Margins between lists

I use a standard margin of one line between many elements including ol and ul. In the case of line-height : 1.5 on the body or the html element this would equate to a standard vertical margin of 1.5rem. However, for nested lists I want them to abut each other vertically:

ol,
ul {
    margin : 1.4em 0;
    padding : 0
}

ol > li > ol,
ol > li > ul,
ul > li > ol,
ul > li > ul {
    margin : 0
}

Outside markers

By changing the display of li to table you enable the li::before pseudo element to have the table-cell display property and hang outside the list item text. On paged media it will also prevent list items from breaking across pages. list-style : none disables the usual user-agent display styles.

li {
    display : table;
    list-style : none
}

li::before {
    display : table-cell
}

Hanging markers on top-level lists

The padding on the body element creates the space for the list markers of top-level lists to hang-in. I use padding rather than margin so that if a background colour is set on body the list marker is within the boundaries of the coloured background. The padding is set to an amount that can just about accommodate an ordered list marker up till “999” as well as a small remainder to leave space between the marker and the list text. The greater the padding the less horizontal space is available which is of particular concern for narrow screens.

If the parent element of the ol or ul is floated, it will have to have the same padding as body to create the same space for .

For the top level lists, their markers need to be ‘pulled’ left into the margin of their parent element. The position : relative of the li element allows the li::before pseudo element to be absolutely positioned. I pull the list marker left by the same length quantity as the padding above.

The markers pseudo element’s default width is only sufficient to contain the marker. I prefer the top level of ordered lists to be aligned right so I need to specify a width for the marker, as I am using .2em padding between the list markers and the list text so for the top-level list’s markers I specify a width equal to the above body padding, the left positioning and minus .2em to make the marker padding consistent for all levels:

body {
    padding : 0 1.9em
}

li {
    position : relative
}

li::before {
    left : -1.9em;
    width : 1.7em;
    position : absolute;
    text-align : right
}

Undoing the positioning top level positioning for nested list markers

The below selectors again use > to ensure that they are targeting only list items that are directly descended from other list items.

The only positive styling is the padding-right : .2em which is equivalent to the difference between the left positioning and width values for top level list items above.

The other stylings merely reset the value since the left positioning and right are align are only used for top level lists.

li > ol > li::before,
li > ul > li::before {
    padding-right : .2em;
    position : static;
    text-align : initial;
    width : auto
}

Setting counters

In their Cascading Style Sheets Level 2 Revision 1 (C.S.S. 2.1) Specification the W.3.C. recommended resetting counters on the ol element. Unfortunately, they seem to have been as perplexed by their counter rules as everyone else. Reseting counters on the ol element means that an ordered list which is the descendent of a sibling element of a previous ordered list will behave as a continuation of the previous list. Quite a mouthful; suffice to say, reset counters on the first-child of an ordered list.

It is necessary to specify “counter” for top level ordered lists and then “counters” for nested ordered lists to be able to display a properly numbered ordered list within an unordered list.

Since I am using full stops to punctuate the numbers in nested ordered list markers they can become quite wide and have too much space for my taste (as with initialisms). I constrict them with a small amount of negative letter-spacing.

ol > li:first-child {
    counter-reset : item
}

ol > li::before {
    content : counter(item) ". ";
    counter-increment : item;
    letter-spacing : -.025em
}

ol > li > ol > li::before {
    content : counters(item, ".") ". "
}

Unordered lists

Again, I am using > to ensure that lists are at a descendent level in relation to another unordered list:

ul > li::before {
    content : "➢"
}

ul > li > ul > li::before {
    content : "✽"
}

ul > li > ul > li > ul > li::before {
    content : "✦"
}

ul > li > ul > li > ul > li > ul > li::before {
    content : "✯"
}

ul > li > ul > li > ul > li > ul > li > ul > li::before {
    content : "✤"
}

Unsetting styles

To remove the above generated styles for ordered and unordered styles:

.no-markers > li::before {
    display : none
}

.no-markers > li:first-child {
    counter-reset : none
}

The no-markers class is for illustrative purposes.

All styles

I like to verbosely annotate my styles and then strip those comments (as well as any whitespace) before uploading them. The following styles are therefore replete with comments that echo the above article:

body {
    padding : 0 1.9em
}

ol,
ul {
    margin : 1.4em 0;
    padding : 0
}

ol > li > ol,
ol > li > ul,
ul > li > ol,
ul > li > ul {
    margin : 0 /* Remove margins between lists nested within other lists */
}

li {
    display : table /* Needed to enable “table-cell” display on “li::before” */;
    list-style : none /* Remove automatic markers */;
    position : relative /* This allows the top–nesting-level of lists to have their markers absolutely positioned into the margin of their parent element */
}

li::before {
    display : table-cell /* Means that the marker is placed outside of item text */;
    left : -1.4em /* For top level only, equal to the horizontal padding on the body element minus the same “.2em” as the padding on nested list items below */;
    width : 1.2em /* For top level only, creates space for list items up to “999” and enables “text-align : right” */;
    position : absolute;
    text-align : right /* For top level only, my personal preference for the variable width list numbers within the top level of an ordered list */
}

ol > li:first-child {
    counter-reset : item
}

ol > li::before {
    content : counter(item) ". ";
    counter-increment : item;
    letter-spacing : -.025em
}

ol > li > ol > li::before {
    content : counters(item, ".") ". " /* It is necessary to specify ‘counter’ for top level ordered lists and then ‘counters’ for nested ordered lists to be able to display a properly numbered ordered list within an unordered list */
}

li > ol > li::before,
li > ul > li::before {
    padding-right : .2em;
    position : static /* Undo the positioning for top level items */;
    text-align : initial;
    width : auto
}

ul > li::before {
    content : "➢"
}

ul > li > ul > li::before {
    content : "✽"
}

ul > li > ul > li > ul > li::before {
    content : "✦"
}

ul > li > ul > li > ul > li > ul > li::before {
    content : "✯"
}

ul > li > ul > li > ul > li > ul > li > ul > li::before {
    content : "✤"
}

.no-markers > li::before {
    display : none
}

.no-markers > li:first-child {
    counter-reset : none
}