All posts

How do I make a table span multiple pages?

You don't do anything. Feed gpdf a table with more rows than fit, and it paginates the body and repeats the header on every page automatically.

The question, in other words

I have a report — invoice line items, a transaction log, a 300-row export — and it obviously won't fit on one A4 page. In a Go PDF library, what do I have to do to make the table flow onto page 2, page 3, and so on, with the header reappearing at the top each time? In gpdf, the answer is short.

TL;DR

Nothing. You write one Table call, give it all your rows, and gpdf paginates it:

c.Table(header, rows) // rows has 300 entries — gpdf splits it across pages

The body is split row by row across as many pages as it needs. The header slice is re-emitted at the top of every continuation page automatically — same column widths, same styling. There is no PageBreak() method to call, no MaxRowsPerPage option, no row-counting loop. Overflow is the layout engine's job, not yours.

Working code

A complete program that produces a multi-page table. Save as main.go, run go run ., get report.pdf.

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/pdf"
    "github.com/gpdf-dev/gpdf/template"
)

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
    )

    brand := pdf.RGBHex(0x1A237E)

    header := []string{"Date", "Invoice #", "Customer", "Amount"}
    rows := make([][]string, 0, 200)
    for i := 1; i <= 200; i++ {
        rows = append(rows, []string{
            fmt.Sprintf("2026-%02d-%02d", (i%6)+1, (i%28)+1),
            fmt.Sprintf("INV-%05d", 10000+i),
            fmt.Sprintf("Customer #%d", i),
            fmt.Sprintf("$%d.00", 100+i*7),
        })
    }

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("2026 Invoice Ledger", template.FontSize(18), template.Bold())
            c.Spacer(document.Mm(4))

            c.Table(header, rows,
                template.ColumnWidths(20, 20, 40, 20),
                template.TableHeaderStyle(
                    template.TextColor(pdf.White),
                    template.BgColor(brand),
                ),
            )
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("report.pdf", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

200 rows on A4 lands on roughly eight pages. On every one of them the dark-blue header sits at the top; the body picks up exactly where the previous page stopped. The only thing in that code that hints at "multi-page" is the 200 in the loop bound.

How it works

Worth understanding so you trust it. When the layout engine lays out the table, it measures body rows in order and adds them to the current page until the next row would exceed the available height. The rows that didn't fit become an overflow table — a new *document.Table carrying the same Header, the same Footer, and the leftover body rows. gpdf flushes the laid-out part to the page, opens the next page, and feeds the overflow table back into the layout engine with the new page's height. Repeat until there's nothing left over.

Two things fall out of that design:

  • The header repeats because it lives in tbl.Header, not in your loop. The overflow table reuses the same slice, so it re-renders identically on every page. You get this for free.
  • There's no "header doesn't fit" edge case. The engine reserves space for the header before measuring how many body rows fit. If a page can't hold the header plus at least one body row, the whole table is pushed to the next page instead of being split awkwardly.

Footers that repeat too

If you want a totals row (or a "page summary") that also appears at the bottom of every page, that's document.Table.Footer — available when you build the table at the document layer instead of through the builder:

import "github.com/gpdf-dev/gpdf/document"

tbl := &document.Table{
    Columns: []document.TableColumn{
        {Width: document.Pct(20)}, {Width: document.Pct(20)},
        {Width: document.Auto},    {Width: document.Pct(20)},
    },
    Header: headerRows, // []document.TableRow
    Body:   bodyRows,
    Footer: []document.TableRow{footerRow},
}

The Footer slice repeats on every continuation page, same mechanism as the header. The builder's c.Table(...) doesn't expose a footer because most short tables don't need one — when you do, you've left the common-case zone, and the deep-dive on tables walks through the document layer.

Forcing the table to start on a fresh page

There's no per-table "begin on a new page" option. You do it at the page level — add a page before the row that holds the table:

doc.AddPage() // the table below starts at the top of this page
page2 := doc.AddPage()
page2.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Table(header, rows /* , opts... */)
    })
})

That's the only "page break" control you need for tables, because the table's internal breaks are handled for you and the external one is just "where does this block start."

What you don't get

  • "Keep these rows together." Every body row is split-eligible. There's no annotation that says "row group 4–7 must stay on one page." It's a known gap. If an invoice line item plus its sub-rows really must not be torn across a page, the workaround is to start a fresh page before that group, or build the table at the document layer and insert your own break hints.
  • A footer on the last page only. document.Table.Footer repeats on every page by design (per-page column totals are the common case). For a one-shot grand total at the document end, append it as a separate block after the table, not inside it.
  • Page-of-N in the table itself. "Page 3 of 8" belongs in the document footer, not the table. See page numbers, headers, and footers for where that lives.

Mistakes that cost ten minutes

  • Looking for a PageBreak option. There isn't one and you don't want one — if you're calling it manually you've already lost. Just feed all the rows.
  • Splitting your data into per-page chunks yourself. People do rows[0:40] on page 1, rows[40:80] on page 2… Don't. You'll get the row math wrong, the last page will be short, and the header styling will drift. Hand gpdf the whole slice.
  • Expecting the header on page 1 only. Some libraries do that. gpdf repeats it on every page, which is what you want for a report someone prints and flips through.
  • A 6 MB CJK font on a 150-page table. The font is subset to the glyphs actually used, so this is fine — the output stays small. But if you somehow disabled subsetting, a long table is where it bites. Leave subsetting on (it's the default).

Try gpdf

gpdf is a Go library for generating PDFs. MIT licensed, zero external dependencies, native CJK support.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs