How to increase the spacing between the number and the title of a heading?

I want to increase the default spacing between the number and the title of a heading.

This solution does not work, it inserts a vertical space before the heading.

#set heading(numbering: "1.1")

Blabla 1

= This is test 1

#show heading: it => [#h(1em) #it]

Blabla 2

= This is test 2

Blabla 3

image

#set heading(numbering: "1.1")
Blabla 1
= This is test 1
#set heading(numbering: (..n) => h(1cm) + numbering("1.1", ..n))
Blabla 2
= This is test 2
Blabla 3

image

You probably can also use pad(left: 1cm, <numbering>) instead of h() + , if you want. Or you can even reconstruct the heading and embed the spacing there, but be sure to wrap it all in block().

However, note that if you plan on using a period (or other symbol) at the end of the numbering format string, this will introduce an unwanted behavior when referencing a heading label:

#set heading(numbering: "1.1.")
= A <a>
@a.
#set heading(numbering: (..n) => numbering("1.1.", ..n))
= B <b>
@b.

image

This is because with a custom/user function, Typst can’t know if the last period is mandatory or can be lifted when a period already exist (at the end of a sentence).

1 Like

If you want to have more spacing between “1.” and “Heading” in “1. Heading”, you can use a show rule to “rebuild” the heading from scratch:

#set heading(numbering: "1.")
#show heading: it => block(counter(heading).display(it.numbering) + h(2em) + it.body)

= Hello world!

== Hello.

(Update [2024-09-24]: added block() around the replaced heading’s contents to ensure block spacing around headings is preserved, see How to increase the spacing between the number and the title of a heading? - #4 by Eric)

5 Likes

One thing to note when rebuilding the heading like that is that you should wrap it in a block to keep the spacing behavior of the original heading. When just using it.body, the heading gets display inline (or here as a normal paragraph).

4 Likes

Thanks, I’ve updated my answer.

If you also want the additional spacing in the outline, you can just do #set heading(numbering: "1.1 ") (with extra spaces in the string)

1 Like

This solution is also very elegant! I’m not sure which answer to accept.

This will affect @references as well, so it’s not recommended. Use a show rule similar to the one in my answer but for outline.entry instead. Note that it gets a bit longer though, since you also have to add it.fill and it.page after the title.

You will also have to write numbering(it.element.numbering, ..counter(heading).at(it.element.location())) instead of just counter(heading).display(it.element.numbering) to ensure it uses the numbers from the heading’s original location instead of the outline entry’s location (which would always return the number 0, since the outline comes before any headings).

Here’s the complete deal:

#set heading(numbering: "1.")

// Don't add spacing when there's no numbering (which is the case for the outline title)
#show heading: it => if it.numbering == none { it } else { block(counter(heading).display(it.numbering) + h(2em) + it.body) }

// NEW! Add spacing in the entries
#show outline.entry: it => {
  if it.element.func() == heading { // don't interfere with figure outlines (optional if you don't have one)
    let head = it.element // just to make our code shorter
    let number = numbering(head.numbering, ..counter(heading).at(head.location()))
    let fill = box(width: 1fr, it.fill)  // ensure the fill doesn't occupy the full page width, just the available space (1fr)
    [#number #h(2em) #head.body #fill #it.page]
  }
}

#outline()
= Hello world!

== Hello world!

= Hello world!

Actually it doesn’t for some reason:

#set heading(numbering: "1.   ")
= A <a>
See @a. 

image

or did you have something else in mind?

In any case I agree it might be a hack rather than a clean solution, unless the ref behavior of stripping the space gets documented.

1 Like

Indeed that’s why I used sijo’s solution in the end, Typst seems to do smart trailing space elision for references.

Hmm, this is slightly weird as I thought it won’t add any additional space to the heading because multiple spaces are generally always merged into one. But for references this rule does apply, although it removes all spaces, since the period is connected to the reference body.

This doesn’t actually have to do with multiple spaces being merged into one. In references, the trimmed numbering pattern is used, which consists only of the actual number. Any prefixes and suffixes are removed. This is also why a reference to an equation with the numbering "(1)" shows as “Equation 1” instead of “Equation (1)”.

3 Likes

Thanks, I wasn’t fully aware that spaces were also trimmed. Though I’d still be wary of issues popping up when e.g. manually invoking numbering(it.numbering, ..numbers) in a show rule.

The example currently seems to fail with:


error: expected string or function, found none

Could this be due to a 0.11 <> 0.12 change?

@a_w it works for me on v0.12. Did you by any chance not add any heading numbering like #set heading(numbering: "1.")?

1 Like

If you have any unnumbered headings (e.g. bibliography), the given show rule will error as you can’t pass none to the numbering function. This is why I would always suggest putting the number (and the following space) in a conditional. For example, when adjusting @PgBiel’s code from above:

#show outline.entry: it => {
  if it.element.func() == heading { // don't interfere with figure outlines (optional if you don't have one)
    let head = it.element // just to make our code shorter
    let number = if head.numbering != none {
      numbering(head.numbering, ..counter(heading).at(head.location()))
    }
    let fill = box(width: 1fr, it.fill)  // ensure the fill doesn't occupy the full page width, just the available space (1fr)
    [#number #h(2em, weak: true) #head.body #fill #it.page]
  } else {
    it // use default style for figure outlines
  }
}

By making the 2em spacing weak, it will collapse if there is no number before it.

3 Likes

You were both right, I have one unnumbered headline (Bibliography). Very, very helpful thanks so much! :pray:

Update: Original part of the post:

This is more of a learning question, but maybe it is also useful for others (and actually related to the topic). I have this if-condition to make the level: 1 heads bold:

    if head.level == 1 {
      [*#number #h(1em, weak: true) #head.body #fill #it.page*]
    } else {
      [#number #h(1em, weak: true) #head.body #fill #it.page]
    }

It works, but it is quite repetitive – is there a better, more elegant way to make such modifications? I don’t think there is a ternary, but I think I had situations like this a couple of times now and always wondered if I could do it better somehow.

Update: My approach to making this shorter and easier to maintain was to store the string contruction in a variable and using that, instead of repeating the line. See post 19.

And while I wrote this actually a follow-up question came up. My

[#number #h(1em, weak: true) #head.body #fill #it.page]
#v(0.1pt, weak: true) // spaces way more than expected

I had in there now goes rampant even if I make it 0.01pt I guess that is because the boxes/blocks collapsed and the vertical space stops that. I can get the expected result by making the #v negative, but it doesn’t seem like the “correct” solution? How would control of the vertical spacing be approach best?

This is still relevant but might get answered below.

Update: Original post, no longer relevant:

Addendum: I also just realized the reconstructing the outline this way removed the link functionality within the document.

Using

repr(head)

I found no obvious clue to “correctly” link to the headline (such as a label?).

I can get

link("https://forum.typst.app")[*#number #h(0.75em, weak: true) #head.body #fill #it.page*]

as a test to work. But not:

link(page: it.page)[*#number #h(0.75em, weak: true) #head.body #fill #it.page*]
// or something like
link((page: int(it.page), x: 0pt, y: 0pt))[*#number #h(0.75em, weak: true) #head.body #fill #it.page*]

Fails with “error: expected string, dictionary, location, or label, found content”. or “error: expected integer, boolean, float, decimal, or string, found content”

This can be solved by using head.location(), please see below.

This is what I came up with to get the links in the outline back:

#show outline.entry: it => {
  if it.element.func() == heading {
    // don't interfere with figure outlines (optional if you don't have one)
    let head = it.element // just to make our code shorter
    let number = if head.numbering != none {
      numbering(head.numbering, ..counter(heading).at(head.location()))
    }
    let fill = box(
      width: 1fr,
      it.fill,
    ) // ensure the fill doesn't occupy the full page width, just the available space (1fr)
    let toc-entry = box(
      //stroke: 1pt + red, //used for debugging
      link(head.location(), box(number + h(0.75em, weak: true) + head.body + fill + it.page)),
    )
    if head.level == 1 {
      strong(toc-entry)
    } else {
      toc-entry
    }
    v(-0.4em, weak: true)
  } else {
    it // use default style for figure outlines
  }
}

#outline(indent: 1.65em)
#pagebreak()

#set page(numbering: "1")
#counter(page).update(1)

#set heading(numbering: "1.1 ")

= First Headline 1
== First Headline 2
== Second Headline 2
#lorem(800)
= Second Headline 1
= Third Headline 1
#heading(numbering: none)[Literature]
Result

What I couldn’t solve:

  • If you need indents for everything below level 1 you get line breaks, messing up the spacing, because the indent “overfills” due to the 1fr fill. I guess it would be possible to leave the indent and add a own spacing instead, substracting this spacing from the 1fr, but then the code would bloat even further and another if/loop for subsequent levels would be necessary. Update: I tried that but subsctracting from fractions is not possible or at least would involve further measuring steps. Maybe there is a compact/elegant solution I’m just not seeing? I also tried pad() for the indents, but that leads to the same problem, just that the line breaks is below instead of above.

  • The spacing needs the weird v(-0.4em, weak: true), which I suppose should not be necessary if done right – I think my code introduces some space where it should not?

Here is an editable link to see the example.

Would be really great if someone more experienced could take a look. :pray: