How do I get the measurement for min-height headlines right?

I’m trying to create a headline slot with a fixed min-height: I have various titles that should be independently able to outgrow the space. So most paragraphs start at the exact same position on the page and only long outliers move the next paragraph down.

To showcase what I mean:

When I’m not measuring my reference block at 100% width the “growing” part does not work and when I add the 100% width I’m starting to measure anything, but not the correct height :tired_face:

Can someone help me get this right please, I can’t get it to work?

Here is my MWE:

#set page(width: 140mm, height: 220mm)
// Toggle debug label rendering
#let debug = true

// Desired minimum height of the heading slot
#let heading-slot = 5em

#show heading.where(level: 1): it => {
  show strong: s => {
    set text(size: 30pt, fill: red)
    set par(leading: 0pt)
    block(above: 0pt, below: 0.15em, s.body)
  }

  context {
    let heading-content = align(center, smallcaps(all: true, it.body))

    // Variant 1: natural width (comment out to compare)
    let measured-height = measure(heading-content).height
    // Variant 2: at full text width (comment out to compare)
    // let measured-height = measure(block(width: 100%, heading-content)).height

    let target-height = measure(block(height: heading-slot)).height
    let slot-height = calc.max(measured-height, target-height)

    let debug-label = if debug {
      align(right, text(6pt, fill: red, [measured: #measured-height, target: #target-height, slot: #slot-height]))
    } else {
      []
    }

    // Fixed-height slot for the heading (debug label + content)
    let slot = block(
      width: 100%,
      height: slot-height,
      [#debug-label #heading-content],
    )

    box(stroke: 0.25pt + blue, slot)
  }
}

= Short title with #strong[Short]
First paragraph after short title.

= Medium title with #strong[Two Words]
First paragraph after medium title.

= Long style #strong[Title] in the middle
First paragraph after long title.

= Even longer title with a very long #strong[Strong Part That Wraps To Three Lines]
First paragraph after very long title.

I have a context-less approach: putting each heading in a two-column grid, whose first cell is a zero-width fixed-height block, and the second cell is the real heading. Is that satisfactory?

#set page(width: 140mm, height: 220mm)
// Toggle debug strokes
#let debug = true

// Desired minimum height of the heading slot
#let heading-slot = 5em

#show heading.where(level: 1): it => {
  show strong: s => {
    set text(size: 30pt, fill: red)
    set par(leading: 0pt)
    block(above: 0pt, below: 0.15em, s.body)
  }

  grid(
    columns: (0pt, 1fr),
    align: center,
    stroke: if debug { green + 0.5pt },
    block(height: heading-slot), smallcaps(all: true, it.body),
  )
}


= Short title with #strong[Short]
First paragraph after short title.

= Medium title with #strong[Two Words]
First paragraph after medium title.

= Long style #strong[Title] in the middle
First paragraph after long title.

= Even longer title with a very long #strong[Strong Part That Wraps To Three Lines]
First paragraph after very long title.

Besides, you might be concerned that 1em in regular texts and headings are different.

#show heading: it => [#it #1em.to-absolute()]
= Heading

#context 1em.to-absolute()

Explanation of the measure function

The measure function lets you determine the layouted size of content. By default an infinite space is assumed, so the measured dimensions may not necessarily match the final dimensions of the content. If you want to measure in the current layout dimensions, you can combine measure and layout.

For example:

#context (
  // Both are the same as the line height
  measure(lorem(5)).height,
  measure(lorem(1000)).height,
)

#box(height: 7.24pt, width: 1pt, stroke: green) #lorem(5)

as @Y.D.X said, using measure + layout is the way to go if you want to measure. Here is the solution I came up with:

#show heading.where(level: 1): it => {
  show strong: s => {
    set text(size: 30pt, fill: red)
    set par(leading: 0pt)
    block(above: 0pt, below: 0.15em, s.body)
  }
  layout(size => [
    #let heading-content = align(center, smallcaps(all: true, it))
    
    #let height = measure(
      width: size.width,
      heading-content,
    ).height
    
    #block(
      stroke: 0.25pt + blue,
      width: 100%,
      height: calc.max(height, heading-slot.to-absolute()),
      heading-content
    )
  ])
}

I think the solution using grids is a bit cleaner, but might have some issues with accessibility, as far as I understand things like screenreaders might get confused about there being a grid (although I’m not 100% sure). This is also why it’s recommended to use it instead of it.body whenever possible, as it preserves the structure better :)

2 Likes

Ah, man, thank you both – I should have thought on a meta level too, the grid solution is very elegant. Measure and I are just not friends. But thank you very much @aarnent I will try both, the note about screen readers is actually very valuable too! :pray:

1 Like