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):
- first item
- second item
- 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):
(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):
- one item
- another item
- 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:
- first item
- second item
- 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):
- one new item
- another recent item
- 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:
- one new item
- another recent item
- 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:
- 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.
- 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:
- 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.
- 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.