Background

In transcribing math contest problems from $\LaTeX$ to HTML, I’ve had to figure out how to display lists of items with bullets other than the unordered aka bulleted lists (using * in Markdown or <ul><li>...</li></ul> in HTML):

  • one item
  • another item
  • etc.

or plain ordered lists (using 1. in Markdown or <ol><li>...</li></ol> in HTML):

  1. first item
  2. second item
  3. etc.

However, some problem definitions call for more creative list formats, e.g., lower-case letters, but surrounded with parentheses (this is $\LaTeX$, rendered to SVG):

or even roman numbering surrounded by parentheses, where the text is vertically aligned together (this is also $\LaTeX$, rendered to SVG):

which is to say, not quite like this (this is plain, unstyled HTML):

(i) one item
(ii) another item
(iii) etc.
Open to see the HTML source for the above block
(i) one item<br/>
(ii) another item<br/>
(iii) etc.<br/>

However, it turns out it’s hard to recreate in HTML what’s easily done in $\LaTeX$, so let’s see how we can try to approximate it and where it fails.

HTML + Markdown implementation

Since we want this to be compatible with both HTML and Markdown, let’s define a style for <div> that will surround an ordered list and replace the list marker with (a), (b), etc.:

div.list-lower-alpha > ol {
  counter-reset: list-alpha;
}

div.list-lower-alpha > ol > li {
  list-style: none;
  counter-increment: list-alpha;
}

div.list-lower-alpha > ol > li::marker {
  content: "(" counter(list-alpha, lower-alpha) ") ";
}

And then we can use this as follows:

<div class="list-lower-alpha">

1. one item
1. another item
1. etc.

</div>

Here’s how this will render in HTML (caveat: the output is currently browser-specific; see the note below for details):

  1. one item
  2. another item
  3. etc.

Compare this with the expected $\LaTeX$ rendering:

Note: as of 4 Oct 2024, the above HTML will only render properly in Firefox, Chrome, and Chromium-derived browsers, but not Safari and other WebKit-based browsers1, which will be missing the item alphabetic counters (a), (b), and (c) entirely.

Although you might not be using Safari or WebKit directly so you might assume you’re not affected by this, it’s important to note that all iPhone and iPad browsers have been required (by Apple policy) until recently to use WebKit for their rendering engine, so even Chrome, Firefox, etc. on iPhone and iPad are all using WebKit under the covers, so this will be broken on those platforms as well.

This changed on 25 Jan 2024 when Apple was forced by EU Digital Markets Act (DMA) to enable browsers on iOS to use rendering engines other than Appel’s built-in WebKit. This is available on iOS 17.4 and higher in EU. The specificity implies this applies to iOS only, so iPad (which runs iPadOS, not iOS) will likely be excluded.

CSS standards & conformance

Unfortunately, as of this writing, Safari does not fully support the ::marker CSS pseudo-element that we tried to use above; in particular, it does not support the content property, which makes the above solution infeasible for Safari and other WebKit-based browsers, and why you’ll see no alphabetical markers (as noted above) if you’re using Safari or another WebKit-based browser at this time.

There’s a WebKit bug to implement the content property for the ::marker selector that was opened in 2019, but not yet implemented.

Thus, we need to consider alternatives and workarounds.

Alternatives

Give up and just use lower-alpha list style

Sure, we could just give up on having lists with (a), (b), etc. and just use the default browser-supported lower-alpha which will get us:

  1. first item
  2. second item
  3. third item

This works well enough, but what’s the fun in that? This is just giving up on the whole plan.

Open to see the HTML + Markdown source for the above block
<style>
  div.basic-lower-alpha > ol > li {
    list-style-type: lower-alpha;
  }
</style>

<div class="basic-lower-alpha">

1. first item
1. second item
3. third item

</div>

Test for li::marker and content support

Maybe we can test for support of li::marker and content property, and only use the feature for those browsers that support it, and fall back on the lower-alpha list style for others?

I have tried a number of approaches, but have not been able to make this work.

There’s a post on Reddit from Sep 2021 asking the same question and … no answers. There’s a similar post on Stack Overflow at around the same time from likely the same author, with the only answer saying it’s not possible.

Test for Safari/WebKit via CSS only, without JS

The next alternative is to just test for Safari as the browser, which means testing for WebKit, but we have to be careful here, since Chromium, Chrome and all derivatives use Blink rendering engine, which was originally forked from WebKit.

Of course, using JavaScript, we can check for browser vendor/version, but we want to do this without any runtime code, only CSS.

Additionally, many non-Safari browsers and non-WebKit rendering engines now support most of the -webkit-* specific CSS properties, so it’s not that simple to just test for something that appears to be WebKit-specific.

After looking at a number of discussions on forums, CSS hacks, etc., I came across a deprecated feature -webkit-transition which fits the requirement of being only supported by WebKit rendering engine, but not any others, and since it’s already deprecated, it shouldn’t be added to any other browsers later on, so it sould work for us for now.

That said, if support for it is ever removed from WebKit, it will break our CSS, but it would only break rendering for WebKit / Safari, so hopefully, they will fix the li::marker support for content before they do that.

Although WebKit doesn’t support li::marker with content, it does support list-style-type, so we will use the support for the -webkit-transition feature to override the list style type for WebKit-based browsers to simply use lower-alpha style (i.e., a., b., etc.), while all other browsers will get the fancier li::marker with content (i.e., (a), (b), etc.).

Here’s how this looks (note: rendering below depends on your browser, as described in the above paragraph):

  1. one new item
  2. another recent item
  3. etc.
Open to see the HTML + Markdown source for the above block
<style>
  /* All browsers that support li::marker + content get the default code. */
  div.limited-list-lower-alpha > ol {
    counter-reset: list-alpha;
  }

  div.limited-list-lower-alpha > ol > li {
    list-style: none;
    counter-increment: list-alpha;
  }

  div.limited-list-lower-alpha > ol > li::marker {
    content: "(" counter(list-alpha, lower-alpha) ") ";
  }

  /*
   * Put this after the ones above so it overrides it since it's later.
   * This appears to only be supported by WebKit based browsers, but not Chrome.
   */
  @media (-webkit-transition) {
    div.limited-list-lower-alpha > ol > li {
      list-style-type: lower-alpha;
    }
  }
</style>

<div class="limited-list-lower-alpha">

1. one new item
1. another recent item
1. etc.

</div>

Use li::before with content instead

Alternatively, we can consider li::before { content: "..." } which works almost as well! An answer on Stack overflow shows how this can be used.

It does add an additional indentation (since it’s part of content, not the marker), but until this is properly fixed in WebKit, this is another alternative to the simple fallback of lower-alpha list style.

Here’s how this looks:

  1. one new item
  2. another recent item
  3. etc.
Open to see the HTML + Markdown source for the above block
<style>
  div.li-before-content > ol {
    counter-reset: list-alpha;
  }

  div.li-before-content > ol > li {
    list-style: none;
    counter-increment: list-alpha;
  }

  div.li-before-content > ol > li::before {
    content: "(" counter(list-alpha, lower-alpha) ") ";
  }
</style>

<div class="li-before-content">

1. one new item
1. another recent item
1. etc.

</div>

One caveat here is that if the list item text is very long, the wrap-around will be different in this case than if this is done via the regular lower-alpha list style, which may look somewhat odd:

  1. This is a very long string of words which we expect should definitel, absolutely wrap around the next line so that we can see how the wrapping actually works in this format.
  2. And this is another such very long line that should also almost certainly wrap around because we added so much filler text that it doesn’t really have a choice, does it.

Compare this with the previous approach of lower-alpha list style which doesn’t have this issue:

  1. This is a very long string of words which we expect should definitel, absolutely wrap around the next line so that we can see how the wrapping actually works in this format.
  2. And this is another such very long line that should also almost certainly wrap around because we added so much filler text that it doesn’t really have a choice, does it.

Add WebKit support for li::marker and content

This would solve this issue and bring WebKit inline with Web standards and other browers, but this is quite a project (based on what it took to implement the same thing for Chromium), and as noted on the same bug, this is not a good first bug for someone to start contributing to WebKit, so I won’t be tackling this right now.

Hopefully, someone with more context will look into this issue soon.

Conclusion

In the end, I think using the li::before with content has certain issues with wrapping, so my preferred option here is to use the list-style-type for those browsers that support it, with a fallback on lower-alpha for those other browsers that don’t.

Appendix: rendering $\LaTeX$ to SVG

In case you’re interested as to how I generated the above $\LaTeX$ renderings to SVG to include into this page, read on.

First, I created list-lower-alpha.tex with the following contents:

\documentclass[preview]{standalone}
\usepackage{enumerate}
\usepackage[sfdefault]{roboto}

\begin{document}

\begin{enumerate}[(a)]
\item one item
\item another item
\item etc.
\end{enumerate}

\end{document}

Similarly, I created list-lower-roman.tex with these contents:

\documentclass[preview]{standalone}
\usepackage{enumerate}
\usepackage[sfdefault]{roboto}

\begin{document}

\begin{enumerate}[(i)]
\item one item
\item another item
\item etc.
\end{enumerate}

\end{document}

Then, we tie it all together with a Makefile:

# Hide commands by default without requiring `make -s`.
# To see the commands as they run, use `make VERBOSE=1`.
VERB = @
ifeq ($(VERBOSE),1)
	VERB =
endif

TEX = list-lower-alpha.tex list-lower-roman.tex

# Output files
PDF = $(TEX:.tex=.pdf)
SVG = $(TEX:.tex=.svg)

# Auto-created temporary files
AUX = $(TEX:.tex=.aux)
LOG = $(TEX:.tex=.log)

%.pdf: %.tex
	$(VERB) pdflatex $<

%.svg: %.pdf
	$(VERB) pdf2svg $< $@

.PHONY: all
all: $(SVG)

.PHONY: clean
clean:
	# We want to keep the SVGs as they're actually included in the page.
	$(VERB) rm -f $(PDF) $(AUX) $(LOG)

.PHONY: allclean
allclean: clean
	# Clean everything, if you really want to!
	$(VERB) rm -f $(SVG)

Then, we just run it as make all or simply make to render the SVGs, and make clean will delete everything other than the SVG outputs which we need to keep.


  1. E.g., on Linux, you can try the GNOME Web browser, aka Epiphany. sudo apt install epiphany-browser on Ubuntu. ↩︎