Totally Custom List Styles

Originally posted Apr 17, 2020 on DEV Written by Stephanie Eckles

This is the fifth post in a series examining modern CSS solutions to problems I've been solving over the last 13+ years of being a frontend developer.

This tutorial will show how to use CSS grid layout for easy custom list styling in addition to:

List HTML#

First we'll setup our HTML, with one ul and one li. I've included a longer bullet to assist in checking alignment, spacing, and line-heihgt.

<ul role="list">
<li>Unordered list item</li>
<li>Cake ice cream sweet sesame snaps dragée cupcake wafer cookie</li>
<li>Unordered list item</li>
</ul>

<ol role="list">
<li>Ordered list item</li>
<li>Cake ice cream sweet sesame snaps dragée cupcake wafer cookie</li>
<li>Ordered list item</li>
</ol>

Note the use of role="list". At first, it may seem extra, but we are going to remove the inherent list style with CSS. While CSS doesn't often affect the semantic value of elements, list-style: none can remove list semantics for some screen readers. The easiest fix is to define the role attribute to reinstate those semantics. You can learn more from this article from Scott O'Hara.

Base List CSS#

First we add a reset of list styles in addition to defining them as a grid with a gap.

ol,
ul
{
margin: 0;
padding: 0;
list-style: none;
display: grid;
grid-gap: 1rem;
}

The grid-gap benefit is adding space between li, taking the place of an older method such as li + li { margin-top: ... }.

Next, we'll prepare the list items:

li {
display: grid;
grid-template-columns: 0 1fr;
grid-gap: 1.75em;
align-items: start;
font-size: 1.5rem;
line-height: 1.25;
}

We've also set list items up to use grid. And we've upgraded an older "hack" of using padding-left to leave space for an absolute positioned pseduo element with a combo of a 0 width first column and grid-gap. We'll see how that works in a moment. Then we use align-items: start instead of the default of stretch, and apply some type styling.

UL: Data attributes for emoji bullets#

Now, this may not exactly be a scalable solution, but for fun we're going to add a custom data attribute that will define an emoji to use as the bullet for each list item.

First, let's update the ul HTML:

<ul role="list">
<li data-icon="🦄">Unordered list item</li>
<li data-icon="🌈">Cake ice cream sweet sesame snaps dragée cupcake wafer cookie</li>
<li data-icon="😎">Unordered list item</li>
</ul>

And to apply the emojis as bullets, we use a pretty magical technique where data attributes can be used as the value of the content property for pseudo elements:

ul li::before {
content: attr(data-icon);
// Make slightly larger than the li font-size
// but smaller than the li grid-gap
font-size: 1.25em;
}

Here's the result, with the ::before element inspected to help illustrate how the grid is working:

ul styled list elements

The emoji still is allowed to take up width to be visible, but effectively sits in the grid-gap. You can experiment with setting the first li grid column to auto which will cause grid-gap to fully be applied between the emoji column and the list text column.

OL: CSS counters and CSS custom variables#

CSS counters have been a viable solution since IE8, but we're going to add an extra flourish of using CSS custom variables to change the background color of each number as well.

We'll apply the CSS counter styles first, naming our counter orderedlist:

ol {
counter-reset: orderedlist;

li::before
{
counter-increment: orderedlist;
content: counter(orderedlist);
}
}

This achieves the following, which doesn't look much different than the default ol styling:

ol with counter

Next, we can apply some base styling to the counter numbers:

// Add to li::before rule
font-family: "Indie Flower";
font-size: 1.25em;
line-height: 0.75;
width: 1.5rem;
padding-top: 0.25rem;
text-align: center;
color: #fff;
background-color: purple;
border-radius: 0.25em;

First, we apply a Google font and bump up the font-size. The line-height is half of the applied line-height of the li (at least that's what worked for this font, it may be a bit of a magic number). It aligns the number where we would like in relation to the main li text content.

Then, we need to specify an explicit width. If not, the background will not appear even though the text will.

Padding is added to fix the alignment of the text against the background.

Now we have this:

ol with additional styles

That's certainly feeling more custom, but we'll push it a bit more by swapping the background-color to a CSS custom variable, like so:

ol {
--li-bg: purple;

li::before
{
background-color: var(--li-bg);
}
}

It will appear the same until we add inline styles to the second and third li to update the variable value:

<ol role="list">
<li>Ordered list item</li>
<li style="--li-bg: darkcyan">Cake ice cream sweet sesame snaps dragée cupcake wafer cookie</li>
<li style="--li-bg: navy">Ordered list item</li>
</ol>

And here's the final ul and ol all put together:

By Stephanie Eckles (@5t3ph)

Upgrade your algos: Multi-column lists#

Our example only had 3 short list items, but don't forget we applied grid to the base ol and ul.

Whereas in a previous life I have done fun things with modulus in PHP to split up lists and apply extra classes to achieve evenly divided multi-column lists.

With CSS grid, you can now apply it in three lines with inherent responsiveness, equal columns, and respect to content line length:

ol, ul {
display: grid;
// adjust the `min` value to your context
grid-template-columns: repeat(auto-fill, minmax(22ch, 1fr));
grid-gap: 1rem;
}

Applying to our existing example (be sure to remove the max-width on the li first) yields:

multi-column lists

You can toggle this view by updating the $multicolumn variable in Codepen to true.

Gotcha: More than plain text as li content#

If you have more than plain text inside the li - including something like an innocent <a> - our grid template will break.

However, it's a very easy solve - wrap the li content in a span. Our grid template doesn't care what the elements are, but it does only expect two elements, where the pseudo element counts as the first.