How do I align a gpdf column to the right edge of a row?
Two things need to happen: a filler Col to push the column to the right edge, and template.AlignRight() on the text inside it. Here's the gpdf code.
The question, in other words
You wrote a row with a single r.Col(4, ...) in it, expecting the column to land on the right side of the page. It rendered on the left, taking up a third of the width, with a big empty gap to its right. Or you got the column onto the right but the text inside it still hugs the left. What's the actual idiom in gpdf?
TL;DR
There is no Offset, no Push, and no row-level Justify in gpdf. You push a column right with an empty filler Col, and you align the text inside it with template.AlignRight(). These are two different things and you almost always need both.
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {}) // filler
r.Col(4, func(c *template.ColBuilder) {
c.Text("Total: $25,575.00", template.AlignRight())
})
})
The code
A complete program — the invoice-header shape that sends most people looking for this:
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/pdf"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := template.New(
template.WithPageSize(document.A4),
template.WithMargins(document.UniformEdges(document.Mm(20))),
)
page := doc.AddPage()
// Left block and right block in one row: 8 + 4 = 12.
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {
c.Text("Acme Inc.", template.Bold(), template.FontSize(16))
c.Text("1-2-3 Shibuya, Tokyo", template.TextColor(pdf.Gray(0.4)))
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("INVOICE #1042", template.AlignRight(), template.Bold())
c.Text("Issued 2026-09-02", template.AlignRight(),
template.TextColor(pdf.Gray(0.4)))
})
})
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Spacer(document.Mm(10))
})
})
// Totals block: 8 spans of nothing, then 4 spans hard against the margin.
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {})
r.Col(4, func(c *template.ColBuilder) {
c.Text("Subtotal: $23,250.00", template.AlignRight())
c.Text("Tax (10%): $2,325.00", template.AlignRight())
c.Spacer(document.Mm(2))
c.Line(template.LineThickness(document.Pt(1)))
c.Spacer(document.Mm(2))
c.Text("Total: $25,575.00", template.AlignRight(),
template.Bold(), template.FontSize(14))
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("right_aligned.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
Why the filler column is required
This is worth understanding rather than memorising, because it explains a handful of other gpdf layout surprises at the same time.
A row is a horizontal box. RowBuilder.build walks r.cols in order and emits one child box per column, and the only style it sets on that child is a width:
colBox := &document.Box{
Content: cb.buildNodes(),
BoxStyle: document.BoxStyle{
Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),
},
}
gridColumns is 12. So Col(4) is literally "a box that is 33.33% of the row's width", nothing more. The row lays its children out left to right and stops when it runs out of children. It has no concept of the leftover space, because document.BoxStyle has no JustifyContent field — grep the whole repo and you will not find one. The fields are Width, Height, MinWidth, MaxWidth, MinHeight, MaxHeight, Margin, Padding, Border, Background, Direction, Position. That's the entire vocabulary.
So a lone Col(4) produces a row containing exactly one 33.33%-wide box, positioned at the start of the horizontal flow. The remaining 66.67% isn't "empty space to the right of your column" — it's nothing at all. There is no element there to push against.
The filler Col(8) gives the layout engine something to occupy that space. It costs one empty box in the tree and renders nothing.
Which also means Col(8) is not special. Col(6) + Col(3) + Col(3) right-aligns the last block just as well, and you can hang content off the fillers later without restructuring anything.
The trap: right column, left text
The mistake that eats the most time:
r.Col(8, func(c *template.ColBuilder) {})
r.Col(4, func(c *template.ColBuilder) {
c.Text("Total: $25,575.00") // ← no AlignRight
})
The column is where you wanted it. The text is not. It starts at the column's left boundary — 66.67% across the page — so it looks sort of right-aligned, and if all your numbers happen to be the same width you may not notice until a three-digit total lands next to a five-digit one and the decimal points stop lining up.
template.AlignRight() is a TextOption (func(*document.Style) setting TextAlign), so it goes on the individual c.Text call. There is no way to set it once for a whole column. Repeat it on every line in the block.
Tables, images, page numbers
Text options don't reach inside other elements. Each one has its own alignment door:
// Table: per-column alignment, positionally matched to the header slice.
c.Table(header, rows, template.ColumnAlign(
document.AlignLeft, document.AlignRight, document.AlignRight,
))
// Image: alignment within its containing column.
c.Image(logoBytes, template.WithAlign(document.AlignRight))
// Page numbers take TextOptions, so AlignRight works directly.
c.PageNumber(template.AlignRight())
ColumnAlign takes document.TextAlign values, not TextOption values — document.AlignRight, not template.AlignRight(). Easy to mix up, and the compiler will tell you immediately.
When the row can't help you
For something pinned to a coordinate regardless of what else is on the page — a PAID stamp, a diagonal watermark, a corner mark — leave the grid:
page.Absolute(document.Mm(140), document.Mm(60),
func(c *template.ColBuilder) {
c.Text("PAID", template.AlignRight(), template.Bold(),
template.FontSize(28), template.TextColor(pdf.RGBHex(0xC62828)))
},
template.AbsoluteWidth(document.Mm(50)),
)
That's the escape hatch, not the idiom. If you find yourself computing x-coordinates for ordinary content, the filler column was the answer.
One last thing worth knowing: Col clamps an individual span to 1–12, but nothing checks the total. Three Col(5) calls in one row give you percentage widths summing to 125%, and the third column overflows the right margin instead of wrapping. gpdf won't warn you. Count your spans.
Related recipes
- How does the 12-column grid work in gpdf? — the grid in full
- How do I nest a Row inside a Col? — the other flat-grid question
- How do I set column widths in a table? — widths and alignment inside
c.Table - Generate an invoice PDF in Go in under 50 lines — this pattern in a whole document
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