Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 20 minutes
  • Examples run with TeX Live 2026, TinyTeX 2026.10, Typst 0.15.1, Python 3.14.8, Pyodide 314.0.7, poppler 26.09.0, verapdf 1.30.2 and qpdf 12.4.1
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

The sum of 1 over n squared for n from 1 to infinity, equal to pi squared over 6, set in four OpenType maths fonts.

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:

One formula in several maths fonts

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(),
)
The fonts each PDF embeds Python · fonts_session.py
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:

Twenty symbols by name, and two typed as characters

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}
The table, and Unicode input in both LaTeX engines Python · symbols_session.py
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.

Unicode Character Inspector Paste a symbol copied from a PDF or a web page and see its code point and Unicode name, the key to finding its command.

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

  1. Question 1 of 2 A Typst document sets its text in Libertinus Serif. Which maths font matches it, and how do you set it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Libertinus Math, with #show math.equation: set text(font: "Libertinus Math")

    Set both fonts: the text font with a set rule and the maths font with a show-set rule on equations, choosing the maths font of the same family.

  2. Question 2 of 2 A source contains $α ≤ β$ typed as characters. Which build sets it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: LuaLaTeX with unicode-math, and Typst

    Typing symbols as characters works in Typst and with unicode-math; in pdfLaTeX use the names, \alpha and \leq.

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:

One tagged document for LuaLaTeX and for pdfLaTeX

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}
Two builds of each, veraPDF and the files inside the PDFs Python · latex_a11y_session.py
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:

Interactive · stage by stage How LuaLaTeX gives each formula its MathML Step through the two LuaLaTeX runs of math-a11y.tex, from the tagging switch to the file veraPDF checks.
How LuaLaTeX gives each formula its MathML, step 5 of 5. Showing tagging=on, First run: no MathML yet, MathML is written out, Second run embeds it, veraPDF checks the rules. In focus: veraPDF checks the rules, pdftex.pdfLaTeX: no MathML, PASStagging=onin \DocumentMetadataFirst run: no MathML yeta warning per formulaMathML is written outone entry per formulaSecond run embeds itan associated file eachveraPDF checks the rulesPDF/UA-2: PASS

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.:

The same document without and with alternative text

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). $,
)
Two exports, veraPDF, the structure tree and the HTML Python · typst_a11y_session.py
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:

Interactive · answer and compare Which route to accessible maths? Answer three questions about your document, and the table marks the routes that fit it.
What is the document written in?
What must you deliver?
Must readers be able to move through a formula's parts?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four routes to accessible maths against what their output gives a reader
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

  1. Question 1 of 2 Both the LuaLaTeX and the pdfLaTeX build of a tagged document passed veraPDF's PDF/UA-2 check. What does that tell you about their maths?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing about its structure; the LuaLaTeX PDF held 2 MathML files and the pdfLaTeX PDF none

    A machine check confirms the rules it tests. Look inside the PDF for the MathML, and listen to it with a screen reader, before promising accessible maths.

  2. Question 2 of 2 A Typst file with a title, a language and one inline equation is exported with --pdf-standard ua-1. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It fails with PDF/UA-1 error: missing alt text, pointing at the inline equation

    The accessibility guide says PDF/UA-1 enforces alternative descriptions for images and mathematics; add alt to every equation with math.equation(alt: …).

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

  1. One question Which alt text is best for the display $ (a + b) / 2 $?

    Choose one answer.

    Show the answer: One question

    Answer: the sum of a and b, divided by 2

    Describe the formula as if read out loud, with words such as "the sum of … divided by" or "all over" where the layout groups.

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.

  1. List the Typst equations that have no alt text To do
  2. 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 include alt:, 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:.

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 of sqrt(, frac( and plus.minus;
  • "names-the-layout": the text describes the picture instead of the formula, because it contains, in any letter case, image of, picture of or screenshot, or starts with equation or formula.

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.

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.

  1. Question 1 of 10 What does an OpenType maths font carry that a text font does not?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: A table of maths layout values, such as script positions and fraction bar thickness

    Typst's reference says only special OpenType maths fonts are suitable for maths, because of these layout values and the symbols they cover.

  2. Question 2 of 10 A LuaLaTeX document loads unicode-math but never calls \setmathfont. Which maths font does it get?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Latin Modern Math

    Without \setmathfont, unicode-math loads Latin Modern Math; with it, any OpenType maths font in the TeX tree or on the system.

  3. Question 3 of 10 Where do you look up the LaTeX command of a symbol you have only seen?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: The Comprehensive LaTeX Symbol List on CTAN, or unicode-math's unimath-symbols.pdf

    For Typst use its symbol reference; for both, the code point of the character is the common key.

  4. Question 4 of 10 What is the Typst name of LaTeX's \cdot, the centred dot of a product?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: dot.op

    Typst names follow the symbol, with modifiers for variants: dot.op is the operator form of dot.

  5. Question 5 of 10 Why did math-a11y.tex need two LuaLaTeX runs?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: The first run wrote the MathML of each formula and warned it was missing; the second embedded it

    Like references, the MathML is written on one run and used on the next.

  6. Question 6 of 10 According to the tagging instructions, how does a pdfLaTeX document get MathML for its formulas?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: From a separate file of MathML, which a LuaLaTeX run of the document can write

    LuaLaTeX with unicode-math is the direct route; pdfLaTeX needs MathML supplied from outside.

  7. Question 7 of 10 Which parameter gives a Typst equation its description?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: alt, as in math.equation(alt: "x squared", $x^2$)

    Wrap an equation in math.equation with alt, for inline equations as well as displays.

  8. Question 8 of 10 You need the formulas' structure, not only a description, and your notes are in Typst. What does Typst 0.15 offer?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: HTML export, which writes every equation as MathML

    For PDF give each equation alt text; for structure today, publish an HTML version beside it, as the accessibility guide suggests.

  9. Question 9 of 10 How does Typst's accessibility guide ask you to write a formula's description?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: As if read out loud, in natural language

    Write what a person reading the formula aloud would say.

  10. Question 10 of 10 Which description of x = (−b ± √(b² − 4ac)) / 2a keeps the grouping clear?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: x equals minus b plus or minus the square root of b squared minus 4 a c, all over 2 a

    Grouping words such as "all over", "the quantity" and "divided by" do in speech what brackets and bars do on the page.

PDF Accessibility Checker (PDF/UA) Open a PDF of your own and see whether it is tagged, which language it declares and which figures and formulas lack a description.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An OpenType maths font carries the symbols and their layout rules; unicode-math loads one with \setmathfont under 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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".

  8. 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.