LaTeX & Typst (papers, theses, resumes and slides) Module 3 – Mathematics
Maths fonts, symbol lookup and accessible equations
Choose OpenType maths fonts, find any symbol's name or command, and make equations accessible with MathML in LuaLaTeX and alt text in Typst.
What you will learn
- Choose an OpenType maths font with unicode-math and in Typst, and pair it with the text font
- Find the Typst name and the LaTeX command of a symbol, or type it as Unicode where the engine allows
- Build a tagged LaTeX PDF whose formulas carry MathML, and check what the file contains
- Write alternative text that lets Typst export every equation as PDF/UA-1
- Explain what a copy, a validator and a screen reader each get from an equation
Before you start
What you’ll learn · Before you start
What you will learn
- Choose an OpenType maths font with unicode-math and in Typst, and pair it with the text font
- Find the Typst name and the LaTeX command of a symbol, or type it as Unicode where the engine allows
- Build a tagged LaTeX PDF whose formulas carry MathML, and check what the file contains
- Write alternative text that lets Typst export every equation as PDF/UA-1
- Explain what a copy, a validator and a screen reader each get from an equation
Before you start
On this page
A short document with two formulas passed the PDF/UA-2 check of veraPDF twice: once with MathML attached to both formulas and once with none at all. A validator checks rules, not whether a listener can follow x over 2a, and the font, the symbols and the description of every equation are the author’s choices.
Below you will choose an OpenType maths font in LaTeX and in Typst, find the name or command of any symbol, build a tagged LaTeX PDF whose formulas carry MathML, give every Typst equation the alternative text that PDF/UA-1 export demands, and see what a copy, a validator and a screen reader each get. Every result on this page comes from files built with TeX Live 2026 and Typst 0.15.1 and checked with veraPDF 1.30.
An OpenType maths font carries the shapes and the layout rules
Typst’s maths reference says that only special OpenType maths fonts are suitable for typesetting maths. Such an OpenType maths font: An OpenType font with a MATH table: the shapes of maths symbols in several sizes and the layout values an engine needs, such as how far to raise a superscript. Latin Modern Math and STIX Two Math are examples. holds the shapes of thousands of symbols and a table of layout values, such as how far a superscript is raised and how thick a fraction bar is, which the engine reads instead of guessing. Here is one formula, set by Typst in four of them:
One formula in four OpenType maths fonts, set by Typst
Text description of the diagram
Four rows show the same displayed formula: the sum, for n from 1 to infinity, of 1 over n squared, which equals pi squared over 6. Each row names its font on the left and sets the formula on the right.
New Computer Modern Math, Typst's default maths font, and Latin Modern Math, the one unicode-math loads when you choose none, look almost alike, with thin strokes. STIX Two Math is the darkest of the four, with thicker strokes in its sum sign, its letters and its fraction bars. Libertinus Math, the maths companion of Typst's default text font Libertinus Serif, is lighter and narrower. In every row the limits sit above and below the sum and the fractions are stacked, because each font carries its own table of maths layout values that Typst reads.
The four rows differ in weight and width, but every one has its limits above and below the sum and its fractions
stacked, because each font brings its own layout values. In LaTeX, the unicode-math package loads such fonts under
LuaLaTeX and XeLaTeX with \setmathfont{STIX Two Math}, and its manual says that Latin Modern Math is loaded
automatically when you choose none. In Typst, a show-set rule changes the font of every equation,
#show math.equation: set text(font: "STIX Two Math"). The session builds one formula with three fonts in LuaLaTeX and
lists what the PDFs embed, then looks at Typst’s defaults and at the figure above:
fonts/fonts.tex
% LuaLaTeX: unicode-math sets the maths in the OpenType font named by \mathfontname, which the session passes on the
% command line, so that one file builds with each font.
\documentclass{article}
\usepackage{unicode-math}
\setmathfont{\mathfontname}
\pagestyle{empty}
\begin{document}
\[
\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}
\]
\end{document} fonts/default.typ
Typst's default text font with its default maths font: $sum_(n=1)^oo 1 / n^2 = pi^2 / 6$. fonts/figure.typ
// The lesson's figure of four maths fonts (diagrams/fonts.src.svg): one formula per font, with the font's name.
// Built with typst compile --format svg and the fonts of TeX Live's texmf-dist/fonts/opentype/public folders,
// then svg_theme.py.
#set page(width: auto, height: auto, margin: 4pt)
#set text(font: "New Computer Modern")
#grid(
columns: 2,
column-gutter: 8pt,
row-gutter: 4pt,
align: (right + horizon, left + horizon),
..("New Computer Modern Math", "Latin Modern Math", "STIX Two Math", "Libertinus Math")
.map(font => (
text(size: 7pt, font),
{
show math.equation: set text(font: font)
$ sum_(n=1)^oo 1 / n^2 = pi^2 / 6 $
},
))
.flatten(),
) from session import files, run
# fonts.tex built with three OpenType maths fonts from the TeX tree, which LuaLaTeX finds by name: each run defines
# \mathfontname on the command line before it reads the file. pdffonts lists the fonts a PDF embeds. Then Typst: its
# defaults, and figure.typ, which finds the same fonts through --font-path.
files("fonts/fonts.tex", "fonts/default.typ", "fonts/figure.typ")
run("""
lualatex -interaction=nonstopmode -halt-on-error -jobname=latin '\\def\\mathfontname{Latin Modern Math}\\input{fonts.tex}' | grep -E "Warning|Output written"
lualatex -interaction=nonstopmode -halt-on-error -jobname=stix '\\def\\mathfontname{STIX Two Math}\\input{fonts.tex}' | grep -E "Warning|Output written"
lualatex -interaction=nonstopmode -halt-on-error -jobname=libertinus '\\def\\mathfontname{Libertinus Math}\\input{fonts.tex}' | grep -E "Warning|Output written"
pdffonts stix.pdf
typst compile default.typ
pdffonts default.pdf
typst fonts | grep -i math
typst compile --font-path "$(kpsewhich -var-value TEXMFDIST)/fonts/opentype/public" figure.typ
pdffonts figure.pdf
""") Output
$ lualatex -interaction=nonstopmode -halt-on-error -jobname=latin '\def\mathfontname{Latin Modern Math}\input{fonts.tex}' | grep -E "Warning|Output written"
Output written on latin.pdf (1 page, 5285 bytes).
$ lualatex -interaction=nonstopmode -halt-on-error -jobname=stix '\def\mathfontname{STIX Two Math}\input{fonts.tex}' | grep -E "Warning|Output written"
Output written on stix.pdf (1 page, 5129 bytes).
$ lualatex -interaction=nonstopmode -halt-on-error -jobname=libertinus '\def\mathfontname{Libertinus Math}\input{fonts.tex}' | grep -E "Warning|Output written"
Output written on libertinus.pdf (1 page, 4957 bytes).
$ pdffonts stix.pdf
name type encoding emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
DTBZRU+STIXTwoMath-Regular CID Type 0C Identity-H yes yes yes 4 0
CANSYA+STIXTwoMath-Regular CID Type 0C Identity-H yes yes yes 5 0
$ typst compile default.typ
$ pdffonts default.pdf
name type encoding emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
FACAPN+LibertinusSerif-Regular-Identity-H CID Type 0C Identity-H yes yes yes 9 0
XISUDI+NewCMMath-Book-Identity-H CID Type 0C Identity-H yes yes yes 12 0
$ typst fonts | grep -i math
New Computer Modern Math
$ typst compile --font-path "$(kpsewhich -var-value TEXMFDIST)/fonts/opentype/public" figure.typ
$ pdffonts figure.pdf
name type encoding emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
EDHZGX+NewCM10-Regular-Identity-H CID Type 0C Identity-H yes yes yes 24 0
JNDSZR+NewCMMath-Book-Identity-H CID Type 0C Identity-H yes yes yes 27 0
SAMQQR+LatinModernMath-Regular-Identity-H CID Type 0C Identity-H yes yes yes 30 0
PESKJC+STIXTwoMath-Regular-Identity-H CID Type 0C Identity-H yes yes yes 33 0
UGNCRR+LibertinusMath-Regular-Identity-H CID Type 0C Identity-H yes yes yes 36 0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fonts_session.py
LuaLaTeX found STIX Two Math in the TeX tree by its name and embedded only that font for the formula. Typst’s own
default pairs two families: its text font is Libertinus Serif, but its maths font is New Computer Modern Math, the only
maths font typst fonts listed. The maths font that matches Libertinus Serif is Libertinus Math, which TeX Live
installs: pointed at TeX Live’s OpenType folder with --font-path, Typst found all four maths fonts of the figure.
Pair the maths font with the text font
Choose the maths font that belongs to the text font: Latin Modern Math with Latin Modern, New Computer Modern Math
with New Computer Modern, Libertinus Math with Libertinus Serif. In Typst, set both, for example
#set text(font: "Libertinus Serif") and #show math.equation: set text(font: "Libertinus Math"), so that an x in a
sentence and an x in a formula look related.
Symbols are found by name, or typed as Unicode characters
Every symbol has a name in each system: \leq and lt.eq for ≤, \infty and infinity for ∞. To find a name you do
not know, LaTeX has the Comprehensive LaTeX Symbol List on CTAN, which lists over 25,000 symbols in tables organised by
topic and package, and unicode-math ships unimath-symbols.pdf, a listing of every maths symbol it defines. Typst’s
symbol reference names its general symbols, and its maths reference says variants are selected by appending a
modifier, such as .not or .eq. This Typst file builds a table by looking twenty names up, so it cannot list a name that Typst does not know:
symbols/symbols.typ
// Twenty symbols with their Typst names and LaTeX commands. Each row looks its symbol up by name, sym.<name>, and
// prints the character and its code point, so the table cannot list a name that Typst does not have.
#set page(width: auto, height: auto, margin: 8pt)
#let pairs = (
("alpha", "\\alpha"), ("pi", "\\pi"), ("Delta", "\\Delta"), ("plus.minus", "\\pm"),
("times", "\\times"), ("dot.op", "\\cdot"), ("lt.eq", "\\leq"), ("gt.eq", "\\geq"),
("eq.not", "\\neq"), ("approx", "\\approx"), ("in", "\\in"), ("subset", "\\subset"),
("infinity", "\\infty"), ("partial", "\\partial"), ("nabla", "\\nabla"), ("sum", "\\sum"),
("integral", "\\int"), ("arrow.r", "\\to"), ("RR", "\\mathbb{R}"), ("dots.h", "\\ldots"),
)
#let code(ch) = {
let hex = upper(str(ch.to-unicode(), base: 16))
"U+" + "0" * calc.max(0, 4 - hex.len()) + hex
}
#table(
columns: 4,
[Symbol], [Typst name], [LaTeX command], [Code point],
..pairs
.map(((name, latex)) => {
let ch = str(eval("sym." + name))
(text(font: "New Computer Modern Math", ch), raw(name), raw(latex), code(ch))
})
.flatten(),
) symbols/unicode.tex
\documentclass{article}
\begin{document}
Typed as characters: $α ≤ β$.
\end{document} symbols/unicode-lua.tex
% LuaLaTeX with unicode-math: maths characters may be typed directly.
\documentclass{article}
\usepackage{unicode-math}
\begin{document}
Typed as characters: $α ≤ β$.
\end{document} from session import files, run
# The table of twenty symbols that symbols.typ builds by looking each name up, read back with pdftotext; then the
# same two Greek letters typed straight into maths, in pdfLaTeX and in LuaLaTeX with unicode-math.
files("symbols/symbols.typ", "symbols/unicode.tex", "symbols/unicode-lua.tex")
run("""
typst compile symbols.typ
pdftotext -layout symbols.pdf - | head -21
!pdflatex -interaction=nonstopmode -halt-on-error unicode.tex > /dev/null
grep -E -A1 "^! LaTeX Error" unicode.log
lualatex -interaction=nonstopmode -halt-on-error unicode-lua.tex | grep -E "Warning|Output written"
pdftotext unicode-lua.pdf - | head -1
""") Output
$ typst compile symbols.typ
$ pdftotext -layout symbols.pdf - | head -21
Symbol Typst name LaTeX command Code point
α alpha \alpha U+03B1
π pi \pi U+03C0
Δ Delta \Delta U+0394
± plus.minus \pm U+00B1
× times \times U+00D7
⋅ dot.op \cdot U+22C5
≤ lt.eq \leq U+2264
≥ gt.eq \geq U+2265
≠ eq.not \neq U+2260
≈ approx \approx U+2248
∈ in \in U+2208
⊂ subset \subset U+2282
∞ infinity \infty U+221E
∂ partial \partial U+2202
∇ nabla \nabla U+2207
∑ sum \sum U+2211
∫ integral \int U+222B
→ arrow.r \to U+2192
ℝ RR \mathbb{R} U+211D
… dots.h \ldots U+2026
$ pdflatex -interaction=nonstopmode -halt-on-error unicode.tex > /dev/null
[exit status 1]
$ grep -E -A1 "^! LaTeX Error" unicode.log
! LaTeX Error: Unicode character α (U+03B1)
not set up for use with LaTeX.
$ lualatex -interaction=nonstopmode -halt-on-error unicode-lua.tex | grep -E "Warning|Output written"
Output written on unicode-lua.pdf (1 page, 5794 bytes).
$ pdftotext unicode-lua.pdf - | head -1
Typed as characters: 𝛼 ≤ 𝛽.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 symbols_session.py
Each row shows the character sym.<name> gave, its code point and the LaTeX command for the same symbol: RR is ℝ,
U+211D, which LaTeX writes \mathbb{R}, and dot.op is ⋅, U+22C5, LaTeX’s \cdot. When all you have is the symbol
itself, copied from a PDF or a web page, its code point is the key to both names.
You can also type a symbol as itself. Typst’s symbol reference says you can always enter Unicode symbols directly into
your text and formulas. LaTeX depends on the engine: pdfLaTeX stopped at $α ≤ β$ with Unicode character α (U+03B1) not set up for use with LaTeX, while LuaLaTeX with unicode-math set it, and its text read 𝛼 ≤ 𝛽.
Checkpoint
Check: OpenType maths fonts · Finding and typing symbols
LuaLaTeX tags every formula with MathML
A tagged PDF: A PDF that also stores its structure, such as headings, paragraphs and reading order, so screen readers and text extraction can follow it. Typst writes tagged PDF by default. More in “LaTeX and Typst: what they are and when to use each” stores a structure tree beside the page, and for a formula the most useful
thing it can hold is MathML: The W3C's markup language for mathematics, which records a formula's structure, such as which part is a fraction or a power, so that browsers can lay it out and assistive technology can read it., the W3C’s markup for the structure of mathematics: which part is
a fraction, which is a power. LaTeX’s tagging code is switched on with \DocumentMetadata, before \documentclass.
The LaTeX Project’s usage instructions call LuaLaTeX the preferred engine and say it includes a basic TeX to MathML
converter, and that with LuaLaTeX and unicode-math the mathematics is tagged with an
associated file: A file embedded in a PDF and attached to part of its structure. LaTeX's tagging code attaches the MathML and the TeX source of a formula to the formula this way., an embedded stream of XML. The session builds the same document
with LuaLaTeX twice and with pdfLaTeX twice, checks each with veraPDF, and counts the MathML files in each PDF:
a11y/math-a11y.tex
% LuaLaTeX: tagged PDF/UA-2, with MathML written for every formula by the tagging code.
\DocumentMetadata{lang=en, pdfstandard=ua-2, tagging=on}
\documentclass{article}
\usepackage{unicode-math}
\title{Roots of a quadratic}
\author{A. Learner}
\begin{document}
\section{Roots}
The equation $ax^2 + bx + c = 0$ has the roots
\[
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}.
\]
\end{document} a11y/math-a11y-pdf.tex
% pdfLaTeX: the same tagged document, with amsmath instead of unicode-math.
\DocumentMetadata{lang=en, pdfstandard=ua-2, tagging=on}
\documentclass{article}
\usepackage{amsmath}
\title{Roots of a quadratic}
\author{A. Learner}
\begin{document}
\section{Roots}
The equation $ax^2 + bx + c = 0$ has the roots
\[
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}.
\]
\end{document} from session import files, require, run
# The same tagged document built twice by LuaLaTeX with unicode-math, as the tagging code needs a first run to write
# the MathML of every formula, and twice by pdfLaTeX. veraPDF checks each PDF against PDF/UA-2 (2>/dev/null hides the
# warnings its Java runtime prints). qpdf rewrites each PDF uncompressed, so grep can count the MathML and the TeX
# source files attached to its formulas. Last, the MathML LuaLaTeX wrote for the inline formula. The size of a tagged
# LuaLaTeX PDF changes by a byte from one temporary folder to the next, so the session prints it as [size].
require("verapdf", "qpdf")
files("a11y/math-a11y.tex", "a11y/math-a11y-pdf.tex")
run("""
lualatex -interaction=nonstopmode -halt-on-error math-a11y.tex | grep -E "^WARNING|Output written"
lualatex -interaction=nonstopmode -halt-on-error math-a11y.tex | grep -E "^WARNING|Output written"
verapdf --flavour ua2 --format text math-a11y.pdf 2>/dev/null
qpdf --qdf --object-streams=disable math-a11y.pdf lua-qdf.pdf
grep -a -c "application#2fmathml+xml" lua-qdf.pdf
grep -a -c "application#2fx-tex" lua-qdf.pdf
pdflatex -interaction=nonstopmode -halt-on-error math-a11y-pdf.tex > /dev/null
pdflatex -interaction=nonstopmode -halt-on-error math-a11y-pdf.tex | grep -E "^WARNING|Output written"
verapdf --flavour ua2 --format text math-a11y-pdf.pdf 2>/dev/null
qpdf --qdf --object-streams=disable math-a11y-pdf.pdf pdf-qdf.pdf
!grep -a -c "application#2fmathml+xml" pdf-qdf.pdf
grep -a -c "application#2fx-tex" pdf-qdf.pdf
sed -n '/<math/,/<\\/math>/p' math-a11y-luamml-mathml.html | head -14
""", mask=[(r"\(1 page, \d+ bytes\)", "(1 page, [size])")]) Output
$ lualatex -interaction=nonstopmode -halt-on-error math-a11y.tex | grep -E "^WARNING|Output written" WARNING: mathml missing for hash 15B9E78F3A7CB11EA59B95C9553FB928 WARNING: mathml missing for hash 820A61339C34FF574758C4EB3BA0B09F Output written on math-a11y.pdf (1 page, [size]). $ lualatex -interaction=nonstopmode -halt-on-error math-a11y.tex | grep -E "^WARNING|Output written" Output written on math-a11y.pdf (1 page, [size]). $ verapdf --flavour ua2 --format text math-a11y.pdf 2>/dev/null PASS ~/demo/math-a11y.pdf ua2 $ qpdf --qdf --object-streams=disable math-a11y.pdf lua-qdf.pdf $ grep -a -c "application#2fmathml+xml" lua-qdf.pdf 2 $ grep -a -c "application#2fx-tex" lua-qdf.pdf 2 $ pdflatex -interaction=nonstopmode -halt-on-error math-a11y-pdf.tex > /dev/null $ pdflatex -interaction=nonstopmode -halt-on-error math-a11y-pdf.tex | grep -E "^WARNING|Output written" Output written on math-a11y-pdf.pdf (1 page, [size]). $ verapdf --flavour ua2 --format text math-a11y-pdf.pdf 2>/dev/null PASS ~/demo/math-a11y-pdf.pdf ua2 $ qpdf --qdf --object-streams=disable math-a11y-pdf.pdf pdf-qdf.pdf $ grep -a -c "application#2fmathml+xml" pdf-qdf.pdf 0 [exit status 1] $ grep -a -c "application#2fx-tex" pdf-qdf.pdf 2 $ sed -n '/<math/,/<\/math>/p' math-a11y-luamml-mathml.html | head -14 <math xmlns="http://www.w3.org/1998/Math/MathML"> <mi>𝑎</mi> <msup> <mi>𝑥</mi> <mn>2</mn> </msup> <mo lspace="0.222em" rspace="0.222em">+</mo> <mi>𝑏</mi> <mi>𝑥</mi> <mo lspace="0.222em" rspace="0.222em">+</mo> <mi>𝑐</mi> <mo lspace="0.278em" rspace="0.278em">=</mo> <mn>0</mn> </math>
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 latex_a11y_session.py
The first LuaLaTeX run warned that MathML was missing for both formulas; the second run embedded it, and the PDF held 2
MathML files and 2 files with each formula’s TeX source. The MathML of the inline formula is at the end of the session:
<msup><mi>𝑥</mi><mn>2</mn></msup> says that 2 is a power of x, which the copied text ax2 could not say. Step through
the two runs:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Switching tagging on. \DocumentMetadata, before \documentclass, sets the language, the standard to follow, PDF/UA-2, and tagging=on. The tagging code then marks headings, paragraphs and formulas in the PDF's structure tree.
- The first run. LuaLaTeX tags each formula, but it has no MathML for them yet, so the run printed WARNING mathml missing for hash, once for each of the two formulas.
- Converting the formulas. The usage instructions say LuaLaTeX includes a basic TeX to MathML converter. During the first run it wrote the MathML of every formula to one file, math-a11y-luamml-mathml.html, keyed by each formula's hash.
- The second run. The second run found the MathML and embedded it, with the default setting, as an associated file of each formula. grep counted 2 MathML files and 2 TeX source files in the PDF.
- The machine check. veraPDF checked the PDF against PDF/UA-2 and printed PASS. pdfLaTeX's build of the same document passed too, with 0 MathML files, so a pass alone does not show that the maths has its structure.
pdfLaTeX’s build also passed PDF/UA-2, yet it held 0 MathML files: the instructions say that other engines need the MathML supplied in a separate file, which a LuaLaTeX run can write. veraPDF, which describes itself as a validator for all PDF/A and PDF/UA parts, checks the rules of the standard; it does not check that the maths makes sense to a listener.
Key idea
For LaTeX, tagged maths means LuaLaTeX with unicode-math, two runs, and a check of what the PDF contains: a PDF/UA pass came with 2 MathML files from LuaLaTeX and with none from pdfLaTeX.
Typst’s PDF/UA-1 export refuses an equation without alt text
Typst writes tagged PDF by default, and its accessibility guide says that PDF/UA-1 enforces alternative descriptions
for images and mathematics and requires a document title. For maths, Typst’s equations take the description as their
alt parameter, which the equation reference says should describe the full equation in natural language. The session
exports the same document twice with --pdf-standard ua-1, first without any
alternative text: A description in words that a PDF or web page gives instead of something a reader may not see, such as an image or an equation. For an equation it reads the formula aloud.:
a11y/missing-alt.typ
#set document(title: [Roots of a quadratic])
#set text(lang: "en")
= Roots
The equation $a x^2 + b x + c = 0$ has the roots
$ x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2 a). $ a11y/math-a11y.typ
#set document(title: [Roots of a quadratic])
#set text(lang: "en")
= Roots
The equation #math.equation(
alt: "a x squared plus b x plus c equals 0",
$a x^2 + b x + c = 0$,
) has the roots
#math.equation(
block: true,
alt: "x equals minus b plus or minus the square root of b squared minus 4 a c, all over 2 a",
$ x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2 a). $,
) from session import files, require, run
# Typst's PDF/UA-1 export of the same document, first without any alternative text, then with a description on both
# equations. veraPDF checks the second PDF against PDF/UA-1, and pdfinfo -struct-text shows the structure tree. Last,
# Typst's HTML export of the same file, which writes every equation as MathML.
require("verapdf")
files("a11y/missing-alt.typ", "a11y/math-a11y.typ")
run("""
!typst compile --pdf-standard ua-1 missing-alt.typ
typst compile --pdf-standard ua-1 math-a11y.typ
verapdf --flavour ua1 --format text math-a11y.pdf 2>/dev/null
pdfinfo -struct-text math-a11y.pdf | grep -a -E "Document|H1|P \\\\(|Formula"
typst compile --features html --format html math-a11y.typ
grep -o "<math[^>]*>" math-a11y.html
grep -o "<msqrt>.*</msqrt>" math-a11y.html
""") Output
$ typst compile --pdf-standard ua-1 missing-alt.typ
error: PDF/UA-1 error: missing alt text
┌─ missing-alt.typ:4:13
│
4 │ The equation $a x^2 + b x + c = 0$ has the roots
│ ^^^^^^^^^^^^^^^^^^^^^
│
= hint: make sure your images and equations have alt text
error: PDF/UA-1 error: missing alt text
┌─ missing-alt.typ:5:0
│
5 │ $ x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2 a). $
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
│
= hint: make sure your images and equations have alt text
[exit status 1]
$ typst compile --pdf-standard ua-1 math-a11y.typ
$ verapdf --flavour ua1 --format text math-a11y.pdf 2>/dev/null
PASS ~/demo/math-a11y.pdf ua1
$ pdfinfo -struct-text math-a11y.pdf | grep -a -E "Document|H1|P \\(|Formula"
Document
H1 "Roots" (block)
P (block)
Formula ["a x squared plus b x plus c equals 0 "]:
Formula ["x equals minus b plus or minus the square root of b squared minus 4 a c, all over 2 a "]:
$ typst compile --features html --format html math-a11y.typ
warning: html export is under active development and incomplete
= hint: its behaviour may change at any time
= hint: do not rely on this feature for production use cases
= hint: see https://github.com/typst/typst/issues/5512 for more information
$ grep -o "<math[^>]*>" math-a11y.html
<math>
<math display="block">
$ grep -o "<msqrt>.*</msqrt>" math-a11y.html
<msqrt><msup><mi>𝑏</mi><mn>2</mn></msup><mo>−</mo><mn>4</mn><mi>𝑎</mi><mi>𝑐</mi></msqrt>
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typst_a11y_session.py
Typst refused the first file with PDF/UA-1 error: missing alt text, once for each equation, the inline one included,
and wrote no PDF. With math.equation(alt: …) around both, the export succeeded, veraPDF printed PASS for PDF/UA-1, and
the structure tree held two Formula elements whose text is the description. The same file exported as HTML wrote both
equations as MathML, <math> and <math display="block">, as the 0.15.0 changelog announced; the HTML export printed
a warning that it is under active development. The guide says that MathML in PDF 2.0 is still to come, so a Typst PDF
carries the description and not the structure.
Four routes, four results. Answer the questions about your own document:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Route | Formula structure | Spoken description | Machine check |
|---|---|---|---|
| pdfLaTeX, tagged Built with: \DocumentMetadata with tagging=on | Poor 0 MathML files; only the TeX source of each formula is attached | Poor no description written for you | Good veraPDF PDF/UA-2: PASS, although the maths has no MathML |
| LuaLaTeX, tagged Built with: the same, with unicode-math, two runs | Good MathML for both formulas, an associated file each | Mixed the MathML carries the structure; no prose description | Good veraPDF PDF/UA-2: PASS |
| Typst PDF/UA-1 Built with: --pdf-standard ua-1 and alt on every equation | Poor no MathML in the PDF yet; the guide calls it future work | Good your alt text on every Formula, required by the export | Good refuses to export without alt; veraPDF PDF/UA-1: PASS |
| Typst HTML Built with: --features html --format html | Good every equation as MathML, such as msqrt and msup | Mixed the alt text did not appear in the HTML | Mixed HTML export is marked as under active development |
How the answers decide
- “What is the document written in?” LaTeX: rules out Typst PDF/UA-1 and Typst HTML. The Typst routes need a Typst source.
- “What is the document written in?” Typst: rules out pdfLaTeX, tagged and LuaLaTeX, tagged. The LaTeX routes need a LaTeX source.
- “What must you deliver?” A PDF, for example to a repository: rules out Typst HTML. HTML export writes a web page, not a PDF.
- “What must you deliver?” A web page: rules out pdfLaTeX, tagged, LuaLaTeX, tagged and Typst PDF/UA-1. These routes write PDF files, not web pages.
- “Must readers be able to move through a formula's parts?” Yes, such as numerator and denominator: rules out pdfLaTeX, tagged and adds a caveat to Typst PDF/UA-1. Moving through a formula needs its structure: pdfLaTeX attached no MathML, and Typst PDFs hold only the description for now.
- “Must readers be able to move through a formula's parts?” No, a spoken description is enough: adds a caveat to LuaLaTeX, tagged. The MathML still gives the structure, but the LaTeX build wrote no sentence that reads the formula aloud.
Checkpoint
Check: Tagged maths in LaTeX · PDF/UA-1 export in Typst
Alt text reads the formula aloud
Typst’s guide asks you to describe a formula as if read out loud in natural language. A description is written for a listener who cannot see the layout, so it says what the layout shows: “x equals minus b plus or minus the square root of b squared minus 4 a c, all over 2 a”. The words “all over” do the work of the fraction bar, and “the square root of … squared” makes the order of the operations clear.
| Formula | A description a listener can follow | Why other versions fail |
|---|---|---|
$a x^2 + b x + c = 0$ |
a x squared plus b x plus c equals 0 | “ax2+bx+c=0” is the copied text, and the 2 is no longer a power |
$ (a + b) / 2 $ |
the sum of a and b, divided by 2 | “a plus b over 2” can mean a + b/2 |
$ sum_(n=1)^oo 1/n^2 = pi^2/6 $ |
the sum, for n from 1 to infinity, of 1 over n squared equals pi squared over 6 | “image of an equation” says nothing about the formula |
$x^2$, inline |
x squared | “x^2” reads the markup, not the maths |
Putting the source in the alt text
Wrong: alt: "x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2 a)", which a screen reader spells out symbol by symbol, or
alt: "equation". Right: the formula in words, with “all over” or “divided by” where the layout groups, and nothing
about how it looks, such as “image of”. The copy of a formula, the alt text and the MathML are three different
things, and a PDF/UA check sees only that the alt text exists.
Checkpoint
Check: Writing alt text for formulas
Practise: find equations without alt text, then check the descriptions
Two small tools for a document that must pass PDF/UA-1. The first finds every Typst equation that has no alt text yet, before an export refuses the file; the second flags descriptions that read the markup instead of the maths. Each step has sample tests that run in your browser:
Code lab · 2 steps
Make every equation describable
List the Typst equations that have no alt text, then flag descriptions that are empty, say image of, or spell out markup instead of reading the formula aloud.
- List the Typst equations that have no alt text To do
- Flag alt text that does not read the formula aloud To do
Exercise · Medium · Python
List the Typst equations that have no alt text
Typst's PDF/UA-1 export stops at every equation without alt text. Find them first. Write equations_without_alt(source) in alts.py. It returns, in order, a pair (line number, equation) for every equation of the Typst source that has no alternative text, counting lines from 1:
- an equation is the text from a
$to the next$on the same line, both included; an escaped dollar,\$, is text, not a dollar sign of an equation; - an equation has alt text when it sits inside a
math.equation(call, on the same line, whose arguments before the equation includealt:, as in#math.equation(alt: "x squared", $x^2$); - an equation after the call's closing parenthesis is outside it, even on the same line.
Every equation in the input is written on one line.
Starter code · alts.py
def equations_without_alt(source):
"""Return (line number, equation) for every equation of the Typst source that has no alt text."""
# Replace this comment: find the equations of each line and check the call around each one.
return [] The sample tests · test_alts.py
from alts import equations_without_alt
MISSING = """#set document(title: [Roots of a quadratic])
#set text(lang: "en")
= Roots
The equation $a x^2 + b x + c = 0$ has the roots
$ x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2 a). $"""
def test_the_lesson_file_without_alt_text():
"""both equations of missing-alt.typ, the inline one included"""
assert equations_without_alt(MISSING) == [
(4, "$a x^2 + b x + c = 0$"),
(5, "$ x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2 a). $"),
]
def test_an_equation_with_alt_text():
"""an equation inside math.equation with alt: is described"""
assert equations_without_alt('The value #math.equation(alt: "x squared", $x^2$) is short.') == []
def test_an_equation_after_the_call():
"""the call ends at its closing parenthesis"""
line = '#math.equation(alt: "y", $y$) and $z$'
assert equations_without_alt(line) == [(1, "$z$")]
def test_a_call_without_alt():
"""math.equation alone does not describe the equation"""
assert equations_without_alt("#math.equation(block: true, $ f(x) $)") == [(1, "$ f(x) $")]
def test_escaped_dollars_are_text():
"""a price is not an equation"""
assert equations_without_alt("It costs \\$5, and $x$ is the count.") == [(1, "$x$")] A hint
For each line, collect the positions of the dollar signs that have no backslash before them and pair them up. For an equation that starts at position start, find the last math.equation( before it on the line. The equation is inside that call while the text between the call's ( and the equation has closed no more parentheses than it opened, and it has alt text if that text also holds alt:.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Exercise · Easy · Python
Flag alt text that does not read the formula aloud
A PDF/UA check only sees that an equation has alt text, not whether it is any good. Write alt_problems(alt) in altcheck.py. It returns the problems of one description as a list of codes, in this order, or an empty list:
"empty": the text is empty or only spaces; return["empty"]and nothing else;"markup": the text contains source code instead of words: a backslash,^,_or$, or one ofsqrt(,frac(andplus.minus;"names-the-layout": the text describes the picture instead of the formula, because it contains, in any letter case,image of,picture oforscreenshot, or starts withequationorformula.
Starter code · altcheck.py
def alt_problems(alt):
"""Return the problems of one equation description: "empty", "markup", "names-the-layout"."""
# Replace this comment: check for an empty text, then for markup, then for words about the layout.
return [] The sample tests · test_altcheck.py
from altcheck import alt_problems
def test_a_good_description():
"""the lesson's description of the quadratic formula"""
text = "x equals minus b plus or minus the square root of b squared minus 4 a c, all over 2 a"
assert alt_problems(text) == []
def test_an_empty_description():
"""spaces only"""
assert alt_problems(" ") == ["empty"]
def test_markup_instead_of_words():
"""the formula's source code"""
assert alt_problems("x^2") == ["markup"]
assert alt_problems("x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2 a)") == ["markup"]
def test_words_about_the_picture():
"""image of, and a description that starts with Equation"""
assert alt_problems("Image of an equation") == ["names-the-layout"]
assert alt_problems("Equation for the area") == ["names-the-layout"]
def test_both_problems():
"""markup and layout words together, in order"""
assert alt_problems("formula: \\frac{a}{b}") == ["markup", "names-the-layout"] A hint
Check alt.strip() first. Then test the markup with any(mark in alt for mark in (...)), and the layout words on alt.lower(), using startswith for the two words that only count at the start.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Last, take one question at a time on each idea of the lesson; missed ideas return first:
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): why can’t any OpenType font be used to set maths?
Because setting maths needs more than letter shapes. An OpenType maths font also carries a table of layout values:
how far to raise a superscript, how thick to draw a fraction bar, how a sum sign grows in a display and how brackets
stretch. Typst’s maths reference says that only special OpenType maths fonts are suitable for typesetting maths, and in
LaTeX the unicode-math package loads them with \setmathfont under LuaLaTeX or XeLaTeX. A text font such as Libertinus
Serif has none of that, so its companion Libertinus Math is the font to choose for its formulas; in a recorded build,
the same formula set in New Computer Modern Math, Latin Modern Math, STIX Two Math and Libertinus Math differed in
weight and width but kept its limits and fractions in place in every one.
Two harder questions follow, about making a thesis’s maths accessible and about 300 equations without alt text. Decide your first step before you open each one:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A university now requires theses to be PDF/UA. Yours is a LaTeX thesis with about 400 equations, built with pdfLaTeX. How do you make its mathematics accessible, and how do you show that you have?
-
Fresher to mid
Your Typst lecture notes must export as PDF/UA-1, and the build stops with missing alt text on 300 equations, most of them inline. How do you get the export through without writing descriptions that help nobody?
Key takeaways
- An OpenType maths font carries the symbols and their layout rules; unicode-math loads one with
\setmathfontunder LuaLaTeX or XeLaTeX, Typst with a show-set rule, and its maths font should match the text font. - Symbols have names in both systems: the Comprehensive LaTeX Symbol List and unimath-symbols.pdf for LaTeX, the symbol reference for Typst; Typst and LuaLaTeX with unicode-math also take the characters themselves.
- LuaLaTeX with unicode-math and
\DocumentMetadata{…, tagging=on}attaches MathML to every formula after two runs; pdfLaTeX’s build passed PDF/UA-2 with no MathML, so check the file, not only the verdict. - Typst’s PDF/UA-1 export refuses every equation without
alt, inline ones included, and its HTML export writes equations as MathML. - Alt text reads the formula aloud in words, with “all over” or “divided by” where the layout groups, never the source code and never “image of”.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why can't any OpenType font set maths?
Maths needs an OpenType maths font, which carries the symbols and a table of layout values, such as how far a superscript is raised and how thick a fraction bar is.
-
How do you give Libertinus Serif text its matching maths font?
Use Libertinus Math: \setmathfont{Libertinus Math} with unicode-math under LuaLaTeX or XeLaTeX, or #show math.equation: set text(font: "Libertinus Math") in Typst, whose default maths font is New Computer Modern Math.
-
Where do you look up the command or name of a symbol?
The Comprehensive LaTeX Symbol List on CTAN and unicode-math's unimath-symbols.pdf for LaTeX; Typst's symbol reference, where modifiers such as .not or .eq select variants.
-
Can you type α ≤ β straight into maths?
In Typst and in LuaLaTeX with unicode-math, yes. pdfLaTeX stops with Unicode character α (U+03B1) not set up for use with LaTeX.
-
How does a LaTeX build attach MathML to its formulas?
LuaLaTeX with unicode-math and \DocumentMetadata{..., tagging=on}, run twice: the second run embedded a MathML file for each formula. pdfLaTeX's build held none.
-
What does Typst's PDF/UA-1 export do with an equation that has no alt text?
It refuses the file with PDF/UA-1 error: missing alt text, inline equations included, and writes no PDF; math.equation(alt: ...) fixes it.
-
What makes good alternative text for a formula?
The formula read aloud in words, with "all over" or "divided by" where the layout groups, never the source code, the copied text or "image of".
-
What did veraPDF's PDF/UA-2 pass not show about the maths?
Whether a listener can follow it: the same document passed with MathML attached to both formulas and with none at all.
Terms in this lesson
- alternative text
- A description in words that a PDF or web page gives instead of something a reader may not see, such as an image or an equation. For an equation it reads the formula aloud.
- associated file
- A file embedded in a PDF and attached to part of its structure. LaTeX's tagging code attaches the MathML and the TeX source of a formula to the formula this way.
- MathML
- The W3C's markup language for mathematics, which records a formula's structure, such as which part is a fraction or a power, so that browsers can lay it out and assistive technology can read it.
- OpenType maths font
- An OpenType font with a MATH table: the shapes of maths symbols in several sizes and the layout values an engine needs, such as how far to raise a superscript. Latin Modern Math and STIX Two Math are examples.
References
- unicode-math: Experimental Unicode mathematical typesetting (CTAN)
- unicode-math (CTAN)
- The Comprehensive LaTeX Symbol List (CTAN)
- Tagging project: usage instructions (The LaTeX Project (Tagged PDF project))
- Tagging status of LaTeX packages and classes (The LaTeX Project (Tagged PDF project))
- Typst reference: Math (math fonts and accessibility) (Typst)
- Typst reference: equation (Typst)
- Typst accessibility guide (Typst)
- Typst reference: General symbols (sym) (Typst)
- Typst 0.15.0 changelog (HTML export and MathML) (Typst)
- Typst reference: HTML (Typst)
- veraPDF (veraPDF consortium)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress