Lists are more than bullets

The humble list is one of the most versatile structures on the web. A navigation menu, a multi-step checkout, a product gallery—all are lists at their core. Choosing the right HTML element and styling it well is about more than just making bullets look nice; it's about clear semantics and accessible structure.

Choosing the right list element

Use a list element whenever items need to be semantically grouped. Screen readers announce lists and their item counts, which is particularly useful for understanding content like a product grid. There are three list types to choose from:

  • Unordered list (<ul>) — for items where order doesn't matter, such as a shopping list.
  • Ordered list (<ol>) — for items where sequence is important, like a set of instructions.
  • Description list (<dl>) — for terms (<dt>) paired with descriptions (<dd>), e.g., a glossary or restaurant menu.

For a semantic navigation menu, wrap your <ul> in a <nav> element and label it. Also mark the current page with aria-current to improve accessibility.

Both <ol> and <ul> may only contain <li> elements as direct children. For <dl>, you can group each term with its descriptions inside a <div>, which is permitted by HTML and helps with styling.

Customizing markers with ::marker

Within body text, lists often need only subtle styling changes. The ::marker pseudo-element allows you to style markers with brand colors or custom images. For a more organic look, you can cycle through different images by setting the content property in ::marker with multiple URLs, as shown in a handwritten-style shopping list example.

Using counters

For ordered lists, the ::marker can use the list-item counter as its content and append additional text. By default, counters increment by one, but you can change that by setting the counter-increment property on list items—for example, to increment by three.

Knowing its limits

The ::marker pseudo-element only exposes a limited set of CSS properties. It can't be positioned with flexbox or grid. If your design requires complex alignment or animations, you can switch to styling the ::before pseudo-element instead.

Building custom markers with ::before

Styling ::before was the standard approach before ::marker was widely supported. It requires manual positioning but opens the door to more advanced layouts, such as alternating marker positions. For ordered lists, you can combine ::before with counters to generate numbers. This method gives you access to the full range of CSS properties, including animations and transitions, which have limited support on ::marker.

When you need a list that doesn't look like a list—like a navigation laid out with flexbox—set list-style: none. Be aware this removes the marker pseudo-element entirely, effectively opting out of its default behavior.

Handy list attributes

Ordered lists accept optional attributes for specific use cases.

Counting down with reversed

The reversed attribute on an <ol> counts list items down from the number of items to one. For presentational purposes, you can negate the counter increment value in CSS if using custom markers, but for semantics, use reversed in markup instead. According to the article, if CSS fails, a screen reader user should still see and understand the correct counting value. Firefox handles a negated counter correctly from the list item count, but Chrome and Safari will start at zero unless you also specify a start attribute.

Continuing lists with start

The start attribute specifies the initial numeric value of an <ol>. This is useful when splitting a long list across two separate <ol> elements with content in between. For instance, if counting down the top 20 albums in groups of ten, set start="20" on the first list so its numbering begins correctly and the second list can continue from one.

Laying out lists in columns

CSS multi-column layout suits lists nicely. By setting a column width, the list automatically flows into as many columns as space allows. You can style the space between columns with a gap and even add a visual separator with the column-rule shorthand property, similar to borders.

One common downside is that column breaks can fall in awkward places, splitting a single list item across two columns. Preventing this is straightforward: apply break-inside: avoid to the list items.

Progression and position with custom properties

CSS custom properties can enhance list styling if you know each item's index. While CSS alone can't determine an element's index, you can set a custom property in the HTML style attribute, which is convenient in a templating language. For example, a Nunjucks template can inject a property like style="--count: {{ index }}". The Splitting.js library can perform similar tasks client-side.

Using that custom property, you can simulate progress through a list. One pattern is a progress bar for each item in a multi-step process. Another is adjusting the hue of a color based on progression, calculating an hsl() value from the index custom property.

Grid layouts for description lists

Description lists allow wrapping each term-description group in a <div>, enabling distinct styling strategies. Without a wrapper <div>, you can set display: grid on the <dl> itself. This aligns terms and descriptions in fixed grid columns whose width is determined by the longest term content. For a card-style layout that distinctly groups each term with its own descriptions, wrapping them in a <div> is the more appropriate approach.