All posts

How do I create striped (zebra) table rows?

Pass template.TableStripe to a table call. gpdf paints the alternate body rows with the color you give it. No row-loop, no manual cell styling.

The question, in other words

I have a table — invoices, transactions, log lines, anything with more than 5 rows — and I want every other row tinted gray so the eye can track across without losing the line. Bootstrap calls it .table-striped. I just want that, in gpdf, without writing a row loop.

TL;DR

c.Table(header, rows, template.TableStripe(pdf.RGBHex(0xF5F5F5)))

That's it. gpdf handles the alternation. The header is excluded — only body rows are striped. The first body row stays plain; the second is tinted; the third plain; the fourth tinted; and so on.

Working code

package main

import (
    "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 background
    stripe := pdf.RGBHex(0xF5F5F5)    // every-other-row tint

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

            c.Table(
                []string{"Product", "Region", "Qty", "Revenue"},
                [][]string{
                    {"Laptop Pro 15", "NA",   "120", "$155,880"},
                    {"Wireless Mouse", "EU",  "640", "$19,193"},
                    {"USB-C Hub",      "APAC","410", "$20,495"},
                    {"Monitor 27\"",   "NA",  "180", "$71,820"},
                    {"Keyboard",       "EU",  "320", "$25,596"},
                    {"Webcam HD",      "APAC","260", "$23,397"},
                },
                template.ColumnWidths(40, 20, 15, 25),
                template.TableHeaderStyle(
                    template.TextColor(pdf.White),
                    template.BgColor(brand),
                ),
                template.TableStripe(stripe),
            )
        })
    })

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

go run main.go. Six body rows, three of them tinted, header in dark blue with white text. The kind of report that goes in a Monday email.

How the alternation works

Internally, gpdf walks the body rows with an index i starting at 0 and applies the stripe to rows where i%2 == 1. The header row is its own slice and isn't counted. So:

Body row index (0-based)VisuallyStriped?
01stno
12ndyes
23rdno
34thyes
.........

That parity matches the Bootstrap convention. The first row of data sits clean, and the stripe is the visual "rest" — your eye travels across the white row, the next is shaded, repeat.

There's no option to flip the parity (stripe odd-indexed rows instead). If you really want it inverted, prepend an empty row to the body — but you don't, because nobody actually wants that.

Pick the color

The whole point is subtle. A stripe loud enough to compete with the text defeats itself.

pdf.RGBHex(0xF5F5F5) // gentle warm gray — Bootstrap default territory
pdf.RGBHex(0xFAFAFA) // even softer, almost imperceptible at small sizes
pdf.RGBHex(0xEEF2FF) // pale brand tint (works if header is the brand color)
pdf.Gray(0.96)       // grayscale equivalent — saves a few bytes in PDF/A workflows

Avoid saturated colors. A blue stripe at 60% saturation reads "this row is selected/important" and breaks the across-row scan that zebra striping is supposed to fix.

For dark themes (rare in PDFs but they exist for slide-style reports), pdf.RGBHex(0x202020) over a 0x1A1A1A page works. Keep the contrast ratio low.

Combine with cell borders

Stripes alone are enough for short tables. For dense, finance-style tables, pair stripes with WithTableCellBorder to draw a hairline between every cell:

hairline := template.Border(
    template.BorderWidth(document.Pt(0.5)),
    template.BorderColor(pdf.Gray(0.85)),
)

c.Table(header, rows,
    template.ColumnWidths(40, 20, 15, 25),
    template.TableHeaderStyle(
        template.TextColor(pdf.White),
        template.BgColor(brand),
    ),
    template.TableStripe(pdf.RGBHex(0xF5F5F5)),
    template.WithTableCellBorder(hairline),
)

Hairline borders + light stripe = the look of every accountant's spreadsheet print preview, deliberately. Keep the border lighter than the stripe so the stripe stays the dominant signal.

If you only want an outer frame (no inner grid), swap WithTableCellBorder for WithTableBorder.

Mistakes that cost ten minutes

  • Color in the wrong unit range. pdf.RGB(245, 245, 245) produces a black box. The constructor expects 0.0–1.0, not 0–255. Use pdf.RGBHex(0xF5F5F5) if you're thinking in CSS values.
  • Striping the header. TableStripe does not touch the header. If you want a tinted header, that's TableHeaderStyle(template.BgColor(...)) — a different option. Confusing the two and then wondering why the header isn't tinted is the classic first-time bug.
  • Two-color alternation. gpdf supports one stripe color, not two. If you set both pdf.White (row 0) and 0xF5F5F5 (row 1), you don't actually need to set white — the page is already white. Asking for [white, gray, blue] 3-cycle is not a feature; it would also be hostile to the reader.
  • Stripes on a 3-row table. A stripe needs at least 4–5 body rows to do its job. On 2–3 rows, it just looks like one cell got selected. Skip it; let the table breathe.

Try gpdf

gpdf is a Go library for generating PDFs. MIT licensed, zero external dependencies, pure-Go TrueType handling.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs