How to get differently-sized header or footer depending on page number?

With a #set page rule and context, it is possible to define header, footer, and page background depending on the page number.

How to do something similar for the size of header and footer, meaning page margins? Imagine wanting a much larger footer on odd pages, for example. If I understand correctly, the above approach does not work here because context always produces (opaque) content values, not a length value or other types required for the margin parameter in the page function.

If you want alternating page margins for binding, i.e., inside and outside margins that alternate based on whether it’s the inner or outer side of a page, you can use inside and outside inside a dictionary for the margin parameter.

For top and bottom margins, it is indeed more difficult. I think it is currently not possible, precisely because of the reason you stated. As a workaround you can always manually add v(1cm) for example in the header or footer.

Yes. But what does “page background” mean? background or fill? Page Function – Typst Documentation

Here are a few examples that should cover all basic cases:

set page(header + footer + background)
#set page(fill: gray, height: 4cm, width: 4cm, margin: 1cm)
#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#set page(
  header: context if is-odd-page() [odd] else [even] + " header",
  footer: context if is-odd-page() [odd] else [even] + " footer",
  background: context if is-odd-page() [odd] else [even] + " background",
)
#for n in range(1, 5) {
  [page #n]
  n += 1
  pagebreak(weak: true)
}

image

image

image

image

context set page(fill)
#set page(height: 4cm, width: 4cm, margin: 1cm)
#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#for n in range(1, 5) {
  context {
    set page(fill: if is-odd-page() { orange } else { purple })
    [page #n]
  }
  n += 1
  pagebreak(weak: true)
}

image

image

image

image

context page(fill)[]
#set page(height: 4cm, width: 4cm, margin: 1cm)
#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#for n in range(1, 5) {
  context page(fill: if is-odd-page() { orange } else { purple })[
    page #n
  ]
  n += 1
  pagebreak(weak: true)
}

image

image

image

image

set page(header + footer + background) + context page(fill)[]
#set page(height: 4cm, width: 4cm, margin: 1cm)
#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#set page(
  header: context if is-odd-page() [odd] else [even] + " header",
  footer: context if is-odd-page() [odd] else [even] + " footer",
  background: context if is-odd-page() [odd] else [even] + " background",
)
#for n in range(1, 5) {
  context page(fill: if is-odd-page() { orange } else { purple })[
    page #n
  ]
  n += 1
  pagebreak(weak: true)
}

image

image

image

image

Note that some of these only work (correctly or at all) from the git version, meaning they all will work in the future version 0.12.0. Try compiling them with v0.11.1 and see if it works.

Overall, you can define set page() once at the top of a document if you need for a field that can be of a content type to be set (because context will return content type, at least in the v0.11.1 and in git version from 2024-09-09). This includes header, footer, background etc.

But if you need to set a field that cannot be of a content type, then you would have to use set page() or page()[] for every new page (where you want the behavior to apply). This includes fill, margin etc.

Thankfully, you can split mixed set page() into two: 1 for content-type fields and 1 for other-type fields.

For margin it’s practically the same thing as the example with fill:

set page(header, footer) + context page(margin)[]
#set page(fill: gray, height: 4cm, width: 6cm)
#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#set page(
  header: context if is-odd-page() [odd] else [even] + " header",
  footer: context if is-odd-page() [odd] else [even] + " footer",
)
#for n in range(1, 5) {
  context page(margin: if is-odd-page() { 1cm } else { 1.5cm })[
    page #n
  ]
  n += 1
  pagebreak(weak: true)
}

image

image

image

image

Loop clarification

The loop in the examples is a shorthand for this:

#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#context page(margin: if is-odd-page() { 1cm } else { 1.5cm })[page 1]
#pagebreak()
#context page(margin: if is-odd-page() { 1cm } else { 1.5cm })[page 2]
#pagebreak()
#context page(margin: if is-odd-page() { 1cm } else { 1.5cm })[page 3]
#pagebreak()
#context page(margin: if is-odd-page() { 1cm } else { 1.5cm })[page 4]

But you can simplify this by using a wrapper function:

#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#let page-margin(..args) = {
  context page(margin: if is-odd-page() { 1cm } else { 1.5cm }, ..args)
}
#page-margin[page 1]
#pagebreak()
#page-margin[page 2]
#pagebreak()
#page-margin[page 3]
#pagebreak()
#page-margin[page 4]

As you can see, there are quite a few thing that can be written in different ways to achieve the same goal. Use whichever is more readable so that you will be able to maintain/go back to the code snippet and understand what it does.

One last note that is worth mentioning is that to have a syntax that accesses some context-based value (without the context keyword) in a separate place (to re-use it multiple times) you have to define a function and not a variable.

Examples (variable vs. function)
// error: `counter(page).get()` can only be used when context is known
#let is-odd-page = calc.rem(counter(page).get().first(), 2) == 1

#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
// error: `counter(page).get()` can only be used when context is known
#let page-margin = page.with(margin: if is-odd-page() { 1cm } else { 1.5cm }) 

// Correct:
#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#let page-margin(..args) = page(margin: if is-odd-page() { 1cm } else { 1.5cm }, ..args) 

// Or consume the context altogether:
#let is-odd-page() = calc.rem(counter(page).get().first(), 2) == 1
#let page-margin(..args) = context {
  page(margin: if is-odd-page() { 1cm } else { 1.5cm }, ..args)
}
1 Like

That does not change the size of the header/footer however. The header/footer content is then just clipped at page top/bottom.

@Andrew

Thanks for the extensive reply. Unfortunately, my question is still not answered. (I suspect that it’s simply not possible though.)

When using page as a function, a “subdocument” of sorts with its own content is created and inserted as separate pages. What I wanted was to have a document (imagine it just being #lorem(10000)) have non-trivial per-page styling. This is possible, as everyone agrees, for header/footer and background content, but probably not for other page properties like margins.

Unless someone knows more?

I thought that #show: it => ... might help, but it doesn’t, as far as I could see, because it postprocesses the main document content, not the typeset pages…

By the way, there is a simple workaround for fill, using background:

// instead of `#set page(fill: context { ... })`
#set page(
  background: context {
    let color = if calc.rem(counter(page).get().at(0), 2) == 0 {
      aqua
    } else {
      lime
    }
    rect(
      height: 100%,
      width: 100%,
      fill: color,
    )
  }
)

#lorem(10000)
1 Like

I don’t think you can do this at the moment, there are some related discussions about specifying styles which depend on other styles scattered over various issues.

One I remember which is fairly similar at the core is defining themes for documents using a state variable, the same problem presented itself for setting the colors depending on the variable in the state, e.g.:

#let theme = state("theme", (
  foreground: black,
  background: white,
))

#set text(fill: theme.get()) // doens't work, needs context
#set text(fill: context theme.get()) // doens't work, is opaque

You can see the whole discussion here:

While the underlying motivation different, the problem of one of the solutions was the same and remains unsolved. I outlined some ideas for resolving some of the style values contextually (see the comment about Lazy Colors, which could probably be applied to all style rules).

1 Like

I don’t understand how this does not answer the question:

The code does allow you to have a much larger footer/header (margins) on odd pages than on even pages. Well, actually, I messed up a little, I set smaller margins for odd pages, but the fix is to just swap the values:

context page(margin: if is-odd-page() { 1.5cm } else { 1cm })[

Maybe you mean how to do the same thing but without calling context page()[] or context [#set page();] at the start of every new page? Meaning, you are only allowed to call set page() once at the very top of a document. Then I don’t think it is possible.

If I now understand correctly, then sure. There is a similar topic, where I wrote a post comparing page.fill and page.background.

I can confirm that it is not possible. Especially varying side margins are hard to implement because the paragraph width can then change based on where a pagebreak falls. That’s why inside/outside margins are ok: They don’t affect the inner width in this way.

There are plans to make this kind of stuff possible at the layout engine level in the future.

4 Likes

Let me still reply to this: You’re calling page multiple times to create pages with specific properties. That of course you can do, but then, each of those pages has separate content provided to each page call. In such a case, depending on what you want to do, you probably don’t even need context, just a good old page call with whatever arguments/properties you desire per page. I’m aware it’s not exactly the same thing, but when I would create individual separate pages like this, I probably would not use context to set their individual properties.

What I wanted is to have page-number-specific layout or style (top/bottom margins specifically) applied to the (however many) pages that are typeset from one content value (the “entire” document).

I hope that clears it up. But thanks for your inputs anyway!

Cheers

Yes, it did, thank you.

2 Likes

I have logged an issue, capturing the gist of this discussion, in the Github project - Ability to use context in page margin attribute · Issue #5182 · typst/typst · GitHub

Is it possible to have differently-sized header for odd and even page in new version 0.12?

No, it’s not possible yet.

Here’s an attempt. Unfortunately it leads to layout divergence very easily (as is to be expected with this kind of hack), in particular if you have anything (a paragraph, grid etc.) that spans more than one page at once. You also need to ensure there’s at least one top-level paragraph or block on every page so the workaround is applied. Even though it’s cumbersome and (most of the time) broken, I’ll leave it here in case others want to improve on it.

// === CHANGE THESE ===
#let smallest-margin = 2em
#let margin-for(page) = if calc.even(page) { 7em } else { smallest-margin }
// ====================

#let in-block = metadata("Yup, we are in a block")
#let in-header = metadata("We are in the header")

// === USE MAKE-HEADER FOR YOUR HEADER ===
#let make-header(body) = {
  // Indicate to our show rule we should ignore headers
  // Use style instead of state to avoid layout divergence
  set outline(title: in-header)
  context place(top, dy: -page.header-ascent, {
  // Pretend stuff inside the header has as much height as our concept of margin allows
    block(height: margin-for(here().page()), body)
  })
}

// Ensure we don't try to place floats inside blocks (doesn't work)
#show <marg>: set bibliography(title: in-block)
#show block: it => {
  show block: set bibliography(title: in-block)
  it
}

#set page(margin: smallest-margin)

// On each top-level paragraph or block
#show selector(par).or(block): it => context {
  if outline.title == in-header {
    // Don't try to add fake margin inside the header
    // (it is, itself, limited by the real margin)
    return it
  }
  if bibliography.title == in-block {
    // Can't parent-scope stuff inside a block.
    return it
  }
  // Add a top float to the page to simulate increased margin.
  let previous = query(selector(<marg>).before(here()))
  let this-page = here().page()
  if previous.len() == 0 or previous.last().location().page() != this-page {
    [#place(
      top,
      float: true,
      scope: "parent",
      clearance: margin-for(this-page) - smallest-margin,
    )[] <marg>]
    it
  } else {
    it
  }
}

Here’s some sample usage.

// USAGE:

// Use make-header!
#set page(height: 200pt, header: make-header[
  #rect(height: 100%)
])

#lorem(25)

#lorem(25)

#lorem(25)

#lorem(25)

#lorem(25)

#rect(fill: red)

#lorem(25)

#lorem(25)

#lorem(25)

#lorem(25)

#lorem(25)

#lorem(25)

#lorem(25)

Any very long paragraph or block (or basically almost anything) will lead to problems… so usage of this solution is extremely discouraged. :sweat_smile:

The only ideal solution would be a built-in implementation, which unfortunately isn’t there yet.

2 Likes