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 11 – Accessible, checked and safe PDFs

Accessible PDFs from Typst

Add the title, language and alt text Typst cannot know, export with PDF/UA-1, build real tables, add PDF/A for archives and export HTML that reflows.

  • 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, qpdf 12.4.1 and verapdf 1.30
  • 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

  • Use Typst's default tagging and add the title, language and alternative text it cannot know
  • Export with --pdf-standard ua-1 and fix each error it reports
  • Build data tables with table.header instead of grids
  • Combine PDF/UA-1 with PDF/A-2a or PDF/A-3a for archiving
  • Export an HTML version for readers who need the text to reflow

Before you start

What you’ll learn · Before you start

What you will learn

  • Use Typst's default tagging and add the title, language and alternative text it cannot know
  • Export with --pdf-standard ua-1 and fix each error it reports
  • Build data tables with table.header instead of grids
  • Combine PDF/UA-1 with PDF/A-2a or PDF/A-3a for archiving
  • Export an HTML version for readers who need the text to reflow

Before you start

On this page

Typst writes a tagged PDF without being asked, yet the default export of this lesson’s report still failed 2 of the 106 rules of veraPDF’s PDF/UA-1 profile. A PDF/UA-1 export of a copy that set its table as a grid passed all 106, though a screen reader would meet its fifteen cells as one flat run of text.

You will see what Typst tags by itself and add what only you can give, export with --pdf-standard ua-1 and fix each error it reports, build tables a screen reader can move through, add PDF/A for archiving, and export HTML for readers who need the text to reflow. Every export was recorded with Typst 0.15.1 and validated with veraPDF 1.30.

Typst tags every PDF; you add the title, the language and the descriptions

Since version 0.14, the changelog says, Typst PDFs are tagged by default: every heading, paragraph, list, figure and table that Typst recognises becomes part of the file’s 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”. The report below gives the three things only its author can know: the document’s title and its language at the top, and the alternative text of its picture and its formula where they appear:

report.typ: a figure, a table, a formula and a link TYP · report/report.typ
// report.typ: a one-page report with a figure, a table, a formula and a link, ready for PDF/UA-1.
#set document(title: [Monsoon rainfall at Station A], author: "Asha Verghese")
#set text(lang: "en", region: "IN")
#set heading(numbering: "1.")

#title()

= Summary
Most of the rain at Station A, an invented station, falls from June to September.

#figure(
  image("chart.png", width: 60%, alt: "Bar chart of monthly rainfall: under 160 mm a month from January to May, 418 mm in June, a peak of 655 mm in July, then falling to 306 mm in September."),
  caption: [Monthly rainfall at Station A.],
)

= The wettest months
#figure(
  table(
    columns: 3,
    align: (left, right, right),
    table.header[Month][Rain (mm)][Rainy days],
    [June], [418], [21],
    [July], [655], [27],
    [August], [541], [25],
    [September], [306], [17],
  ),
  caption: [The four monsoon months.],
)

The total for the four months is
#math.equation(alt: "418 plus 655 plus 541 plus 306 equals 1920", $418 + 655 + 541 + 306 = 1920$)~mm.
The data are described at #link("https://example.com/station-a").
The default export, its structure tree and its language Python · defaults_session.py
from session import files, read, run, write

# report.typ exported as Typst does by default: what pdfinfo reads back, veraPDF's verdict against PDF/UA-1, the
# structure tree shortened by tags.py and the language in the catalog; then a copy without the text language.
# (2>/dev/null hides a warning poppler prints about an attribute of Typst's table tags.)
files("report/report.typ", "report/make_chart.py", "vera.py", "tags.py")
write("no-lang.typ", read("report.typ").replace('#set text(lang: "en", region: "IN")\n', ""))
run(r"""
python3 make_chart.py
typst compile report.typ default.pdf
pdfinfo default.pdf | grep -E "^(Title|Tagged|PDF version)"
python3 vera.py ua1 default.pdf
pdfinfo -struct-text default.pdf 2>/dev/null | python3 tags.py
qpdf --qdf --object-streams=disable default.pdf default-qdf.pdf
grep -a "/Lang" default-qdf.pdf
typst compile no-lang.typ no-lang.pdf
qpdf --qdf --object-streams=disable no-lang.pdf no-lang-qdf.pdf
grep -a "/Lang" no-lang-qdf.pdf
""")

Output

$ python3 make_chart.py
$ typst compile report.typ default.pdf
$ pdfinfo default.pdf | grep -E "^(Title|Tagged|PDF version)"
Title:           Monsoon rainfall at Station A
Tagged:          yes
PDF version:     1.7
$ python3 vera.py ua1 default.pdf
default.pdf: PDF/UA-1 validation profile: NOT compliant, 104 rules passed, 2 failed
  5-1 (1 check): The document metadata stream doesn't contain PDF/UA Identification Schema
  7.1-10 (1 check): ViewerPreferences dictionary is not present in the document Catalog, or DisplayDocTitle key is set to false or is not present in the ViewerPreferences dictionary (ViewerPreferences = dictionary(size = 1), DisplayDocTitle = null)
$ pdfinfo -struct-text default.pdf 2>/dev/null | python3 tags.py
Document
  P "Monsoon rainfall at Station A"
  H1 "1. Summary"
  P "Most of the rain at Station A, an invented station, falls from June to September."
  Div
    Caption
      Span "Figure 1: Monthly rainfall at Station A."
    Figure [Bar chart of monthly rainfall: under 160 mm a month from January to May, 418 mm in June, a peak of 655 mm in July, then falling to 306 mm in September.]
  H1 "2. The wettest months"
  Table
    Caption
      Span "Table 1: The four monsoon months."
    THead
      TR
        TH "Month"
        TH "Rain (mm)"
        TH "Rainy days"
    TBody
      TR
        TD "June"
        TD "418"
        TD "21"
      TR
        TD "July"
        TD "655"
        TD "27"
      TR
        TD "August"
        TD "541"
        TD "25"
      TR
        TD "September"
        TD "306"
        TD "17"
  P "The total for the four months is mm. The data are described at ."
    Formula "4 1 8 + 6 5 5 + 5 4 1 + 3 0 6 = 1 9 2 0"
    Link "https://example.com/station-a"
$ qpdf --qdf --object-streams=disable default.pdf default-qdf.pdf
$ grep -a "/Lang" default-qdf.pdf
  /Lang (en-IN)
$ typst compile no-lang.typ no-lang.pdf
$ qpdf --qdf --object-streams=disable no-lang.pdf no-lang-qdf.pdf
$ grep -a "/Lang" no-lang-qdf.pdf
  /Lang (en)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 defaults_session.py

pdfinfo found 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” of PDF version 1.7, Typst’s default, with the title from set document. The tree holds the headings as H1, the image’s description in square brackets, a Table with a THead of TH cells, the Formula and the Link. The title that #title() prints is a P, not a heading, which is what Typst’s guide wants: it warns against making the document’s title a heading. set text(lang: "en", region: "IN") wrote /Lang (en-IN); without it the copy said /Lang (en), because, the guide says, Typst assumes English.

veraPDF still failed two rules of 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”-1: 5-1, because the metadata does not say that the file claims PDF/UA, and 7.1-10, because the file does not ask viewers to show its title instead of its file name. Both are settings of the export, which the next section switches on.

Typst does it by itself You give it in the source
tags headings, paragraphs, lists, figures, tables and links set document(title: …) before any content
marks headers, footers, page backgrounds and automatic hyphens as artifacts set text(lang: …, region: …) at the start
marks shapes as artifacts, keeping any text inside them alt: on every image and every equation
writes PDF 1.7, with /Lang (en) when no language is set pdf.artifact around any other decoration

The left column comes from the guide’s list of what Typst handles automatically and from what the session read back; the right column is what the guide asks of the author. The guide adds one more duty that no export can check: semantic elements such as emph, strong, lists and quote instead of text styled to look like them.

Checkpoint

Check: What Typst tags and what you add

  1. Question 1 of 2 Which two PDF/UA-1 rules did the default export of report.typ fail?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 5-1, no PDF/UA identification in the metadata, and 7.1-10, the title is not shown instead of the file name

    The default export is tagged but claims no standard. Exporting with ua-1 adds the identification and the display-title setting, and the same report then passed all 106 rules.

  2. Question 2 of 2 How does report.typ give its title to the file and to the page?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: set document(title: …) for the metadata, and #title() to print it once, which is tagged as a P

    The metadata title goes into the viewer's title bar and catalogues; the printed title is for readers. Headings then start at level 1 for the first section.

PDF/UA-1 export turns each gap into an error

typst compile --pdf-standard ua-1 adds the PDF/UA identification and the display-title setting, and it refuses to write a file that breaks the rules it can check. The PDF reference names its extra checks, such as alternative text for images; the accessibility guide adds equations. The session exports the report, then three drafts that each lack one thing, and the report with tags switched off:

PDF/UA-1 export: the report, three drafts and one refused setting Python · ua1_session.py
from session import files, read, run, write

# report.typ exported with --pdf-standard ua-1 and checked by veraPDF; then what the export refuses: a draft without
# the document's title, one with an image without alternative text, one with an inline formula without it, and the
# report with PDF tags switched off. Last, the data a script can read before any export: the alternative text of every
# image and every formula, as typst eval lists them.
files("report/report.typ", "report/make_chart.py", "vera.py")
text = read("report.typ")
ALT = ', alt: "Bar chart of monthly rainfall: under 160 mm a month from January to May, 418 mm in June, a peak of 655 mm in July, then falling to 306 mm in September."'
FORMULA = '#math.equation(alt: "418 plus 655 plus 541 plus 306 equals 1920", $418 + 655 + 541 + 306 = 1920$)'
assert ALT in text and FORMULA in text
write("no-title.typ", text.replace("title: [Monsoon rainfall at Station A], ", "").replace("#title()\n", ""))
write("no-alt.typ", text.replace(ALT, ""))
write("formula.typ", text.replace(FORMULA, "$418 + 655 + 541 + 306 = 1920$"))
run(r"""
python3 make_chart.py
typst compile --pdf-standard ua-1 report.typ ua1.pdf
python3 vera.py ua1 ua1.pdf
!typst compile --pdf-standard ua-1 no-title.typ
!typst compile --pdf-standard ua-1 no-alt.typ
!typst compile --pdf-standard ua-1 formula.typ
!typst compile --no-pdf-tags --pdf-standard ua-1 report.typ
typst eval --in no-alt.typ 'query(image).map(it => (it.source, it.alt))'
typst eval --in formula.typ 'query(math.equation).map(it => (it.block, it.alt))'
""")

Output

$ python3 make_chart.py
$ typst compile --pdf-standard ua-1 report.typ ua1.pdf
$ python3 vera.py ua1 ua1.pdf
ua1.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
$ typst compile --pdf-standard ua-1 no-title.typ
error: PDF/UA-1 error: missing document title
 = hint: set the title with `set document(title: [...])`

[exit status 1]
$ typst compile --pdf-standard ua-1 no-alt.typ
error: PDF/UA-1 error: missing alt text
   ┌─ no-alt.typ:12:2
   │
12 │   image("chart.png", width: 60%),
   │   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   │
   = hint: make sure your images and equations have alt text

[exit status 1]
$ typst compile --pdf-standard ua-1 formula.typ
error: PDF/UA-1 error: missing alt text
   ┌─ formula.typ:31:0
   │
31 │ $418 + 655 + 541 + 306 = 1920$~mm.
   │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   │
   = hint: make sure your images and equations have alt text

[exit status 1]
$ typst compile --no-pdf-tags --pdf-standard ua-1 report.typ
error: cannot disable PDF tags when exporting a PDF/UA-1 document
[exit status 1]
$ typst eval --in no-alt.typ 'query(image).map(it => (it.source, it.alt))'
[["chart.png",null]]
$ typst eval --in formula.typ 'query(math.equation).map(it => (it.block, it.alt))'
[[false,null]]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ua1_session.py

The report passed all 106 rules. Each draft stopped with an error that names the file and the line: “missing document title”, with the hint to set it with set document(title: [...]); “missing alt text” for the image, with a caret under the call; and the same message for an inline formula, which needs math.equation(alt: …) too. With --no-pdf-tags, the export refused outright: PDF/UA-1 cannot be met without tags. The resume lesson met the two heading errors as well: the first heading must be of level 1, and levels may not be skipped.

The last two commands read the same gaps without exporting: typst eval with query(image) lists every image with its 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. More in “Maths fonts, symbol lookup and accessible equations”, null where there is none, and the same for equations. A build script can run them over every chapter before a long export. Step through the loop that the errors set up:

Interactive · stage by stage Export, read the error, fix the source Step through one draft of report.typ from its first PDF/UA-1 export to the file veraPDF passes.
Export, read the error, fix the source, step 7 of 7. Showing Write report.typ, Export with ua-1, Read the error, Fix the source, veraPDF against UA-1, Read the structure tree. In focus: Read the structure tree.export againWrite report.typtitle, lang, altExport with ua-1--pdf-standard ua-1Read the errornames the file and lineFix the sourceone gap at a timeveraPDF against UA-1106 rulesRead the structure treewhat no rule can see

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

Every step as text
  1. The draft. The draft has a title, a language and a table with table.header, but its chart has no alt text. Typst would export it as an ordinary tagged PDF without a word.
  2. The export stops. With --pdf-standard ua-1 the same draft stops before any file is written. Nothing reaches a reader in a state the export can tell is broken.
  3. The error names the gap. error: PDF/UA-1 error: missing alt text, with a caret under image("chart.png", width: 60%) on line 12 and the hint to give images and equations alt text.
  4. One fix at a time. The fix goes into the source, not the PDF. A formula without alt text gives the same message for its line, and a missing title asks for set document(title).
  5. Export again. The loop repeats until the export writes a file. typst eval with query(image) lists the images still without alt text, so a script can find them all first.
  6. The validator. veraPDF passed the exported report on all 106 rules of its PDF/UA-1 profile. It checks what a machine can decide, and nothing more.
  7. The tree. A grid used for data, a table without a header and a description that says only chart all passed. Read the tree, then listen to the file with a screen reader.

Alternative text on the figure and on the image

Wrong: figure(image("chart.png", alt: "…"), alt: "…"), or alt on a figure that holds a table. The guide says a figure’s alt makes many screen readers skip its contents, so it belongs only on a figure whose body cannot speak for itself, such as a drawing made of shapes, and never on both. Right: alt on the image, as in report.typ.

Checkpoint

Check: PDF/UA-1 export and its errors

  1. Question 1 of 2 A draft writes the total as $418 + 655 + 541 + 306 = 1920$ inside a sentence. What happens with --pdf-standard ua-1?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The export stops with missing alt text at that line; math.equation(alt: …) fixes it

    Write the alternative text the way a person would read the formula aloud: "418 plus 655 plus 541 plus 306 equals 1920".

  2. Question 2 of 2 What did typst compile --no-pdf-tags --pdf-standard ua-1 report.typ do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It refused: cannot disable PDF tags when exporting a PDF/UA-1 document

    The PDF reference says --no-pdf-tags makes a file inaccessible and blocks the accessible PDF/A levels and every PDF/UA part.

A table needs table.header; a grid is read as loose text

The guide’s rule for tables is short: use table, not grid, for data, because a table lets assistive technology: Software or devices that people with disabilities use to read and work with documents, such as screen readers, which speak the text, and refreshable braille displays. They read a PDF through its tags. move by row and column, while a grid’s contents are read in source order. This session sets the report’s data three more ways and exports each for PDF/UA-1:

A grid, a table without a header, and row headers Python · tables_session.py
from session import files, read, run, write

# The report's table in three more forms: as a grid, as a table without table.header, and as a table whose first column
# is marked as row headers with pdf.header-cell, which needs the a11y-extras feature. Each is exported for PDF/UA-1,
# and the structure trees show what a screen reader gets. (2>/dev/null hides a warning poppler prints about an
# attribute of Typst's table tags.)
files("report/report.typ", "report/make_chart.py", "vera.py", "tags.py")
text = read("report.typ")
HEADER = "table.header[Month][Rain (mm)][Rainy days],"
ROWS = "[June], [418], [21],\n    [July], [655], [27],\n    [August], [541], [25],\n    [September], [306], [17],"
assert HEADER in text and ROWS in text
write("grid.typ", text.replace("table(", "grid(").replace(HEADER, "[Month], [Rain (mm)], [Rainy days],"))
write("no-header.typ", text.replace(HEADER, "[Month], [Rain (mm)], [Rainy days],"))
write("row-headers.typ", text.replace(HEADER, 'table.header(pdf.header-cell(scope: "both")[Month], [Rain (mm)], [Rainy days]),')
      .replace(ROWS, "\n    ".join(f'pdf.header-cell(scope: "row")[{month}], [{rain}], [{days}],' for month, rain, days in
                                    (("June", 418, 21), ("July", 655, 27), ("August", 541, 25), ("September", 306, 17)))))
run(r"""
python3 make_chart.py
typst compile --pdf-standard ua-1 grid.typ grid.pdf
pdfinfo -struct-text grid.pdf 2>/dev/null | python3 tags.py | sed -n '/wettest/,/June/p'
typst compile --pdf-standard ua-1 no-header.typ no-header.pdf
pdfinfo -struct-text no-header.pdf 2>/dev/null | python3 tags.py | grep -a -E "T[HD] " | head -4
python3 vera.py ua1 grid.pdf no-header.pdf
!typst compile --pdf-standard ua-1 row-headers.typ row-headers.pdf
typst compile --features a11y-extras --pdf-standard ua-1 row-headers.typ row-headers.pdf
pdfinfo -struct-text row-headers.pdf 2>/dev/null | python3 tags.py | grep -a -E "T[HD] " | head -8
python3 vera.py ua1 row-headers.pdf
""")

Output

$ python3 make_chart.py
$ typst compile --pdf-standard ua-1 grid.typ grid.pdf
$ pdfinfo -struct-text grid.pdf 2>/dev/null | python3 tags.py | sed -n '/wettest/,/June/p'
  H1 "2. The wettest months"
  Div
    Caption
      Span "Figure 2: The four monsoon months."
    Div
      Div
        Span "Month"
      Div
        Span "Rain (mm)"
      Div
        Span "Rainy days"
      Div
        Span "June"
$ typst compile --pdf-standard ua-1 no-header.typ no-header.pdf
$ pdfinfo -struct-text no-header.pdf 2>/dev/null | python3 tags.py | grep -a -E "T[HD] " | head -4
      TD "Month"
      TD "Rain (mm)"
      TD "Rainy days"
      TD "June"
$ python3 vera.py ua1 grid.pdf no-header.pdf
grid.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
no-header.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
$ typst compile --pdf-standard ua-1 row-headers.typ row-headers.pdf
error: module `pdf` does not contain `header-cell`
   ┌─ row-headers.typ:21:21
   │
21 │     table.header(pdf.header-cell(scope: "both")[Month], [Rain (mm)], [Rainy days]),
   │                      ^^^^^^^^^^^

[exit status 1]
$ typst compile --features a11y-extras --pdf-standard ua-1 row-headers.typ row-headers.pdf
$ pdfinfo -struct-text row-headers.pdf 2>/dev/null | python3 tags.py | grep -a -E "T[HD] " | head -8
      TH "Month"
      TH "Rain (mm)"
      TH "Rainy days"
      TH "June"
      TD "418"
      TD "21"
      TH "July"
      TD "655"
$ python3 vera.py ua1 row-headers.pdf
row-headers.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 tables_session.py

The grid became Div and Span tags in source order, and its caption turned into “Figure 2”: with no table inside, Typst counted the figure as an ordinary figure. The table without table.header kept its Table tag, but every cell was a TD. veraPDF passed both files on all 106 rules: neither the grid nor a missing header breaks a rule a machine can check. With table.header, the report’s own export had a THead of three TH cells, so each number can be read with the name of its column.

The months in the first column name their rows. pdf.header-cell(scope: "row") marks such a cell, and scope: "both" the corner cell, but only behind the a11y-extras feature: without --features a11y-extras the export stopped with “module pdf does not contain header-cell”, and with it the months became TH cells. The PDF reference warns that these functions have no final interface and will be removed in a future release, so keep them out of a template that must build for years. Answer three questions about your own content:

Interactive · answer and compare Which element for these rows and columns? Answer three questions about your content; the ways that cannot serve a listener are ruled out, with the reason from the recorded exports.
Is the content data that a reader compares across rows and columns?
Does the first column name each row, as the months do?
Must the source build unchanged with later Typst versions, as a template does?

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

Four ways to set rows and columns in Typst against what a listener gets and what the export accepts
Element Moving by row and column Cells named by their headings PDF/UA-1 export Stable in later versions
grid Tags it as: Div and Span in source order Poor read as loose text, cell after cell Poor no header cells at all Good passed all 106 rules Good a built-in element
table without a header Tags it as: a Table of TD cells Good a Table with rows Poor every cell a TD Good passed all 106 rules Good a built-in element
table with table.header Tags it as: a THead of TH cells, then TD Good a Table with rows Mixed columns named; the row labels stay TD Good passed all 106 rules Good a built-in element
table.header and pdf.header-cell Tags it as: TH cells for columns and rows Good a Table with rows Good the months became TH cells too Mixed passed, but only with --features a11y-extras Poor no final interface; to be removed later
How the answers decide
  • “Is the content data that a reader compares across rows and columns?” Yes, it is data: rules out grid and table without a header. The guide says to use table, not grid, for data, and a table without a header gives a listener numbers with no column names.
  • “Is the content data that a reader compares across rows and columns?” No, it only places things side by side: rules out table without a header, table with table.header and table.header and pdf.header-cell. Layout is not a table. A grid tags its parts as Div and Span, which is right for things placed side by side.
  • “Is the content data that a reader compares across rows and columns?” Yes, it is data and “Does the first column name each row, as the months do?” Yes: adds a caveat to table with table.header. table.header names the columns only. The months stayed TD cells until pdf.header-cell marked them as row headers.
  • “Does the first column name each row, as the months do?” Yes and “Must the source build unchanged with later Typst versions, as a template does?” Yes: adds a caveat to table.header and pdf.header-cell. The PDF reference says pdf.header-cell has no final interface and will be removed in a future release, so a template that uses it may break.
  • “Must the source build unchanged with later Typst versions, as a template does?” No, it is built once: Every way passed the PDF/UA-1 export, so the export cannot choose for you; the structure tree shows the difference.

Key idea

Data goes in a table with a table.header, never in a grid: the validator passed all three versions, and only the structure tree shows which one a screen reader can move through.

Checkpoint

Check: Tables, grids and header cells

  1. Question 1 of 2 The grid copy of the report passed all 106 PDF/UA-1 rules. What does a screen reader get from its data?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Div and Span tags in source order: the fifteen cells as one run of text, with no rows or columns

    A validator checks that the tags are well formed, not that data is tagged as a table. Use table with table.header for data, and grid for layout.

  2. Question 2 of 2 A department template sets a timetable whose first column names each day. It must build unchanged for years. What do you choose?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: table with table.header now, and pdf.header-cell for the days only if the department accepts that the API may go

    table.header names the columns with a stable element. Row headers are possible today only through an experimental function, so weigh it against the years the template must last, and pin the Typst version if you use it.

One export can meet PDF/UA-1 and PDF/A-2a

An archive asks for PDF/A: The ISO 19005 family of standards for PDFs meant to be kept, whose files must carry what they need to show the same pages later, such as their fonts, and name their part and level in their metadata. More in “Archival PDF/A and final submission checks”, a reader for PDF/UA. Typst 0.15 can target several compatible standards in one build, its changelog says, and the accessibility guide recommends PDF/UA-1 with PDF/A-2a by default, or PDF/A-3a when files are attached. The session exports both pairs, checks each file against every profile it claims, and then tries the combinations Typst refuses:

PDF/UA-1 with PDF/A-2a and with PDF/A-3a, and five refusals Python · standards_session.py
from session import files, run

# report.typ exported for PDF/UA-1 together with PDF/A-2a, and together with PDF/A-3a, each file checked by veraPDF
# against the profiles it claims; then the combinations Typst 0.15.1 refuses, and PDF/UA-2.
files("report/report.typ", "report/make_chart.py", "vera.py")
run(r"""
python3 make_chart.py
typst compile --pdf-standard ua-1 --pdf-standard a-2a report.typ ua1-a2a.pdf
python3 vera.py ua1 ua1-a2a.pdf
python3 vera.py 2a ua1-a2a.pdf
pdfinfo -meta ua1-a2a.pdf | grep -o -E "<pdf(a|ua)id:(part|conformance)>[^<]*"
typst compile --pdf-standard ua-1 --pdf-standard a-3a report.typ ua1-a3a.pdf
python3 vera.py ua1 ua1-a3a.pdf
python3 vera.py 3a ua1-a3a.pdf
!typst compile --pdf-standard ua-1 --pdf-standard a-4 report.typ
!typst compile --pdf-standard ua-1 --pdf-standard 2.0 report.typ
!typst compile --pdf-standard a-2a --pdf-standard a-3a report.typ
!typst compile --pdf-standard 1.7 --pdf-standard 2.0 report.typ
!typst compile --pdf-standard ua-2 report.typ
""")

Output

$ python3 make_chart.py
$ typst compile --pdf-standard ua-1 --pdf-standard a-2a report.typ ua1-a2a.pdf
$ python3 vera.py ua1 ua1-a2a.pdf
ua1-a2a.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
$ python3 vera.py 2a ua1-a2a.pdf
ua1-a2a.pdf: PDF/A-2a validation profile: compliant, 153 rules passed, 0 failed
$ pdfinfo -meta ua1-a2a.pdf | grep -o -E "<pdf(a|ua)id:(part|conformance)>[^<]*"
<pdfaid:part>2
<pdfaid:conformance>A
<pdfuaid:part>1
$ typst compile --pdf-standard ua-1 --pdf-standard a-3a report.typ ua1-a3a.pdf
$ python3 vera.py ua1 ua1-a3a.pdf
ua1-a3a.pdf: PDF/UA-1 validation profile: compliant, 106 rules passed, 0 failed
$ python3 vera.py 3a ua1-a3a.pdf
ua1-a3a.pdf: PDF/A-3a validation profile: compliant, 155 rules passed, 0 failed
$ typst compile --pdf-standard ua-1 --pdf-standard a-4 report.typ
error: PDF/A-4 and PDF/UA-1 are mutually incompatible because they do not have any overlapping PDF versions
hint: PDF/A-4 requires version PDF 2.0
hint: PDF/UA-1 requires a version between PDF 1.4 and PDF 1.7
[exit status 1]
$ typst compile --pdf-standard ua-1 --pdf-standard 2.0 report.typ
error: PDF 2.0 is not compatible with PDF/UA-1
hint: PDF/UA-1 requires a version between PDF 1.4 and PDF 1.7
[exit status 1]
$ typst compile --pdf-standard a-2a --pdf-standard a-3a report.typ
error: choose at most one PDF/A standard
[exit status 1]
$ typst compile --pdf-standard 1.7 --pdf-standard 2.0 report.typ
error: PDF cannot conform to PDF 1.7 and PDF 2.0 at the same time
[exit status 1]
$ typst compile --pdf-standard ua-2 report.typ
error: invalid value 'ua-2' for '--pdf-standard <PDF_STANDARD>'
  [possible values: 1.4, 1.5, 1.6, 1.7, 2.0, a-1b, a-1a, a-2b, a-2u, a-2a, a-3b, a-3u, a-3a, a-4, a-4f, a-4e, ua-1]

  tip: a similar value exists: 'ua-1'

For more information, try '--help'.
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 standards_session.py

Standards asked for What happened
ua-1 and a-2a passed 106 rules of PDF/UA-1 and 153 of PDF/A-2a; the metadata claims both
ua-1 and a-3a passed 106 rules of PDF/UA-1 and all 155 of PDF/A-3a, the part that allows attachments
ua-1 and a-4 refused: no PDF version overlaps, since PDF/A-4 needs PDF 2.0 and PDF/UA-1 at most 1.7
ua-1 and 2.0 refused: PDF 2.0 is not compatible with PDF/UA-1
a-2a and a-3a refused: choose at most one PDF/A standard
1.7 and 2.0 refused: a PDF cannot conform to two versions at once
ua-2 not a value: Typst offers PDF/UA-1 only

The a in 2a and 3a is the conformance level: The letter after a PDF/A part, as in PDF/A-2b or 2u: b for the basic rules, u for text that maps to Unicode as well, a for an accessible tagged file as well. More in “Archival PDF/A and final submission checks” that requires a tagged, accessible file, so it pairs naturally with PDF/UA-1. PDF/UA-2 needs PDF 2.0, which is why it pairs with PDF/A-4; the guide says it is planned for Typst but not yet available, and that PDF/A-4 dropped level a in favour of PDF/UA. The archival lesson shows what an archive’s PDF/A check reads.

Checkpoint

Check: PDF/UA-1 together with PDF/A

  1. One question A thesis must be accessible and archival, and the library wants its data file attached to the PDF. Which export?

    Choose one answer.

    Show the answer: One question

    Answer: typst compile --pdf-standard ua-1 --pdf-standard a-3a thesis.typ

    The guide's rule: PDF/UA-1 with PDF/A-2a by default, with PDF/A-3a when files are attached.

HTML export reflows the text and writes MathML

A PDF keeps its lines where they were set. Someone who enlarges the text or widens its spacing needs text that reflow: Laying text out again to fit the reader's screen, text size and spacing, as a web page does in a browser. A PDF keeps its lines where they were set, which is why an HTML version can help readers who enlarge the text., and WCAG 2.2’s success criterion 1.4.12 asks content written in markup, such as HTML, to survive wider spacing. The guide knows of no PDF viewer that lets readers change text spacing, and suggests distributing an HTML version beside the PDF. Typst’s HTML export is experimental, so it needs a feature flag:

html_outline.py: the elements of a page, in order Python · html_outline.py
"""Prints the elements of an HTML file in document order, one a line, indented by nesting: the element's name, its
lang or alt attribute, and the text it holds directly, cut at 60 characters. What a <style> element holds and the src
of an image (a long data: URL) are left out, so the outline shows the page's structure.

    python3 html_outline.py report.html
"""

import sys
from html.parser import HTMLParser

VOID = {"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"}
ATTRIBUTES = ("lang", "alt", "href")


class Outline(HTMLParser):
    def __init__(self):
        super().__init__()
        self.lines = []  # [depth, text]
        self.open = []  # the lines of the elements that are open, innermost last

    def handle_starttag(self, tag, attrs):
        shown = " ".join(f'{name}="{value}"' for name, value in attrs if name in ATTRIBUTES and value)
        line = [len(self.open), tag + (f" {shown}" if shown else "")]
        self.lines.append(line)
        if tag not in VOID:
            self.open.append(line)

    def handle_endtag(self, tag):
        if self.open and self.open[-1][1].split()[0] == tag:
            self.open.pop()

    def handle_data(self, data):
        text = " ".join(data.split())
        if text and self.open and self.open[-1][1].split()[0] not in ("style", "script"):
            line = self.open[-1]
            line.append(text)


outline = Outline()
with open(sys.argv[1], encoding="utf-8") as page:
    outline.feed(page.read())
for depth, element, *texts in outline.lines:
    text = " ".join(texts)
    if len(text) > 60:
        text = text[:57] + "..."
    print("  " * depth + element + (f' "{text}"' if text else ""))
report.typ as HTML, and the outline of the page Typst wrote Python · html_session.py
from session import files, run

# report.typ exported as HTML: refused without the html feature, then exported with it. html_outline.py prints the
# elements of the page Typst wrote, in order, with the language, the alternative text and the text of each element.
files("report/report.typ", "report/make_chart.py", "html_outline.py")
run(r"""
python3 make_chart.py
!typst compile --format html report.typ report.html
typst compile --features html --format html report.typ report.html
python3 html_outline.py report.html
""")

Output

$ python3 make_chart.py
$ typst compile --format html report.typ report.html
error: html export is only available when `--features html` is passed
 = hint: html export is under active development and incomplete
 = hint: see https://github.com/typst/typst/issues/5512 for more information

[exit status 1]
$ typst compile --features html --format html report.typ report.html
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

$ python3 html_outline.py report.html
html lang="en-IN"
  head
    meta
    meta
    title "Monsoon rainfall at Station A"
    meta
    style
  body
    h1 "Monsoon rainfall at Station A"
    h2 "1. Summary"
    p "Most of the rain at Station A, an invented station, falls..."
    figure
      img alt="Bar chart of monthly rainfall: under 160 mm a month from January to May, 418 mm in June, a peak of 655 mm in July, then falling to 306 mm in September."
      figcaption "Figure 1: Monthly rainfall at Station A."
    h2 "2. The wettest months"
    figure
      table
        thead
          tr
            th "Month"
            th "Rain (mm)"
            th "Rainy days"
        tbody
          tr
            td "June"
            td "418"
            td "21"
          tr
            td "July"
            td "655"
            td "27"
          tr
            td "August"
            td "541"
            td "25"
          tr
            td "September"
            td "306"
            td "17"
      figcaption "Table 1: The four monsoon months."
    p "The total for the four months is mm. The data are describ..."
      math
        mn "418"
        mo "+"
        mn "655"
        mo "+"
        mn "541"
        mo "+"
        mn "306"
        mo "="
        mn "1920"
      a href="https://example.com/station-a" "https://example.com/station-a"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 html_session.py

Without --features html the export refused; with it, Typst warned that HTML export is under active development and not for production use. The page it wrote kept what the tags held: lang="en-IN" on the html element, the title as h1, the headings as h2, the image’s alt, a table with th cells, captions as figcaption and the formula as MathML, which the 0.15.0 changelog says Typst now writes for equations. Pages built this way are HTML like any other: check them with the same tools you use for a web page.

In the PDF’s tags In the HTML page
/Lang (en-IN) <html lang="en-IN">
H1 “1. Summary” <h2> “1. Summary”
Figure with alternative text <img alt="…"> in a <figure>
TH in a THead <th> in a <thead>
Formula <math> with MathML elements

An experimental export

The warning says the behaviour may change at any time. Pin the Typst version that builds your HTML, and look at the outline again after an upgrade.

Checkpoint

Check: HTML export for reflow

  1. Question 1 of 2 What did typst compile --format html report.typ need before it wrote report.html?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: --features html, because HTML export is experimental

    The warning says the behaviour may change at any time, so pin the version that builds your HTML.

  2. Question 2 of 2 Why does Typst's accessibility guide suggest an HTML version beside the PDF?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Readers can change text size and spacing in a browser, which the guide knows of no PDF viewer allowing

    Offer both: the PDF for printing and archiving, the HTML for reading on a phone or with changed text settings.

Practise: find the gaps before the export does

Two checks a build can run before it exports. The first reads what typst eval lists for images and equations and names each one without alternative text; the second says which --pdf-standard values Typst 0.15.1 will refuse together, worded as the session’s errors are. Write each in the editor below and press Run for its sample tests; one hint is open for each:

Code lab · 2 steps

Check a Typst export before it runs

Name the images and equations without alternative text from typst eval's lists, then catch the PDF standards that cannot be combined.

  1. Name the images and equations without alternative text To do
  2. Catch the PDF standards Typst will not combine To do

Exercise · Easy · Python

Name the images and equations without alternative text

A --pdf-standard ua-1 export stops at the first image or equation without alternative text. A script can list them all first. These two commands print what it needs, as JSON:

  • typst eval --in report.typ 'query(image).map(it => (it.source, it.alt))' prints one [source, alt] pair per image, such as [["chart.png",null]];
  • typst eval --in report.typ 'query(math.equation).map(it => (it.block, it.alt))' prints one [block, alt] pair per equation, where block is true for a display equation and false for an inline one.

Write missing_alt(images, equations) in missing.py. It takes the two printed texts and returns a list of messages, the images first and then the equations, each in document order:

  • "image <source>: no alt text" for an image whose alt is null or only spaces;
  • "equation <n> (inline): no alt text" or "equation <n> (display): no alt text" for such an equation, where n is its position among all the equations, counting from 1.

For the lesson's no-alt.typ, missing_alt('[["chart.png",null]]', '[[false,"418 plus 655 plus 541 plus 306 equals 1920"]]') returns ["image chart.png: no alt text"].

Starter code · missing.py

import json


def missing_alt(images, equations):
    """Return one message for each image and equation without alternative text."""
    # Replace this line: read both JSON texts and check each alt.
    return []
The sample tests · test_missing.py
from missing import missing_alt

REPORT_IMAGES = '[["chart.png","Bar chart of monthly rainfall: under 160 mm a month from January to May."]]'
REPORT_EQUATIONS = '[[false,"418 plus 655 plus 541 plus 306 equals 1920"]]'


def test_report():
    """finds nothing in the lesson's report.typ"""
    assert missing_alt(REPORT_IMAGES, REPORT_EQUATIONS) == []


def test_image_without_alt():
    """names the chart of no-alt.typ"""
    assert missing_alt('[["chart.png",null]]', REPORT_EQUATIONS) == ["image chart.png: no alt text"]


def test_inline_equation_without_alt():
    """names the inline formula of formula.typ as equation 1"""
    assert missing_alt(REPORT_IMAGES, "[[false,null]]") == ["equation 1 (inline): no alt text"]


def test_numbers_and_kinds():
    """numbers equations among all of them and says display or inline"""
    equations = '[[false,"x equals 2"],[true,null],[false,null]]'
    assert missing_alt("[]", equations) == [
        "equation 2 (display): no alt text",
        "equation 3 (inline): no alt text",
    ]


def test_spaces_only():
    """treats alternative text of spaces only as missing"""
    assert missing_alt('[["divider.svg","   "]]', "[]") == ["image divider.svg: no alt text"]


def test_images_first():
    """lists the images before the equations, each in document order"""
    images = '[["a.png",null],["b.png","A map of the station"],["c.svg",null]]'
    assert missing_alt(images, "[[true,null]]") == [
        "image a.png: no alt text",
        "image c.svg: no alt text",
        "equation 1 (display): no alt text",
    ]


def test_empty_document():
    """returns an empty list for a document without images or equations"""
    assert missing_alt("[]", "[]") == []
A hint

json.loads(images) turns the text into a list of two-item lists, with None for null. Treat an alt as missing when alt is None or not alt.strip(), and number the equations with enumerate(..., start=1).

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

Catch the PDF standards Typst will not combine

A build script takes the --pdf-standard values of a Typst export from a settings file. Write standard_problem(values) in standards.py, which says what Typst 0.15.1 would refuse before the export runs. values is a list such as ["ua-1", "a-2a"]. Check, in this order, and return the first problem as Typst words it, or None:

  1. a value that is not one of 1.4 1.5 1.6 1.7 2.0 a-1b a-1a a-2b a-2u a-2a a-3b a-3u a-3a a-4 a-4f a-4e ua-1: "invalid value '<value>'";
  2. more than one PDF/A value (those starting with a-): "choose at most one PDF/A standard";
  3. more than one PDF version (1.4 to 2.0): "PDF cannot conform to PDF <first> and PDF <second> at the same time", with the first two versions in the order given;
  4. ua-1 with a-4, a-4f or a-4e: "PDF/A-<part> and PDF/UA-1 are mutually incompatible", where <part> is what follows a-, such as 4f;
  5. ua-1 with 2.0: "PDF 2.0 is not compatible with PDF/UA-1".

The lesson's session met all five: ["ua-1", "a-2a"] gives None, and ["ua-2"] gives "invalid value 'ua-2'".

Starter code · standards.py

ACCEPTED = "1.4 1.5 1.6 1.7 2.0 a-1b a-1a a-2b a-2u a-2a a-3b a-3u a-3a a-4 a-4f a-4e ua-1".split()


def standard_problem(values):
    """Return the first problem Typst 0.15.1 would report for these --pdf-standard values, or None."""
    # Replace this line: test the five rules in order.
    return None
The sample tests · test_standards.py
from standards import standard_problem


def test_lesson_pairs():
    """accepts ua-1 with a-2a and with a-3a, as the session's exports did"""
    assert standard_problem(["ua-1", "a-2a"]) is None
    assert standard_problem(["ua-1", "a-3a"]) is None


def test_ua2():
    """rejects ua-2, which Typst 0.15.1 does not offer"""
    assert standard_problem(["ua-2"]) == "invalid value 'ua-2'"


def test_two_pdfa():
    """refuses two PDF/A parts"""
    assert standard_problem(["a-2a", "a-3a"]) == "choose at most one PDF/A standard"


def test_two_versions():
    """refuses two PDF versions, named in the order given"""
    assert standard_problem(["1.7", "2.0"]) == "PDF cannot conform to PDF 1.7 and PDF 2.0 at the same time"


def test_pdfa4_with_ua1():
    """names the PDF/A-4 part that cannot go with PDF/UA-1"""
    assert standard_problem(["ua-1", "a-4"]) == "PDF/A-4 and PDF/UA-1 are mutually incompatible"
    assert standard_problem(["a-4f", "ua-1"]) == "PDF/A-4f and PDF/UA-1 are mutually incompatible"


def test_version_two_with_ua1():
    """refuses PDF 2.0 with PDF/UA-1"""
    assert standard_problem(["ua-1", "2.0"]) == "PDF 2.0 is not compatible with PDF/UA-1"


def test_order_of_rules():
    """reports an invalid value before any other problem"""
    assert standard_problem(["a-2a", "a-3a", "a-2c"]) == "invalid value 'a-2c'"


def test_alone_and_empty():
    """accepts PDF/A-4 or PDF 2.0 alone, and no standard at all"""
    assert standard_problem(["a-4"]) is None
    assert standard_problem(["2.0"]) is None
    assert standard_problem([]) is None
A hint

Keep the 17 accepted values in a list. Then split values into pdfa = [v for v in values if v.startswith("a-")] and versions = [v for v in values if v[0].isdigit()], and test the rules one after another, returning at the first one that applies.

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.

Then test yourself on the five ideas, from the default tags to the HTML page. Whatever you pick, the reason appears at once, and the next question comes from the idea that needs it most:

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 A Typst document has no set text(lang: …). Which language does its PDF declare?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: en, English

    Set the language at the very start, with region where the variant matters, and mark passages in other languages with text(lang: …).

  2. Question 2 of 10 A template draws a thin coloured rule under each heading with line(). Must you wrap it in pdf.artifact?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: No; the guide says shapes are artifacts already

    pdf.artifact is for other decoration, such as a decorative image, which would otherwise need alternative text.

  3. Question 3 of 10 The same report failed 2 rules by default and passed all 106 with --pdf-standard ua-1. What did ua-1 add?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: The PDF/UA identification in the metadata and the setting to show the title instead of the file name

    ua-1 also checks what it can and stops the export on a gap, as the three drafts showed.

  4. Question 4 of 10 Before a two-hour export of a long book, a script prints [["fig-3.png",null]] for typst eval --in book.typ 'query(image).map(it => (it.source, it.alt))'. What does it tell you?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: fig-3.png has no alternative text, so a ua-1 export would stop on it; add alt before exporting

    The same query over math.equation lists formulas with their alt; the lesson's first exercise turns both lists into messages.

  5. Question 5 of 10 Which of these copies of the report passed all 106 PDF/UA-1 rules? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 5 of 10

    Answer:

    • The table without table.header
    • The report with table.header
    • The grid copy

    veraPDF's documentation says that for PDF/UA only machine verifiable checks are performed. The tree, not the verdict, told the three apart.

  6. Question 6 of 10 An invoice template lays out its line items, quantities and prices with grid. What do you change, and how do you check it?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Use table with table.header for the line items, then look for TH cells in the structure tree

    Keep grid for the layout around the table, such as the address block beside the logo, and table for the data.

  7. Question 7 of 10 Why did Typst refuse --pdf-standard ua-1 --pdf-standard a-4?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: PDF/A-4 needs PDF 2.0 and PDF/UA-1 at most PDF 1.7, so no version satisfies both

    PDF/A-4 belongs with PDF/UA-2, which needs PDF 2.0 and which Typst does not offer yet.

  8. Question 8 of 10 A funder's rules say "PDF/UA-2". What can a Typst 0.15.1 project deliver today?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: Not PDF/UA-2: ua-2 is not a value of --pdf-standard; ask whether PDF/UA-1 with PDF/A-2a is accepted

    Read the funder's rule closely: many ask for PDF/UA without a part. Where part 2 is required, LaTeX's tagging, which targets PDF/UA-2, is the other route this module teaches.

  9. Question 9 of 10 In the HTML page Typst wrote, what became of the report's level-1 headings?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: h2 elements, because the title from #title() became the h1

    The page has one h1, its title, and the document's headings below it, as a web page should.

  10. Question 10 of 10 A team wants to publish only Typst's HTML export of its annual report and stop making the PDF. What do you advise?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Keep the PDF/UA-1 file as well, and treat the HTML as an experimental extra with a pinned Typst version

    Check the HTML with the tools you use for web pages, and compare its outline after each Typst upgrade.

PDF Accessibility Checker (PDF/UA) Open the exported PDF to see its title, language, figures' alternative text and table header cells, checked in your browser. Color Contrast Checker (WCAG 2.2) Check the colours of a Typst theme against the contrast the guide asks for: 4.5:1 for normal text.

Interview questions

Warm-up (fresher to 2–5 years): Typst tags every PDF by default. Why export with --pdf-standard ua-1 anyway?

The default export is a tagged PDF, but it is not a PDF/UA file. In this lesson’s run it failed 2 of the 106 rules of veraPDF’s PDF/UA-1 profile: its metadata did not claim PDF/UA, and it did not ask viewers to show its title. --pdf-standard ua-1 adds both, and it also turns the gaps Typst can detect into errors that stop the export: a missing document title, an image or an equation without alternative text, a first heading below level 1 or a skipped level. That makes the build fail where a person would otherwise have to notice. What it cannot catch is equally important: a grid used for data, a table without a header row and poor descriptions all passed, so the structure tree and a screen reader still need a look.

Two harder questions follow: invoices that read as a jumble of numbers, and theses that must be both archival and accessible. 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 Typst template produces about 2,000 invoices a month as tagged PDFs, with the line items laid out by grid. A customer who uses a screen reader says the line items arrive as a jumble of numbers. Diagnose it and fix the template so that the fix holds.

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

  2. Mid to senior

    A university will accept theses written in Typst if each is both archival and accessible, and some must carry their data files inside the PDF. Choose the export settings and design the checks the build runs before a thesis is accepted.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Typst tags every PDF by default, but only you can give the title, the language and the alternative text, and the default export still failed two PDF/UA-1 rules.
  • --pdf-standard ua-1 passed all 106 rules and stopped with an error for a missing title and for an image or a formula without alternative text; typst eval lists the same gaps before an export.
  • Put data in a table with table.header: a grid and a table without a header both passed the validator, and only the structure tree showed the difference.
  • ua-1 pairs with a-2a, or a-3a for attachments, in one export; PDF/A-4, PDF 2.0 and a second PDF/A part were refused, and PDF/UA-2 is not available yet.
  • HTML export, behind --features html, kept the language, the headings, the alternative text, the table headers and the formula as MathML, for readers who need text that reflows.

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does Typst tag by itself, and what must you add?

    It tags headings, paragraphs, lists, figures, tables and links, and marks headers, footers and shapes as artifacts; you add set document(title: ...), set text(lang: ...) and alt on every image and equation.

  2. Why did Typst's default export fail PDF/UA-1?

    Its metadata did not claim PDF/UA (rule 5-1) and it did not ask viewers to show its title (rule 7.1-10); --pdf-standard ua-1 adds both.

  3. What does --pdf-standard ua-1 refuse?

    A missing document title, an image or an equation without alt text, a first heading below level 1 and a skipped level. It refuses --no-pdf-tags outright.

  4. Where does alt go for a picture inside a Typst figure?

    On the image. A figure's own alt makes many screen readers skip its contents, so give it only to a figure whose body cannot speak for itself, and never to both.

  5. Why put data in a Typst table with table.header rather than a grid?

    A grid is tagged as Div and Span in source order, and a table without a header has only TD cells; both passed veraPDF, but only a table with its header can be read by row and column.

  6. Which standards can one Typst export combine with PDF/UA-1?

    PDF/A-2a, or PDF/A-3a when files are attached. PDF/A-4 and PDF 2.0 were refused, because PDF/UA-1 needs PDF 1.7 at most, and PDF/UA-2 is not available yet.

  7. Why export an HTML version beside the PDF?

    Readers who enlarge text or widen its spacing need text that reflows. Typst's HTML export, behind --features html, kept the language, headings, alt text, table headers and the formula as MathML.

  8. How can a build list the images and equations without alt text before exporting?

    typst eval with query(image) or query(math.equation) lists each one with its alt text, null where there is none.

Terms in this lesson

assistive technology
Software or devices that people with disabilities use to read and work with documents, such as screen readers, which speak the text, and refreshable braille displays. They read a PDF through its tags.
reflow
Laying text out again to fit the reader's screen, text size and spacing, as a web page does in a browser. A PDF keeps its lines where they were set, which is why an HTML version can help readers who enlarge the text.

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.