---
title: "Generate a PDF with tables"
url: https://recursiveai.org/onepdf/cookbook/generate-tables/
description: "Generate a multi-page PDF table in C# with OnePdf.Layout: column widths, striped rows, header and footer rows on every page, spans and page numbers."
version: "1.11.0"
---

# Generate a PDF with tables

Generate a multi-page PDF table in C# with OnePdf.Layout: column widths, striped rows, header and footer rows on every page, spans and page numbers.

```sh
dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0
```

```csharp
using System.Globalization;
using OnePdf.Layout;

var us = CultureInfo.GetCultureInfo("en-US");
var rows = Enumerable.Range(1, 120)
    .Select(i => (Date: new DateOnly(2026, 1, 1).AddDays(i * 2), Crew: $"Crew {(i % 6) + 1}", Kg: 40m + (i * 37 % 90)))
    .ToList();

LayoutDocument.Compose(doc =>
{
    doc.Title("Catch log, 2026").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.Letter);
        page.Margin(54);
        page.Content().Column(col =>
        {
            col.Spacing(12);
            col.Item().Heading(1).Text("Catch log, 2026").FontSize(20).Bold();
            col.Item().Table(table =>
            {
                table.Columns(c =>
                {
                    c.Constant(90);       // a fixed width in points
                    c.Relative(2);        // shares the rest of the width by weight
                    c.Relative(1);
                });
                table.CellPadding(4);
                table.CellBorder(0.5, Colors.LightGrey);
                table.RowBackground(i => i % 2 == 1 ? LayoutColor.FromHex("#F2F2F2") : null);   // striped body rows

                table.HeaderRow(r =>                                     // repeated at the top of every page
                {
                    r.Cell().Text("Date").Bold();
                    r.Cell().Text("Crew").Bold();
                    r.Cell().AlignEnd().Text("Catch (kg)").Bold();
                });
                foreach (var row in rows)
                {
                    table.Row(r =>
                    {
                        r.Cell().Text(row.Date.ToString("MMM d", us));
                        r.Cell().Text(row.Crew);
                        r.Cell().AlignEnd().Text(row.Kg.ToString("N1", us));
                    });
                }
                table.FooterRow(r =>                                     // repeated at the bottom of every page
                {
                    r.Cell().Span(2).Text("Total, all crews").Bold();     // one cell over two columns
                    r.Cell().AlignEnd().Text(rows.Sum(x => x.Kg).ToString("N1", us)).Bold();
                });
            });
        });
        page.Footer().AlignCenter().Text(t =>
        {
            t.FontSize(9);
            t.Span("Page ");
            t.CurrentPageNumber();
            t.Span(" of ");
            t.TotalPages().ReservedDigits(2);
        });
    });
}).Save("catch-log.pdf");
```

The 120 rows fill five US Letter pages. Each page starts with the “Date, Crew, Catch (kg)” header row and ends with the total row, above the page number.

## Columns

- `Constant(points)` keeps a fixed width.
- `Relative(weight)` shares the width left over by weight, and never gets narrower than its longest word.
- `Auto()` takes the width of its widest content when there is room. An `Auto` column makes every row count for the width, so prefer `Constant` and `Relative` for long tables.

## Rows and cells

- `HeaderRow` and `FooterRow` repeat on every page the table reaches and never split. Body rows split between lines of text; `r.KeepTogether()` moves a row to the next page instead.
- `Span(columns, rows)` merges cells. `CellBackground`, `CellBorder`, `CellPadding` and `CellAlign` set one cell. `RowHeader()` makes a cell a row header for screen readers.
- Borders are collapsed: where two cells meet they share one line.
- A cell holds any content: text, an image, a nested column, another table.

## Good to know

- `ReservedDigits` sets the room left for the total page count, which isn’t known until the end. Reserve as many digits as the count can have; a centered footer shows the unused room as a gap.
- For a running subtotal per page (“Carried forward”), use `Accumulate` on the amount cells and `DynamicText` in the footer, as shown in [Layout](https://recursiveai.org/onepdf/docs/layout/).
- The output is tagged, with `TH` header cells, so the same document can be saved as PDF/UA: see [Create PDF/UA](https://recursiveai.org/onepdf/cookbook/create-pdf-ua/).
- `Save(path)` writes page by page, so memory stays small for long tables, and layout time grows in proportion to the number of rows.
- Text is set in Liberation Sans unless you register other fonts on `doc.Fonts`.

Every table option and the column-width rules are in [Layout](https://recursiveai.org/onepdf/docs/layout/).

For a complete multi-page invoice with an ASP.NET Core download, see [Create PDF invoices and reports in C#](https://recursiveai.org/onepdf/articles/create-pdf-csharp/).

Coming from MigraDoc tables: [Migrate from PDFsharp and MigraDoc](https://recursiveai.org/onepdf/articles/migrate-from-pdfsharp/).
