Skip to content

Render a table

Renderer.Table renders a slice of structs or maps as an aligned text table — or, when the renderer's format is csv/tsv/markdown/json/yaml, in that machine-readable form instead. One call, every format.

From struct tags

Tag the fields you want as columns with table:"Header[,sortable]":

type Release struct {
    Tag     string `table:"Tag,sortable"`
    Date    string `table:"Date,sortable"`
    Assets  int    `table:"Assets,sortable"`
    Notes   string `table:"-"` // excluded
}

r := output.New(output.WithWriter(os.Stdout), output.WithFormat(output.FormatText))
_ = r.Table(releases, output.WithSortBy("Date"), output.WithSortDescending())
Tag     Date        Assets
v1.4.0  2026-07-20  6
v1.3.0  2026-06-11  6

Explicit columns (and maps)

[]map[string]any has no tags, so pass columns explicitly. WithColumns also lets you set widths and per-cell formatters:

_ = r.Table(rows, output.WithColumns(
    output.Column{Header: "Name", Field: "name", Sortable: true},
    output.Column{Header: "Size", Field: "bytes", Formatter: humanize},
))

Options

Option Effect
WithColumns(cols...) define columns explicitly (required for maps)
WithSortBy(header) sort by a Sortable column (numeric-aware)
WithSortDescending() reverse the sort
WithNoHeader() omit the header row
WithNoTruncation() do not truncate to terminal width (e.g. piping to a file)
WithMaxWidth(n) override terminal-width detection

Not every option applies to every format — behaviour by format has the matrix. In short: json and yaml ignore all of them, and csv/tsv ignore WithNoHeader.

Sorting: what the key actually is

WithSortBy takes the column's Header, not the Go field name. For Tag string `table:"Tag,sortable"` the two happen to coincide; for Name string `table:"NAME,sortable"` the sort key is "NAME". A mismatch returns unknown sort column: "...", and a column without sortable returns column "..." is not sortable.

The sort runs on the rendered cell text, after any Formatter. Each pair is compared with strconv.ParseFloat first and falls back to a string compare, so a numeric column sorts numerically — until a formatter adds a unit:

// "100 B" sorts before "9 B": both fail ParseFloat, so this is a string sort.
output.Column{Header: "Size", Field: "Size", Sortable: true, Formatter: bytesWithUnit}

If you need both a numeric sort and a formatted display, sort on a raw column and format a separate one.

Machine-readable formats

The same call honours the renderer's format. With output.WithFormat(output.FormatCSV):

Tag,Date,Assets
v1.4.0,2026-07-20,6

Markdown output is safe by construction: a literal | in a cell is backslash-escaped and newlines fold to <br>, so a value can never break out of its cell or open a new row.

Unicode

Column widths and truncation use display width, not byte length, via charmbracelet/x/ansi. CJK, emoji and combining characters align correctly and a truncated cell never splits a rune mid-sequence.

When a table will not render

Table returns an error rather than guessing:

You passed Error
a single struct, not a slice rows must be a slice
[]map[string]any without WithColumns Table requires a slice of structs or explicit WithColumns
a struct with no table: tags no table tags found on struct
an untyped nil cannot derive columns from nil; use WithColumns

A Column.Field that names nothing is not an error — the cell is empty. Full list, including the three inputs that panic, in errors and failure modes.

What tables cannot do

No right alignment, no cell wrapping (long values are truncated, not folded), no borders, no footer row, and no dotted paths into nested structs. See what this does not do for the workarounds.