LaTeX & Typst (papers, theses, resumes and slides) Module 7 – Slides, posters and handouts
Accessible slides, posters and handouts
Make slides and posters that pass PDF/UA checks with ltx-talk or Typst, measure theme contrast, and decide whether to share slides, handout or text.
What you will learn
- Build a slide deck that passes a PDF/UA check, with ltx-talk or with Typst, and read its structure tree
- Measure theme colours against WCAG contrast ratios and keep meaning from resting on colour alone
- Design a conference poster on one large tagged page in Typst or LaTeX
- Decide what to share after a talk, the slides, the handout or a text version, for each kind of reader
Before you start
What you’ll learn · Before you start
What you will learn
- Build a slide deck that passes a PDF/UA check, with ltx-talk or with Typst, and read its structure tree
- Measure theme colours against WCAG contrast ratios and keep meaning from resting on colour alone
- Design a conference poster on one large tagged page in Typst or LaTeX
- Decide what to share after a talk, the slides, the handout or a text version, for each kind of reader
Before you start
On this page
The talk of the Beamer lesson failed 7 of the 106 rules of veraPDF’s PDF/UA-1 check, starting with the most basic one: its PDF has no structure tree, so a screen reader has nothing to follow but the order in which text was drawn. The same talk in the ltx-talk class passed all 1,727 rules of the PDF/UA-2 check, and in Typst it passed PDF/UA-1 once its equations had alternative text.
Below you will check a deck with veraPDF, port it to ltx-talk, export the Typst version to PDF/UA-1, measure the contrast of three themes’ colours, build an A0 poster in both systems and decide which PDF to share with whom. Passing a validator is where the work starts: every structure tree on this page is printed, so you can see what a listener would hear.
A validator shows what a PDF gives a screen reader
A PDF/UA: ISO 14289, the standard for accessible PDF files: tagged content, a title and a structure that assistive software can follow. PDF/UA-1 builds on PDF 1.7 and PDF/UA-2 on PDF 2.0. More in “A one-page resume in LaTeX that parses” file tells assistive software what its text is: headings, paragraphs, lists,
formulas, in reading order, in a structure tree: The tree of tags in a tagged PDF, such as headings, paragraphs, lists and links, in reading order, which screen readers follow. pdfinfo -struct-text prints it with the text of each tag. More in “A one-page resume in LaTeX that parses”. veraPDF checks a PDF against a
validation profile: The set of rules that veraPDF checks a file against, one profile for each part and level of PDF/A or PDF/UA, such as the 146 rules of PDF/A-2u. More in “Archival PDF/A and final submission checks”, here PDF/UA-1, and reports every rule it fails. vera.py
runs it and prints the verdict and the failed rules; the deck is talk.tex of the Beamer lesson, unchanged:
talk/talk-beamer.tex
\documentclass[aspectratio=169]{beamer}
\setbeamertemplate{navigation symbols}{}
\title{How much rain one roof can catch}
\author{R. Menon}
\institute{Station A rain gauge team}
\date{}
\begin{document}
\begin{frame}
\titlepage
\end{frame}
\section{The question}
\begin{frame}{Three questions}
\begin{itemize}[<+->]
\item How much rain falls on the roof in July?
\item How much of it reaches the pipe?
\item Where does the water go?
\end{itemize}
\end{frame}
\section{The answer}
\begin{frame}[label=formula]{The volume a roof catches}
\[ V = A \times R \times C \]
\pause
Roof area $A = 120~\mathrm{m^2}$, July rain $R = 0.655~\mathrm{m}$,
runoff share $C = 0.8$.
\pause
\[ V = \alert<3>{62.88~\mathrm{m^3}}, \text{ about 62,880 litres.} \]
\end{frame}
\begin{frame}{Where the water goes}
A 10,000-litre tank holds part of it.
\only<1| handout:0>{The tank fills in the first rainy week.}
\only<2>{The other 52,880 litres go to a recharge pit.}
\uncover<2>{Plan the pit before the tank.}
\end{frame}
\end{document} vera.py
"""Checks PDFs with veraPDF against one PDF/UA or PDF/A profile and prints a short report for each file: the profile,
the verdict, how many of the profile's rules passed and failed, and each failed rule with its clause and test number,
how many checks failed and veraPDF's description of the rule.
python3 vera.py ua1 talk.pdf other.pdf
The first argument is veraPDF's name for the profile (ua1, ua2, 2b …). veraPDF writes a JSON report; it also holds
times and build dates, which change on every run, so only the results are printed, and the failed rules in the order
of the standard's clauses.
"""
import json
import subprocess
import sys
def main(flavour, pdfs):
done = subprocess.run(["verapdf", "--format", "json", "--flavour", flavour, *pdfs], capture_output=True, text=True)
report = json.loads(done.stdout)["report"]
for job in report["jobs"]:
name = job["itemDetails"]["name"].rsplit("/", 1)[-1]
results = job["validationResult"]
result = results[0] if isinstance(results, list) else results
details = result["details"]
verdict = "compliant" if result["compliant"] else "NOT compliant"
print(f"{name}: {result['profileName']}: {verdict}, "
f"{details['passedRules']} rules passed, {details['failedRules']} failed")
rules = sorted(details["ruleSummaries"], key=lambda r: ([int(n) for n in r["clause"].split(".") if n.isdigit()],
r["testNumber"]))
for rule in rules:
checks = f"{rule['failedChecks']} check" + ("s" if rule["failedChecks"] != 1 else "")
print(f" {rule['clause']}-{rule['testNumber']} ({checks}): {rule['description']}")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2:]) from session import files, require, run
# The talk of the Beamer lesson, unchanged, against veraPDF's PDF/UA-1 profile: vera.py prints the verdict and each
# rule the PDF fails.
SIZES = [(r"\((\d+ pages?), \d+ bytes\)", r"(\1)")]
require("verapdf")
files("talk/talk-beamer.tex", "vera.py")
run("""
latexmk -pdf -interaction=nonstopmode -halt-on-error talk-beamer.tex > /dev/null 2>&1
pdfinfo talk-beamer.pdf | grep -E "^(Pages|Tagged)"
python3 vera.py ua1 talk-beamer.pdf
""", mask=SIZES) Output
$ latexmk -pdf -interaction=nonstopmode -halt-on-error talk-beamer.tex > /dev/null 2>&1 $ pdfinfo talk-beamer.pdf | grep -E "^(Pages|Tagged)" Tagged: no Pages: 9 $ python3 vera.py ua1 talk-beamer.pdf talk-beamer.pdf: PDF/UA-1 validation profile: NOT compliant, 99 rules passed, 7 failed 6.2-1 (1 check): The document catalog dictionary shall include a MarkInfo dictionary containing an entry, Marked, whose value shall be true 7.1-3 (140 checks): Content shall be marked as Artifact or tagged as real content 7.1-8 (1 check): The Catalog dictionary of a conforming file shall contain the Metadata key whose value is a metadata stream as defined in ISO 32000-1:2008, 14.3.2. The metadata stream dictionary shall contain entry Type with value /Metadata and entry Subtype with value /XML 7.1-10 (1 check): The document catalog dictionary shall include a ViewerPreferences dictionary containing a DisplayDocTitle key, whose value shall be true 7.1-11 (1 check): The logical structure of the conforming file shall be described by a structure hierarchy rooted in the StructTreeRoot entry of the document catalog dictionary, as described in ISO 32000-1:2008, 14.7 7.2-2 (2 checks): Natural language in the Outline entries shall be determined 7.2-34 (131 checks): Natural language for text in page content shall be determined
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_session.py
pdfinfo said Tagged: no, and 99 rules passed while 7 failed. The seven fall into three groups:
| What is missing | The failed rules | What a listener gets |
|---|---|---|
| A structure tree, and the mark that says the PDF has one | 7.1-11, 6.2-1 | no headings, lists or reading order |
| Tags on the content | 7.1-3, in 140 checks | text with no part in any structure |
| A language, a document title shown as title, XMP metadata | 7.2-34 in 131 checks, 7.2-2, 7.1-10, 7.1-8 | a guessed language, a file name for a title |
None of these can be fixed with a theme or an option. The LaTeX Project’s tagging status list gives Beamer “no-support”: its internals rebox material in a way no tagging can follow, and the list recommends the ltx-talk class for new tagged presentations.
A check, not a verdict on the slides
veraPDF tests what a machine can test: that tags exist, that the language and title are set, that images and formulas have text. Whether the tags make sense to a listener is a separate question, which the structure trees below answer.
Checkpoint
Check: What a PDF/UA check finds
ltx-talk keeps Beamer’s frames and tags the last slide of each
ltx-talk is a new class whose syntax is close to Beamer’s and whose manual makes tagged output its aim. It requires
\DocumentMetadata, works best with LuaLaTeX and does not support XeTeX. The port of the talk changed two places, as
diff prints them: the first two lines, and the title frame, which \maketitle makes:
talk/talk-ltxtalk.tex
\DocumentMetadata{lang = en, pdfstandard = ua-2, tagging = on}
\documentclass[frame-title-arg]{ltx-talk}
\title{How much rain one roof can catch}
\author{R. Menon}
\institute{Station A rain gauge team}
\date{}
\begin{document}
\maketitle
\section{The question}
\begin{frame}{Three questions}
\begin{itemize}[<+->]
\item How much rain falls on the roof in July?
\item How much of it reaches the pipe?
\item Where does the water go?
\end{itemize}
\end{frame}
\section{The answer}
\begin{frame}[label=formula]{The volume a roof catches}
\[ V = A \times R \times C \]
\pause
Roof area $A = 120~\mathrm{m^2}$, July rain $R = 0.655~\mathrm{m}$,
runoff share $C = 0.8$.
\pause
\[ V = \alert<3>{62.88~\mathrm{m^3}}, \text{ about 62,880 litres.} \]
\end{frame}
\begin{frame}{Where the water goes}
A 10,000-litre tank holds part of it.
\only<1| handout:0>{The tank fills in the first rainy week.}
\only<2>{The other 52,880 litres go to a recharge pit.}
\uncover<2>{Plan the pit before the tank.}
\end{frame}
\end{document} tree.py
"""Prints the structure tree that `pdfinfo -struct-text` reads from a tagged PDF, given on standard input, in short:
one line per tag, indented as in the tree, with the text the tag holds in quotes. The attributes poppler prints under
a tag and the numbers it gives each tag are left out, so what is left is what a screen reader follows: the tags, in
reading order, and their words. A tag that holds no text but has a title, as some heading tags do, shows the title in
square brackets; poppler prints such titles as UTF-16 bytes, which are decoded here.
pdfinfo -struct-text talk.pdf | python3 tree.py
"""
import re
import sys
UTF16_TITLE = re.compile(rb'"\xfe\xff((?:[\x00-\xff][\x00-\xff])*?)"')
tags = [] # [depth, tag, title, text]
for raw in sys.stdin.buffer.read().splitlines():
title_match = UTF16_TITLE.search(raw)
title = title_match.group(1).decode("utf-16-be", errors="replace").strip() if title_match else ""
line = UTF16_TITLE.sub(b"", raw).decode("utf-8", errors="replace")
body = line.lstrip(" ")
depth = (len(line) - len(body)) // 2
if not body or body.startswith(("/", "Syntax", "Object ")):
continue
if body.startswith('"'):
text = body.strip().strip('"').strip()
owner = next((t for t in reversed(tags) if t[0] < depth), None)
if text and owner:
owner[3] = (owner[3] + " " + text).strip()
continue
found = re.match(r"(\w+)", body)
if found:
tags.append([depth, found.group(1), title, ""])
if not tags:
print("no structure tree: the PDF is not tagged")
for depth, tag, title, text in tags:
line = " " * depth + tag
if text:
line += f' "{text}"'
elif title:
line += f" [{title}]"
print(line) from session import files, require, run
# The same talk in the ltx-talk class: what changed against the Beamer source, the build with LuaLaTeX, veraPDF's
# PDF/UA-2 profile, the structure tree (tree.py) and how often two of the talk's lines are in the page text and in the
# tags. LuaLaTeX's first run in a new home folder also builds its font name database, which takes a while.
require("verapdf")
files("talk/talk-beamer.tex", "talk/talk-ltxtalk.tex", "vera.py", "tree.py")
run("""
!diff talk-beamer.tex talk-ltxtalk.tex
latexmk -lualatex -interaction=nonstopmode -halt-on-error talk-ltxtalk.tex > /dev/null 2>&1
pdfinfo talk-ltxtalk.pdf | grep -E "^(Pages|Page size|Tagged|PDF version)"
python3 vera.py ua2 talk-ltxtalk.pdf
pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | python3 tree.py
pdftotext talk-ltxtalk.pdf - | grep -c "How much rain falls"
pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | grep -c "How much rain falls"
pdftotext talk-ltxtalk.pdf - | grep -c "The tank fills"
!pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | grep -c "The tank fills"
""") Output
$ diff talk-beamer.tex talk-ltxtalk.tex
1,2c1,2
< \documentclass[aspectratio=169]{beamer}
< \setbeamertemplate{navigation symbols}{}
---
> \DocumentMetadata{lang = en, pdfstandard = ua-2, tagging = on}
> \documentclass[frame-title-arg]{ltx-talk}
11,13c11
< \begin{frame}
< \titlepage
< \end{frame}
---
> \maketitle
[exit status 1]
$ latexmk -lualatex -interaction=nonstopmode -halt-on-error talk-ltxtalk.tex > /dev/null 2>&1
$ pdfinfo talk-ltxtalk.pdf | grep -E "^(Pages|Page size|Tagged|PDF version)"
Tagged: yes
Pages: 9
Page size: 503.937 x 283.465 pts
PDF version: 2.0
$ python3 vera.py ua2 talk-ltxtalk.pdf
talk-ltxtalk.pdf: PDF/UA-2 + Tagged PDF validation profile: compliant, 1727 rules passed, 0 failed
$ pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | python3 tree.py
Document
Sect
Part
P
P "How much rain one roof can catch"
P "R. Menon"
P "Station A rain gauge team"
Sect
H1 [The question]
Sect
H4 "Three questions"
Part
L
LI
Lbl "•"
LBody
Part
P "How much rain falls on the roof in July?"
LI
Lbl "•"
LBody
Part
P "How much of it reaches the pipe?"
LI
Lbl "•"
LBody
Part
P "Where does the water go?"
Sect
H1 [The answer]
Sect
H4 "The volume a roof catches"
Part
Formula "𝑉=𝐴×𝑅×𝐶"
P "Roof area , July rain , runoff share ."
Formula "𝐴=120m2"
Formula "𝑅=0.655m"
Formula "𝐶=0.8"
Formula "𝑉=62.88m3, about 62,880 litres."
Sect
H4 "Where the water goes"
Part
P "A 10,000-litre tank holds part of it."
Part
P "The other 52,880 litres go to a recharge pit."
Part
P "Plan the pit before the tank."
$ pdftotext talk-ltxtalk.pdf - | grep -c "How much rain falls"
3
$ pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | grep -c "How much rain falls"
1
$ pdftotext talk-ltxtalk.pdf - | grep -c "The tank fills"
1
$ pdfinfo -struct-text talk-ltxtalk.pdf 2>/dev/null | grep -c "The tank fills"
0
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ltxtalk_session.py
The PDF is version 2.0, tagged, 9 pages of 503.937 by 283.465 points: the manual’s slides are 100 mm high, here at 16:9. veraPDF’s PDF/UA-2 profile passed every one of its 1,727 rules. The tree is the useful part. Step through it as a screen reader would:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The title page. The tree starts with three paragraphs, the title, the author and the institute, as \maketitle set them.
- A section. \section{The question} becomes an H1 tag. The class prints no section page, so the tag carries the name only as its title.
- Frame titles are H4. Each frame title is an H4 tag, deliberately below sections and subsections, as the class manual says. Some checkers may warn about the jump from H1 to H4.
- Each item once. The frame made three slides, and pdftotext found the first question 3 times in the page text. The tags hold it once, because only the last slide of a frame is tagged; the others are artifacts.
- Formulas. The volume frame has a Formula tag for each display equation and one for each inline formula, inside the paragraph of numbers.
- The last frame. The last frame is tagged from its last slide, slide 2, so a listener hears the tank, the recharge pit and the plan for the pit.
- What is never heard. "The tank fills in the first rainy week." was only on slide 1, so the tags never hold it: grep found it once in the page text and 0 times in the tags.
The manual explains the counts: only the last slide of each frame is tagged, and the others are marked as artifacts
(its tag-slides option, set to n, the last slide). That is right for a list that builds up, whose last slide holds
every item, and wrong for the \only<1| handout:0> line, which a listener never hears.
A line that only an early slide shows
Wrong: putting a sentence that matters in \only<1>, so that it is on slide 1 alone, which ltx-talk tags as an
artifact. Right: keep it on the last slide of its frame, with \uncover for the lines that come later, or list
the slides to tag in the frame’s tag-slides option.
Checkpoint
Check: Tagged slides with ltx-talk
A Typst deck passes PDF/UA-1 once every equation has a description
Typst writes tagged PDF by default, and --pdf-standard ua-1 makes the export refuse what PDF/UA-1 forbids. Its
accessibility guide lists what that means: a document title, and an alternative description for every image and every
equation, written as you would read the formula aloud. The first export of the Touying talk stopped, and
talk-touying-ua.typ fixes it:
talk/talk-touying-ua.typ
#import "@preview/touying:0.8.0": *
#import themes.simple: *
#set document(title: [How much rain one roof can catch], author: "R. Menon")
#set text(lang: "en")
#show: simple-theme.with(
aspect-ratio: "16-9",
config-common(
new-section-slide-fn: none,
// In article mode, no title block from the article theme: the title slide's lines open the text.
article-theme: themes.article.article-theme.with(title-block-fn: (..args) => none),
),
// Header and footer text darker than the theme's gray, and alerts bold as well as coloured.
config-colors(neutral-light: rgb("#595959")),
config-methods(alert: (self: none, it) => text(fill: self.colors.primary, weight: "bold", it)),
)
#title-slide[
#text(1.4em, weight: "bold")[How much rain one roof can catch]
R. Menon, Station A rain gauge team
]
= The question
== Three questions
- How much rain falls on the roof in July?
#pause
- How much of it reaches the pipe?
#pause
- Where does the water go?
= The answer
== The volume a roof catches
#math.equation(block: true, alt: "V equals A times R times C", $V = A times R times C$)
#pause
Roof area _A_ = 120 m², July rain _R_ = 0.655 m, runoff share _C_ = 0.8.
#pause
#math.equation(
block: true,
alt: "V equals 62.88 cubic metres, about 62,880 litres",
$V = #alert[$62.88 "m"^3$], "about 62,880 litres." $,
)
== Where the water goes
A 10,000-litre tank holds part of it.
// Every line stays on the last subslide, which the handout and the text version keep.
The tank fills in the first rainy week.
#uncover(2)[The other 52,880 litres go to a recharge pit.]
#uncover(2)[Plan the pit before the tank.] talk/talk-touying.typ
#import "@preview/touying:0.8.0": *
#import themes.simple: *
#set document(title: [How much rain one roof can catch], author: "R. Menon")
#show: simple-theme.with(
aspect-ratio: "16-9",
config-common(new-section-slide-fn: none),
)
#title-slide[
#text(1.4em, weight: "bold")[How much rain one roof can catch]
R. Menon, Station A rain gauge team
]
= The question
== Three questions
- How much rain falls on the roof in July?
#pause
- How much of it reaches the pipe?
#pause
- Where does the water go?
= The answer
== The volume a roof catches
$ V = A times R times C $
#pause
Roof area $A = 120 "m"^2$, July rain $R = 0.655 "m"$, runoff share $C = 0.8$.
#pause
$ V = #alert[$62.88 "m"^3$], "about 62,880 litres." $
== Where the water goes
A 10,000-litre tank holds part of it.
#only(1)[The tank fills in the first rainy week.]
#only(2)[The other 52,880 litres go to a recharge pit.]
#uncover(2)[Plan the pit before the tank.] from session import files, packages, require, run
# The Touying talk of the Typst lesson exported as PDF/UA-1, first as it was, then talk-touying-ua.typ: alternative
# text on the two display equations, the inline maths written as text, the language, a darker header and bold alerts.
# Then veraPDF's PDF/UA-1 profile, the structure tree and how often a line of the talk is in the tags.
require("verapdf")
packages("touying:0.8.0", "uniwarn:0.1.1")
files("talk/talk-touying.typ", "talk/talk-touying-ua.typ", "vera.py", "tree.py")
run("""
!typst compile --pdf-standard ua-1 talk-touying.typ
typst compile --pdf-standard ua-1 talk-touying-ua.typ
python3 vera.py ua1 talk-touying-ua.pdf
pdfinfo -struct-text talk-touying-ua.pdf | python3 tree.py | sed -n 1,30p
pdfinfo -struct-text talk-touying-ua.pdf | grep -c "How much rain falls"
""") Output
$ typst compile --pdf-standard ua-1 talk-touying.typ
error: PDF/UA-1 error: missing alt text
┌─ @preview/touying:0.8.0/src/core/tree.typ:336:2
│
336 │ f(..leading, ..fields, ..extra)
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
│
= hint: make sure your images and equations have alt text
[exit status 1]
$ typst compile --pdf-standard ua-1 talk-touying-ua.typ
$ python3 vera.py ua1 talk-touying-ua.pdf
talk-touying-ua.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
$ pdfinfo -struct-text talk-touying-ua.pdf | python3 tree.py | sed -n 1,30p
Document
P "How much rain one roof can catch"
P "R. Menon, Station A rain gauge team"
H1
H2
Span "Three questions"
L
LI
Lbl "•"
LBody "How much rain falls on the roof in July?"
Span "Three questions"
L
LI
Lbl "•"
LBody "How much rain falls on the roof in July?"
LI
Lbl "•"
LBody "How much of it reaches the pipe?"
Span "Three questions"
L
LI
Lbl "•"
LBody "How much rain falls on the roof in July?"
LI
Lbl "•"
LBody "How much of it reaches the pipe?"
LI
Lbl "•"
LBody "Where does the water go?"
H1
$ pdfinfo -struct-text talk-touying-ua.pdf | grep -c "How much rain falls"
3
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typst_session.py
The error, “PDF/UA-1 error: missing alt text”, pointed at a line inside Touying, tree.typ:336, not at the equation:
Touying rebuilds slide content, so the place it names is in the package. The fix gives both display equations an alt
through math.equation and writes the inline quantities as text, “A = 120 m²”, which needs no description. With
that the export finished, and veraPDF passed all 106 rules of PDF/UA-1. #set text(lang: "en") names the language
as well: Typst assumes English when a document does not say, and its guide asks every document to state its language.
The tree shows what the check does not judge. Every subslide is tagged, so the first question is in the tags 3 times,
once per step, and the H1 and H2 heading tags hold no words: the slide titles are drawn in Span tags. Set beside
the ltx-talk tree, the two passing decks give a listener different things:
| In the structure tree | ltx-talk | Typst with Touying |
|---|---|---|
| Section headings | H1, the name as a title | H1, empty |
| Slide titles | H4, with their words | Span, below an empty H2 |
| A list built up in 3 steps | once | 3 times |
Key idea
A deck that passes PDF/UA can still repeat itself and hide its headings: read the structure tree, and share a version made for listeners as well as the slides.
Checkpoint
Check: Typst decks exported as PDF/UA-1
Contrast and colour decide who can read the slides
Under WCAG 2.2, text needs 4.5:1 or more against its background, large text (18 points, or 14 points bold) and marks
such as chart lines need 3:1, and colour may never be the only way information is shown. A
contrast ratio: WCAG's measure of how far apart two colours are in luminance, from 1:1 to 21:1, worked out as (L1 + 0.05) / (L2 + 0.05). Text needs at least 4.5:1, large text and graphics 3:1. compares how light two colours are: two identical colours give 1:1,
and black text on white gives the highest value, 21:1. contrast.py works it out with the formulas WCAG defines, for
colour pairs taken from the theme files:
"""Contrast ratios of slide-theme colours, worked out with the formulas of WCAG 2.2.
Each pair is a text colour on the background it sits on, as the theme files define them: Beamer's default colour
theme (beamercolorthemedefault.sty), the metropolis colour theme (beamercolorthememetropolis.sty) and Touying's
simple theme (themes/simple.typ). A colour a theme gives in fractions is rounded to whole channel values, half up:
Beamer's rgb 0.2, 0.2, 0.7 is #3333B3, and Touying's aqua.darken(50%), #7FDBFF with each channel halved, is #406E80.
The last two pairs are the fixes this lesson uses. Change a pair, or add your own, and run it again.
"""
def channel(value):
"""One sRGB channel, 0 to 255, as the linear value the luminance formula needs."""
c = value / 255
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
def luminance(hex_colour):
"""Relative luminance of a colour written as #RRGGBB: 0 for black, 1 for white."""
r, g, b = (int(hex_colour[i:i + 2], 16) for i in (1, 3, 5))
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
def contrast(foreground, background):
"""The contrast ratio of two colours, from 1 (the same) to 21 (black on white)."""
lighter, darker = sorted((luminance(foreground), luminance(background)), reverse=True)
return (lighter + 0.05) / (darker + 0.05)
PAIRS = [
("Beamer default", "body text", "#000000", "#FFFFFF"),
("Beamer default", "frame title", "#3333B3", "#FFFFFF"),
("Beamer default", "\\alert text", "#FF0000", "#FFFFFF"),
("metropolis", "body text", "#23373B", "#FAFAFA"),
("metropolis", "\\alert text", "#EB811B", "#FAFAFA"),
("metropolis", "example text", "#14B03D", "#FAFAFA"),
("Touying simple", "body text", "#000000", "#FFFFFF"),
("Touying simple", "#alert text", "#406E80", "#FFFFFF"),
("Touying simple", "header and footer", "#AAAAAA", "#FFFFFF"),
("Beamer, fixed", "\\alert, red!80!black", "#CC0000", "#FFFFFF"),
("Touying, fixed", "header and footer", "#595959", "#FFFFFF"),
]
print(f"{'theme':<16}{'text':<21}{'ratio':>7} 4.5:1 3:1")
for theme, text, foreground, background in PAIRS:
ratio = contrast(foreground, background)
small = "yes" if ratio >= 4.5 else "no"
large = "yes" if ratio >= 3 else "no"
print(f"{theme:<16}{text:<21}{ratio:>7.2f} {small:<5} {large}") Output
theme text ratio 4.5:1 3:1 Beamer default body text 21.00 yes yes Beamer default frame title 9.27 yes yes Beamer default \alert text 4.00 no yes metropolis body text 11.97 yes yes metropolis \alert text 2.62 no no metropolis example text 2.75 no no Touying simple body text 21.00 yes yes Touying simple #alert text 5.58 yes yes Touying simple header and footer 2.32 no no Beamer, fixed \alert, red!80!black 5.89 yes yes Touying, fixed header and footer 7.00 yes yes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 contrast.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Beamer’s default \alert red reached 4.00:1, enough only for large text. metropolis set its alerts at 2.62:1 and its
example text at 2.75:1, below even the 3:1 floor, and the grey header and footer of Touying’s simple theme, #aaaaaa,
reached 2.32:1. Slide text is read at many sizes, projected, on a phone and four to a printed page, so 4.5:1 is the
safe bar for all of it. The fixes are one line each: \setbeamercolor{alerted text}{fg=red!80!black}, the example
colour of Beamer’s own guide, gave 5.89:1, and config-colors(neutral-light: rgb("#595959")) in talk-touying-ua.typ
gave the header 7.00:1.
Test your own theme
Press Edit, add a line to PAIRS with your theme’s text and background colours, and run it again. A “no” in the
4.5:1 column means the colour must be darker, unless the text is large, when the 3:1 column decides.
Colour alone is the second trap. \alert and Touying’s #alert change only the colour, so a reader who cannot tell
red from black, or who prints in greyscale, loses the emphasis: talk-touying-ua.typ makes alerts bold as well. A
chart whose two lines differ only by red and green needs labels on the lines, or different marks and dashes.
Checkpoint
Check: Contrast ratios and colour alone
A poster is one large tagged page with its columns in reading order
A conference poster is a single page, usually A0, 841 by 1189 mm, with a title band and a few columns. In Typst it is
a page size and a grid; the chart keeps its alternative text and its caption, and the equation its description. In
LaTeX the usual poster tools are a problem for tagging: the beamerposter package builds on Beamer, and the tagging
status list gives it no support, while the tikzposter class is not on the list at all. poster.tex uses the article
class instead, with geometry for the A0 page, multicol for the columns and unicode-math for the formula, all of them
marked compatible in the list, and sets its sizes by redefining \normalsize, \large, \Large and \LARGE:
poster/poster.typ
// An A0 poster: one page, a title band and three columns, read left to right.
#set document(title: [How much rain one roof can catch], author: "R. Menon")
#set text(lang: "en", size: 32pt)
#set page(paper: "a0", margin: 3cm)
#set heading(numbering: none)
#show heading: set text(size: 54pt)
#show title: set text(size: 110pt)
#align(center)[
#title()
#text(48pt)[R. Menon, Station A rain gauge team]
]
#v(2cm)
#grid(
columns: (1fr, 1fr, 1fr),
column-gutter: 2.5cm,
[
= The question
How much rain falls on one roof in July, how much of it reaches the pipe, and where does the water go?
#figure(
image("rain.svg", width: 100%, alt: "Bar chart of monthly rainfall at Station A: under 65 mm a month from December to May, a peak of 655 mm in July."),
caption: [Monthly rainfall at Station A, an invented station.],
)
],
[
= The answer
#math.equation(block: true, alt: "V equals A times R times C", $V = A times R times C$)
A roof of 120 m², the 0.655 m of July rain and a runoff share of 0.8 give 62.88 m³: about 62,880 litres.
],
[
= Where the water goes
A 10,000-litre tank holds part of it. The other 52,880 litres go to a recharge pit, so plan the pit before the
tank.
],
) poster/poster.tex
\DocumentMetadata{lang = en, pdfstandard = ua-2, tagging = on}
\documentclass{article}
\usepackage[a0paper, margin = 3cm]{geometry}
\usepackage{multicol}
\usepackage{graphicx}
\usepackage{unicode-math}
% Poster sizes: body text 32 pt, headings 54 pt, the author line 48 pt and the title 110 pt.
\renewcommand{\normalsize}{\fontsize{32}{40}\selectfont}
\renewcommand{\Large}{\fontsize{54}{64}\selectfont}
\renewcommand{\large}{\fontsize{48}{58}\selectfont}
\renewcommand{\LARGE}{\fontsize{110}{124}\selectfont}
\setlength{\columnsep}{2.5cm}
\pagestyle{empty}
\title{How much rain one roof can catch}
\author{R. Menon, Station A rain gauge team}
\date{}
\begin{document}
\normalsize
\maketitle
\begin{multicols}{3}
\section*{The question}
How much rain falls on one roof in July, how much of it reaches the pipe, and where does the water go?
\begin{center}
\includegraphics[width = \linewidth, alt = {Bar chart of monthly rainfall at Station A: under 65 mm a month
from December to May, a peak of 655 mm in July.}]{rain}
Monthly rainfall at Station A, an invented station.
\end{center}
\section*{The answer}
\[ V = A \times R \times C \]
A roof of 120~m², the 0.655~m of July rain and a runoff share of 0.8 give 62.88~m³: about 62,880 litres.
\section*{Where the water goes}
A 10,000-litre tank holds part of it. The other 52,880 litres go to a recharge pit, so plan the pit before the tank.
\end{multicols}
\end{document} from session import files, require, run
# Two A0 posters of the talk: poster.typ in Typst, exported as PDF/UA-1, and poster.tex, the article class at A0 with
# multicol, tagged for PDF/UA-2 and built with LuaLaTeX. rsvg-convert turns the chart into a PDF for LaTeX.
SIZES = [(r"\((\d+ pages?), \d+ bytes\)", r"(\1)")]
require("verapdf", "rsvg-convert")
files("poster/poster.typ", "poster/poster.tex", "poster/rain.svg", "vera.py", "tree.py")
run("""
typst compile --pdf-standard ua-1 poster.typ poster-typst.pdf
pdfinfo poster-typst.pdf | grep -E "^(Pages|Page size)"
pdfinfo -struct-text poster-typst.pdf | python3 tree.py
python3 vera.py ua1 poster-typst.pdf
rsvg-convert -f pdf -o rain.pdf rain.svg
latexmk -lualatex -interaction=nonstopmode -halt-on-error poster.tex > /dev/null 2>&1
pdfinfo poster.pdf | grep -E "^(Pages|Page size)"
python3 vera.py ua2 poster.pdf
""", mask=SIZES) Output
$ typst compile --pdf-standard ua-1 poster.typ poster-typst.pdf
$ pdfinfo poster-typst.pdf | grep -E "^(Pages|Page size)"
Pages: 1
Page size: 2383.94 x 3370.39 pts (A0)
$ pdfinfo -struct-text poster-typst.pdf | python3 tree.py
Document
P "How much rain one roof can catch"
P "R. Menon, Station A rain gauge team"
Div
Div
H1 "The question"
P "How much rain falls on one roof in July, how much of it reaches the pipe, and where does the water go?"
Div
Caption
Span "Figure 1: Monthly rainfall at Station A, an invented station."
Figure "Monthly rainfall at Station A (mm)0200400600JanFebMarAprMayJunJulAugSepOctNovDec655"
Div
H1 "The answer"
Formula "𝑉 = 𝐴 × 𝑅 × 𝐶"
P "A roof of 120 m², the 0.655 m of July rain and a runoff share of 0.8 give 62.88 m³: about 62,880 litres."
Div
H1 "Where the water goes"
P "A 10,000-litre tank holds part of it. The other 52,880 litres go to a recharge pit, so plan the pit before the tank."
$ python3 vera.py ua1 poster-typst.pdf
poster-typst.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
$ rsvg-convert -f pdf -o rain.pdf rain.svg
$ latexmk -lualatex -interaction=nonstopmode -halt-on-error poster.tex > /dev/null 2>&1
$ pdfinfo poster.pdf | grep -E "^(Pages|Page size)"
Pages: 1
Page size: 2383.94 x 3370.39 pts (A0)
$ python3 vera.py ua2 poster.pdf
poster.pdf: PDF/UA-2 + Tagged PDF validation profile: compliant, 1727 rules passed, 0 failed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 poster_session.py
Both pages are 2383.94 by 3370.39 points, A0. The Typst poster passed PDF/UA-1 with its 106 rules and the LaTeX poster
PDF/UA-2 with its 1,727. The tree of the Typst poster shows why a grid suits a poster: each column is a Div in the
order of the grid’s cells, with its heading, paragraphs, figure and formula inside, so a listener reads the first
column to the end before the second. Write the columns in the order they should be read, and keep the body text large:
32 points here, with headings at 54 and the title at 110. The two posters and their checks:
| Poster | Engine | Check | Result |
|---|---|---|---|
| poster.typ | Typst 0.15.1 | PDF/UA-1 | compliant, 106 rules |
| poster.tex | LuaLaTeX | PDF/UA-2 | compliant, 1,727 rules |
Checkpoint
Check: Posters on one tagged page
Share the version that fits each reader
After the talk, people read it again in different ways: some page through it as it was shown, some print it, some
listen to it with a screen reader, some search it. One Typst source gives three PDFs for them, each exported as
PDF/UA-1: the slides, the handout with --input export-mode=handout, and a text version with
--input export-mode=article, which Touying’s changelog describes as the same source compiled to a flowing prose
document:
from session import files, packages, require, run
# Three PDFs from talk-touying-ua.typ for three readers: the slides, the handout (export-mode=handout) and the text
# version (export-mode=article, one flowing page). For each: pages, and how often one line of the talk is in the page
# text and in the tags. Then the first tags of the handout, the text version's structure tree and its PDF/UA-1 check.
require("verapdf")
packages("touying:0.8.0", "uniwarn:0.1.1")
files("talk/talk-touying-ua.typ", "vera.py", "tree.py")
run("""
typst compile --pdf-standard ua-1 talk-touying-ua.typ slides.pdf
typst compile --pdf-standard ua-1 --input export-mode=handout talk-touying-ua.typ handout.pdf
typst compile --pdf-standard ua-1 --input export-mode=article talk-touying-ua.typ text.pdf
pdfinfo slides.pdf | grep -E "^Pages"
pdftotext slides.pdf - | grep -c "How much rain falls"
pdfinfo handout.pdf | grep -E "^Pages"
pdftotext handout.pdf - | grep -c "How much rain falls"
pdfinfo -struct-text handout.pdf | python3 tree.py | sed -n 1,6p
pdfinfo text.pdf | grep -E "^(Pages|Page size)"
pdftotext text.pdf - | grep -c "How much rain falls"
pdftotext text.pdf - | grep -c "The tank fills"
pdfinfo -struct-text text.pdf | python3 tree.py
python3 vera.py ua1 text.pdf
""") Output
$ typst compile --pdf-standard ua-1 talk-touying-ua.typ slides.pdf
$ typst compile --pdf-standard ua-1 --input export-mode=handout talk-touying-ua.typ handout.pdf
$ typst compile --pdf-standard ua-1 --input export-mode=article talk-touying-ua.typ text.pdf
$ pdfinfo slides.pdf | grep -E "^Pages"
Pages: 9
$ pdftotext slides.pdf - | grep -c "How much rain falls"
3
$ pdfinfo handout.pdf | grep -E "^Pages"
Pages: 4
$ pdftotext handout.pdf - | grep -c "How much rain falls"
1
$ pdfinfo -struct-text handout.pdf | python3 tree.py | sed -n 1,6p
Document
P "How much rain one roof can catch"
P "R. Menon, Station A rain gauge team"
H1
H2
Span "Three questions"
$ pdfinfo text.pdf | grep -E "^(Pages|Page size)"
Pages: 1
Page size: 595.276 x 841.89 pts (A4)
$ pdftotext text.pdf - | grep -c "How much rain falls"
1
$ pdftotext text.pdf - | grep -c "The tank fills"
1
$ pdfinfo -struct-text text.pdf | python3 tree.py
Document
P "How much rain one roof can catch"
P "R. Menon, Station A rain gauge team"
H1 "The question"
H2 "Three questions"
L
LI
Lbl "•"
LBody "How much rain falls on the roof in July?"
LI
Lbl "•"
LBody "How much of it reaches the pipe?"
LI
Lbl "•"
LBody "Where does the water go?"
H1 "The answer"
H2 "The volume a roof catches"
Formula "𝑉 = 𝐴 × 𝑅 × 𝐶"
P "Roof area = 120 m², July rain = 0.655 m, runoff share = 0.8."
Span "A"
Span "R"
Span "C"
Formula "𝑉 = 6 2 . 8 8 m 3 , about 62,880 litres."
H2 "Where the water goes"
P "A 10,000-litre tank holds part of it."
P "The tank fills in the first rainy week."
P "The other 52,880 litres go to a recharge pit."
P "Plan the pit before the tank."
$ python3 vera.py ua1 text.pdf
text.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 share_session.py
The slides held the first question 3 times in 9 pages, the handout once in 4 pages, and the
text version: A talk written out as a flowing document, with real headings and each line once, such as Touying's article mode gives, for readers who listen with a screen reader or search. once on a single A4 page, with every line of the last slide, the tank
included, because talk-touying-ua.typ keeps each line on the last step. The handout’s tree starts as the slides’
does, with an empty H1 and H2 before the first slide title. The text version’s tree is the one a listener needs: H1
sections and H2 slide titles, each with its words, and every line once. It passed PDF/UA-1 as well. For a LaTeX talk
the same choices exist: Beamer’s guide describes an article version of a talk next to the handout, and ltx-talk has a
handout mode. Answer two questions about your readers:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Version | Each line once | Looks like the talk | Headings with words | Pages |
|---|---|---|---|---|
| Slides Made with: typst compile | Poor the first question 3 times, once per step | Good every step, as the audience saw it | Poor empty H1 and H2 tags; slide titles are Span tags | Mixed 9 pages of 297 by 167 mm |
| Handout Made with: --input export-mode=handout | Good each line once, from the last step of each slide | Good one page per slide, as the slide ended | Poor the same empty heading tags as the slides | Good 4 pages |
| Text version Made with: --input export-mode=article | Good each line once | Poor one flowing A4 page, without the slide design | Good H1 sections and H2 slide titles, with their words | Good 1 A4 page |
How the answers decide
- “Will some readers use a screen reader, or jump through the document by its headings?” Yes, or I cannot rule it out: rules out Slides and adds a caveat to Handout. A listener hears each step of the slides again and finds empty headings; the handout repeats nothing but has the same empty headings, and the text version has both right.
- “Do readers need the slides as they looked, with their layout and pictures?” Yes, the look matters: adds a caveat to Text version. The text version flows the words onto one A4 page without the slide design, so share it beside the handout rather than instead of it.
In short: the handout for readers who print or page through, the text version for listeners and for search, and the slides beside them for anyone who wants every step.
Checkpoint
Check: Which version to share
Practise: fix a colour, read a veraPDF report
Two checks for a deck before you send it: the first darkens a theme colour step by step until it reaches a contrast
ratio, the second reads veraPDF’s JSON report and lists the rules a PDF failed, as vera.py printed them. The sample
tests of both steps run in the browser, and a first hint goes with each:
Code lab · 2 steps
Check a deck before you send it
Find the lightest darker shade of a colour that reaches a contrast ratio, then list the failed rules of a veraPDF report in the order of the standard.
Exercise · Medium · Python
Darken a theme colour until it is readable
A theme colour that fails a contrast check is usually fixed by making it darker, as little as possible, so that it still looks like the theme. Write darken_to(colour, background, target=4.5) in darken.py. Both colours are written as #RRGGBB (hexadecimal, any letter case). Return the first colour, written as #RRGGBB in upper case, that has a contrast ratio of at least target with background, trying the colour itself first and then darker steps.
- Work out the contrast ratio with WCAG's formulas, as
contrast.pyin the lesson does: each channel divided by 255, thenc / 12.92when it is at most 0.04045, else((c + 0.055) / 1.055) ** 2.4; the luminance is0.2126 R + 0.7152 G + 0.0722 B; the ratio is(lighter + 0.05) / (darker + 0.05). - One darker step takes 1 away from each of the three channels that is still above 0.
- If even black does not reach the target, return
None.
Starter code · darken.py
def darken_to(colour, background, target=4.5):
"""Return the first colour, from colour itself down in steps of 1 per channel, that reaches the target ratio."""
# Replace this line: work out the contrast ratio, then darken one step at a time.
return colour The sample tests · test_darken.py
from darken import darken_to
def test_a_colour_that_already_passes_is_kept():
"""Beamer's frame title blue, 9.27:1 on white"""
assert darken_to("#3333B3", "#FFFFFF") == "#3333B3"
assert darken_to("#3333b3", "#ffffff") == "#3333B3"
def test_touyings_grey_header():
"""#AAAAAA on white is 2.32:1; #767676 is the first grey from there that reaches 4.5:1"""
assert darken_to("#AAAAAA", "#FFFFFF") == "#767676"
def test_the_target_can_change():
"""3:1 is enough for large text and for chart lines"""
assert darken_to("#AAAAAA", "#FFFFFF", target=3) == "#949494"
def test_beamers_alert_red():
"""#FF0000 on white is 4.00:1"""
assert darken_to("#FF0000", "#FFFFFF") == "#EE0000"
def test_metropolis_alert_on_its_background():
assert darken_to("#EB811B", "#FAFAFA") == "#BE5400"
def test_black_is_the_last_step():
"""black on white is exactly 21:1, the highest ratio there is"""
assert darken_to("#FF0000", "#FFFFFF", target=21) == "#000000"
assert darken_to("#0000FF", "#000080") is None A hint
Write a contrast(a, b) helper first and test it on the lesson's numbers: #FF0000 on #FFFFFF is 4.00. Then keep the three channels as integers in a loop: test the current colour, and if it fails, take 1 from every channel above 0 and try again, until all three are 0.
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 · Medium · Python
List the rules a veraPDF report failed
verapdf --format json --flavour ua1 talk.pdf writes a JSON report; json.loads turns it into dictionaries and lists. Write failed_rules(report) in report.py. report is that parsed report. Return the failed rules of its first job as strings "<clause>-<test number>", such as "7.1-3", sorted in the order of the standard: by the numbers of the clause, then by the test number. Return an empty list for a compliant PDF.
- The job is
report["report"]["jobs"][0]. Its"validationResult"is a dictionary, or in some veraPDF versions a list whose first item is that dictionary. - The result's
"details"holds"ruleSummaries", one dictionary per failed rule, with"clause"(a string such as"7.1"or"6.2") and"testNumber"(a number). - Sort clauses as numbers, part by part:
"7.2"comes before"7.10", and"6.2"before both.
Starter code · report.py
def failed_rules(report):
"""Return the failed rules of a parsed veraPDF JSON report as "clause-test" strings, in the standard's order."""
# Replace this line: find the first job's result, then sort its rule summaries.
return [] The sample tests · test_report.py
from report import failed_rules
def rule(clause, test, checks=1):
return {"ruleStatus": "FAILED", "clause": clause, "testNumber": test, "failedChecks": checks, "description": "…"}
def report(rules, as_list=False, compliant=False):
result = {
"profileName": "PDF/UA-1 validation profile",
"compliant": compliant,
"details": {"passedRules": 106 - len(rules), "failedRules": len(rules), "ruleSummaries": rules},
}
job = {"itemDetails": {"name": "talk-beamer.pdf"}, "validationResult": [result] if as_list else result}
return {"report": {"jobs": [job]}}
# The seven rules the Beamer talk failed, in the order veraPDF happened to list them.
BEAMER = [rule("7.1", 3, 140), rule("6.2", 1), rule("7.2", 34, 131), rule("7.1", 11), rule("7.1", 8),
rule("7.2", 2, 2), rule("7.1", 10)]
def test_the_beamer_talk():
assert failed_rules(report(BEAMER)) == ["6.2-1", "7.1-3", "7.1-8", "7.1-10", "7.1-11", "7.2-2", "7.2-34"]
def test_a_list_of_results():
assert failed_rules(report(BEAMER, as_list=True))[:2] == ["6.2-1", "7.1-3"]
def test_clauses_sort_as_numbers():
assert failed_rules(report([rule("7.10", 1), rule("7.2", 5), rule("7.9", 2)])) == ["7.2-5", "7.9-2", "7.10-1"]
def test_a_compliant_pdf():
assert failed_rules(report([], compliant=True)) == [] A hint
Get the result first, taking the first item when it is a list. Then sort the rule summaries with a key that turns the clause into a list of numbers and adds the test number: [int(n) for n in rule["clause"].split(".")], then rule["testNumber"].
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.
The practice questions take the six ideas in turn and come back to any you miss:
Practice that adapts to you
Practise this lesson
18 questions about 6 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): what does a PDF/UA check tell you about a deck, and what does it not tell you?
It tells you whether the PDF has the structure assistive software needs: a structure tree, content that is either tagged or marked as an artifact, a language, a title, descriptions for images and formulas. veraPDF tests those rules one by one; the Beamer talk failed 7 of the 106 in PDF/UA-1 because it had no tags at all. It does not tell you whether the tags make sense. The Typst talk passed all 106 rules while its heading tags were empty and every step of a list was tagged again, so a listener would hear the first question three times. Passing means the machine-checkable rules hold; reading the structure tree, and listening with a screen reader, tell you whether the deck works.
Two harder questions follow, about a conference that asks for tagged slides and a poster that is hard to read. 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 conference asks every speaker for slides as a PDF/UA file, checked with veraPDF. Your 30-frame talk is in Beamer and fails 7 rules, among them the one that asks for a structure tree. You have a week. What do you do, and how do you show the result works for a listener as well as for the validator?
-
Fresher to mid
Your A0 poster's chart shows two series as a red and a green line, with a legend in light grey on white and the abstract at 24 points. A colleague with a colour-vision deficiency cannot tell the series apart, and others find the legend hard to read. What do you change, and how do you check each change?
Key takeaways
- veraPDF found no structure tree in the Beamer talk: 7 of the 106 PDF/UA-1 rules failed, and no option fixes it, because Beamer has no tagging support.
- ltx-talk took the same frames with two changes and passed all 1,727 PDF/UA-2 rules; it tags the last slide of each frame, so a line on an early slide alone is never heard.
- Typst exported the talk as PDF/UA-1 once both equations had descriptions; it passed 106 rules, but its tags repeat every step and leave the headings empty.
- Theme colours fell as low as 2.32:1 against a bar of 4.5:1; darker colours and bold alerts fixed them, and colour must never be the only signal.
- A poster is one tagged page whose columns are read in grid order; Typst and an article-class LaTeX page both passed, while beamerposter has no tagging support and tikzposter is not on the status list.
- Share the handout and a text version beside the slides: the text version said each line once, with real headings.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What did veraPDF find in the Beamer talk?
No structure tree: it failed 7 of the 106 PDF/UA-1 rules, and no theme or option can fix it, because Beamer has no tagging support.
-
What is ltx-talk, and what does it need?
A class with Beamer-like frames made for tagged output. It requires \DocumentMetadata, works best with LuaLaTeX and does not support XeTeX; the ported talk passed all 1,727 PDF/UA-2 rules.
-
Which slide of a frame does ltx-talk tag?
Only the last slide, by default; the others are artifacts. A line shown only on an early slide, such as with \only<1>, is never heard.
-
Why did the Touying talk stop under --pdf-standard ua-1?
Its display equations had no alt text. The error pointed inside Touying because the package rebuilds slide content; math.equation(alt: ...) on both equations fixed it.
-
What contrast does WCAG 2.2 ask of slide text?
At least 4.5:1 against its background; 3:1 for large text, meaning at least 18 point or 14 point bold, and for marks such as chart lines. Black text on white gives 21:1.
-
Why make alerts bold as well as coloured?
Colour must never be the only signal: a reader who cannot tell red from black, or a greyscale print, loses an emphasis that only its colour shows.
-
How do you build an accessible A0 poster in LaTeX?
Use the article class with geometry for the page, multicol and unicode-math, all marked compatible with tagging; beamerposter has no tagging support and tikzposter is not on the status list.
-
Which version of a Typst talk suits a listener best?
The text version, --input export-mode=article: one page that says each line once, with real H1 and H2 headings, beside the handout for readers who print.
Terms in this lesson
- contrast ratio
- WCAG's measure of how far apart two colours are in luminance, from 1:1 to 21:1, worked out as (L1 + 0.05) / (L2 + 0.05). Text needs at least 4.5:1, large text and graphics 3:1.
- text version
- A talk written out as a flowing document, with real headings and each line once, such as Touying's article mode gives, for readers who listen with a screen reader or search.
References
- ltx-talk: a class for typesetting (accessible) presentations (CTAN)
- The ltx-talk manual (CTAN (Joseph Wright))
- Tagging status of LaTeX packages and classes (The LaTeX Project (Tagged PDF project))
- Typst guide: Accessibility (Typst)
- Typst reference: equation (Typst)
- Web Content Accessibility Guidelines (WCAG) 2.2 (W3C)
- veraPDF: the PDF/A and PDF/UA validator (veraPDF Consortium)
- beamerposter: extend beamer and a0poster for custom sized posters (CTAN)
- tikzposter: create scientific posters using TikZ (CTAN)
- Touying changelog: article mode and export modes (The Touying project)
- The beamer class: user guide (CTAN (the beamer maintainers))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress