Toolarity

Typst Line Spacing and Paragraph Spacing Not Working

Fix Typst Published October 10, 2026

“I set the line spacing and nothing changed” is a common complaint about Typst’s par function. The two parameters involved, leading and spacing, sound like they should both be “line spacing”, and the par reference page (read 2026-09-29) does define both, correctly. What it doesn’t say is where each one stops working: leading can look completely dead if your paragraphs are one line long, and spacing can look completely dead inside a tight list or right after a heading.

I compiled every case below with the official Typst CLI, 0.15.1 (current release) and 0.14.2, and measured the exact vertical gap between lines in the rendered PDF (in points) with a small Python script using PyMuPDF. Both versions produced identical numbers in every test.

Short answer (Typst 0.15.1 and 0.14.2, tested 2026-09-29):

You wantUseWatch out for
More/less space between wrapped lines inside one paragraph#set par(leading: ...)Has zero effect if the paragraph is only one line, which is common with short, blog-style paragraphs
More/less space between two paragraphs#set par(spacing: ...)Doesn’t touch the line spacing inside a paragraph at all
More/less space between bullet or numbered list items#set list(spacing: ...) / #set enum(spacing: ...)#set par(spacing: ...) has no effect on a tight list (items on back-to-back lines), which falls back to par’s leading. A loose list, with a blank line between items, does use par’s spacing
More/less space between a heading and the paragraph right after it#show heading: set block(below: ...)#set par(spacing: ...) had no measurable effect on this gap in any variant I tried (heading levels 1 to 3, em and pt units, with and without a heading show rule)

Test 1: leading and spacing are two separate knobs

I compiled four versions of the same two paragraphs (the first one long enough to wrap across five lines on a 9cm-wide page, the second one short):

#set page(width: 9cm, height: 22cm, margin: 1.2cm)
#set text(size: 10pt, font: "New Computer Modern")

#lorem(28)

#lorem(10)

Page 1 is the default. Page 2 adds #set par(leading: 1.5em) only. Page 3 adds #set par(leading: 0.65em, spacing: 3em) (spacing raised, leading back to default). Page 4 raises both.

$ typst compile leading_vs_spacing.typ leading_vs_spacing.pdf
EXIT:0

Measuring the y-position of each line with PyMuPDF and taking the difference between consecutive lines:

PageleadingspacingGap between wrapped linesGap between paragraphs
1 (default)0.65em1.2em (default)13.33pt18.83pt
21.5emdefault21.83pt18.83pt (unchanged)
3default3em13.33pt (unchanged)36.83pt
41.5em3em21.83pt36.83pt

Two labeled panels rendered from Typst 0.15.1. Panel A, set par leading to 1.5em with spacing left default: wrapped lines are visibly further apart, red caption reads "line-to-line gap 21.83pt (was 13.33pt); paragraph-to-paragraph gap: still 18.83pt, unchanged". Panel B, set par spacing to 3em with leading left default: wrapped lines look the same as default, the gap before the next paragraph is visibly larger, red caption reads "line-to-line gap still 13.33pt, unchanged; paragraph-to-paragraph gap: 36.83pt, was 18.83pt".

Raising leading on its own never moved the paragraph-to-paragraph gap (18.83pt in both page 1 and page 2), and raising spacing on its own never moved the line-to-line gap (13.33pt in both page 1 and page 3). They are independent by design; the confusion is just the names.

Test 2: with one-line paragraphs, leading does literally nothing

This is a common cause of “I changed it and nothing happened”. leading only affects the gap between lines that exist because a paragraph wrapped. If every paragraph is short enough to fit on one line, there’s no such gap to change.

#set page(width: 18cm, height: 10cm, margin: 1.2cm)
#set text(size: 10pt, font: "New Computer Modern")

#set par(leading: 0.65em) // page 1; page 2 uses 4em instead
Short paragraph one, definitely one line.

Short paragraph two, definitely one line.

Short paragraph three, definitely one line.

Page 1 uses the default leading: 0.65em. Page 2 uses leading: 4em, more than six times larger.

$ python3 measure_lines.py single_line_leading.pdf
--- page 1 (leading: 0.65em) ---
y0=32.79
y0=51.62  gap=18.83
y0=70.45  gap=18.83
--- page 2 (leading: 4em) ---
y0=32.79
y0=51.62  gap=18.83
y0=70.45  gap=18.83

The two gap columns are identical, down to the hundredth of a point.

Two labeled panels rendered from Typst 0.15.1, each showing three short one-line paragraphs. Panel A, leading 0.65em (default), caption reads "gap between paragraph lines: 18.83pt". Panel B, leading 4em, caption reads "gap between paragraph lines: 18.83pt, identical to A. Each paragraph is one line, so leading never applies." The two panels look visually identical.

Multiplying leading by more than six did not move a single pixel in the output. If your content is written in short paragraphs (which is normal for web copy), leading is the wrong parameter for “more space between paragraphs” — that’s spacing, covered in Test 1.

Test 3: par’s spacing doesn’t reach a tight list

The next place people reach for #set par(spacing: ...) is to loosen up a bullet or numbered list. For an ordinary tight list, written as - item lines back to back, it doesn’t work.

#set par(spacing: 3em)

- First item
- Second item
- Third item

Paragraph after the list.
$ python3 measure_lines.py list_spacing.pdf
--- page 1 (only par spacing set) ---
list item gap = 13.33pt   (default, unchanged)
list-to-paragraph gap = 36.83pt   (this one did change)

The gap between list items stayed at the default 13.33pt. The gap from the list down to the next paragraph did change to 36.83pt, because that transition is governed by par. Adding #set list(spacing: 3em) on top fixes the item spacing:

#set par(spacing: 3em)
#set list(spacing: 3em)

- First item
- Second item
- Third item

Paragraph after the list.
list item gap = 36.83pt   (now matches)

Two labeled panels rendered from Typst 0.15.1. Panel A, only par spacing set to 3em: a bullet list with tight spacing between its three items, then a paragraph clearly further below, caption reads "list item gap: 13.33pt, unchanged; list-to-paragraph gap: 36.83pt". Panel B, list spacing also set to 3em: the same bullet list now has visibly larger gaps between its own items, caption reads "list item gap: 36.83pt, now matches; list-to-paragraph gap: 36.83pt".

I checked why in a copy of Typst’s layout rules source (the file that implements LIST_RULE and ENUM_RULE) that I saved on 2026-09-25. I didn’t record which commit that copy came from, so treat the excerpt below as supporting evidence; the measurements above are the proof. A tight list (no blank line between items, the default when you write - item back to back) inserts its own spacing before it renders, and if you never set list(spacing:), that value falls back to par’s leading, not its spacing:

if tight {
    let spacing = elem
        .spacing
        .get(styles)
        .unwrap_or_else(|| styles.get(ParElem::leading));
    let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
    realized = v + realized;
}

That explains the 13.33pt: it’s the same figure as the default paragraph line-to-line gap from Test 1, not the default paragraph spacing. enum has the identical fallback. If you want tight list items spaced like your paragraphs, set list/enum’s own spacing explicitly, or make the list loose (next section).

Loose lists behave differently

Put a blank line between the items and Typst treats the list as “loose”. Then par’s spacing does apply:

#set par(spacing: 4em)   // second copy: 1.2em

- one

- two

- three
par spacingItem gap, tight listItem gap, loose list
1.2em (default)13.33pt18.83pt
4em13.33pt (unchanged)46.83pt

Numbered lists (+ one) gave the same numbers. If you also set list(spacing:), it wins over par in a loose list too: par(spacing: 4em) plus list(spacing: 1.2em) measured 18.83pt between items.

So a blank line between items is a second fix: the list then follows your paragraph spacing. The trade-off is that the list reads looser, each item spaced like a separate paragraph. The source excerpt above only covers the tight case; for loose lists this is a measured result, not something I found in the source.

Test 4: par’s spacing doesn’t reach the paragraph right after a heading

This is the least intuitive one, and the one I’d flag as “verify this yourself before you rely on it.” I set the heading’s own block spacing via #show heading: set block(above:, below:) (headings apply their default spacing through BlockElem::above/below; a copy of Typst’s heading.rs I saved on 2026-09-25, commit not recorded, computes below = Em::new(0.75) / scale for level 2+) and a par(spacing:) value on the paragraph that follows, using plain pt units on both sides so there’s no ambiguity about what a relative em is measured against:

#show heading: set block(above: 10pt, below: 5pt)   // case A
#set par(spacing: 50pt)
== Heading
Paragraph after the heading.

Four cases, only swapping which of below / spacing is large:

Caseheading belowpar spacingMeasured gap
A5pt50pt15.05pt
B50pt5pt60.05pt
C30pt30pt40.05pt
D5pt5pt15.05pt

Case A and case D both use below: 5pt, and their spacing differs by 10x (50pt vs 5pt) — the measured gap is identical (15.05pt) in both. Line up all four: the gap is below + 10.05pt every time, regardless of what spacing was set to.

Two labeled panels rendered from Typst 0.15.1. Panel A, heading block below set to 5pt and par spacing set to 50pt: a heading followed closely by a paragraph, caption reads "measured gap: 15.05pt. The large par spacing (50pt) had no effect." Panel B, heading block below set to 50pt and par spacing set to 5pt: the same heading now has a visibly larger gap before its paragraph, caption reads "measured gap: 60.05pt. The small par spacing (5pt) had no effect either, only heading's own below moved the gap."

As a control, I ran the same two spacing values (50pt and 5pt) between two plain paragraphs, no heading involved: the gaps came out to 56.83pt and 11.83pt — clearly tracking spacing (roughly spacing + 6.83pt). So spacing is not broken in general; it just doesn’t participate in this one specific transition, straight after a heading.

Because “doesn’t work” claims are easy to get wrong, I also tried the variants a reader is most likely to write, each with par(spacing:) at 0.5em and at 5em: a level 1 heading with no show rule (17.55pt both times), a level 2 heading with no show rule (15.94pt both times), and a level 3 heading under #show heading: set block(below: 1em) (16.83pt both times). A pt value (5pt vs 50pt, level 2, no show rule) gave 15.94pt both times. Typst 0.14.2 matched in every case.

I want to be precise about what this result is and isn’t. It’s a black-box measurement across four combinations on two Typst versions, and it was consistent every time. It is not a documented rule — the local archive of Typst’s reference docs (资料/typst_docs_20260925/) doesn’t include a page on block-level spacing merging, so I could not find an official explanation for why the paragraph’s own spacing is ignored here specifically. If you’re building something where this gap matters precisely, reproduce Test 4 yourself before depending on it; see “How to check this yourself” below.

What I’d do

  • Reach for leading only when you actually have wrapped, multi-line paragraphs. For short paragraphs, “no visible change” after raising leading isn’t a bug — there’s no wrapped line for it to act on.
  • Reach for spacing for the gap between paragraphs, but confirm it separately for anything that isn’t a plain paragraph next to another plain paragraph. It does not reach into tight lists (the usual back-to-back - item lines), and — in my tests — it did not move the gap right after a heading either.
  • For lists, set list(spacing:) / enum(spacing:) directly instead of assuming par’s settings cascade down. A tight list’s unset default equals par’s leading, which is usually much smaller than what people expect. Or leave a blank line between items, if a looser-looking list is fine.
  • For the space under a heading, use #show heading: set block(below: ...) instead of trying to push it via par.

How to check this yourself

The three numbers worth re-verifying, with the exact commands:

// single_line_leading.typ — confirms Test 2
#set page(width: 18cm, height: 10cm, margin: 1.2cm)
#set text(size: 10pt)
#set par(leading: 4em)
Short paragraph one, definitely one line.

Short paragraph two, definitely one line.
$ typst compile single_line_leading.typ single_line_leading.pdf
$ python3 measure_lines.py single_line_leading.pdf

Compare the gap value against the same file with leading: 0.65em — they should match.

// heading_gap.typ — confirms Test 4, case A vs case D
#show heading: set block(below: 5pt)
#set par(spacing: 50pt)   // then try 5pt in a second copy
== Heading
Paragraph after the heading.

Measure the gap both times; it should stay at roughly 15pt either way.

What this test does not cover

  • Only the official macOS (Apple Silicon) CLI builds of Typst 0.15.1 and 0.14.2, tested 2026-09-29. I didn’t test the web app.
  • Only New Computer Modern, the Typst default font, at 10pt. Other fonts have different metrics, and the constant offsets I measured (like the “+10.05pt” in Test 4) will differ with font and size. I’d expect which parameter applies to stay the same, but I only measured this one font and size.
  • The internal mechanism behind Test 4 (why par’s spacing is skipped right after a heading) is a measured, not a documented, result. I looked for it in the local Typst source and docs snapshot and didn’t find it; treat it as “reproduce before you rely on it,” not as a cited rule.
  • PDF output only. I didn’t test HTML export, which may lay out spacing differently.
  • Grid, table, and figure captions were not covered here — the table caption article on this site handles table-specific spacing separately.