An Excel table (a ListObject) is an object, not formatting, and V5 treats it as one.
A TableDefinition holds a range, a header row count and a style name; each cell’s colour is
worked out from where it sits inside that range, every time the sheet is drawn.
Because the colours are never baked into the cells:
- a 100,000-row table costs one object,
- inserting a row in the middle re-bands the rest instead of breaking the pattern,
- a cell painted by hand punches through the banding,
- and the file gets a real
xl/tables/tableN.xmlpart rather than a scattering of ad-hoc fills.
The basics
using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.Tables;
// format A1:D100 as a table, first row as the header (default TableStyleMedium2)
var table = ws.AddTable("A1:D100");
// column names come from the header row
string first = table.Columns[0].Name;
// Excel's "Convert to Range" — the styling was never in the cells, so it just stops
ws.RemoveTable(table.Name);
_ = first;
AddTable takes an A1-style address or a defined name, or a RangePosition. The first row
becomes the header and the column names are read from it — blanks and duplicates are made unique
(Column3, Qty2), because Excel refuses a table whose column names collide.
Style and options
var table = ws.AddTable(RangePosition.Parse("A1:D100"), new AddTableOptions
{
Name = "Sales",
Style = "TableStyleMedium7", // one of the 60 built-ins; the name survives I/O verbatim
ShowRowStripes = true, // banded rows (default true)
ShowFirstColumn = true, // emphasise the first column
ShowFilterButton = true, // filter dropdowns on the header row (default false)
});
// flip the switches later — Style is a record struct, so change it with `with`
table.Style = table.Style with { ShowColumnStripes = true, ShowRowStripes = false };
| Option | Default | Meaning |
|---|---|---|
Name | Table1, Table2, … | Unique-in-workbook internal name. It is an identifier — a letter or _ first, then letters, digits, . and _ (no spaces, and nothing that reads as a cell reference like Q1) — because Excel spells it in structured references |
Style | TableStyleMedium2 | Built-in style name. An unknown one is never renamed (see below) |
HeaderRowCount | 1 | 0 or 1 — an Excel table has no multi-row header |
TotalsRowCount | 0 | 0 or 1. The row is coloured and round-trips, but the aggregates are not implemented |
ShowRowStripes | true | Banded rows |
ShowColumnStripes | false | Banded columns |
ShowFirstColumn / ShowLastColumn | false | Emphasise the first / last column |
ShowFilterButton | false | Filter dropdowns on the header row (creates the sheet’s auto-filter) |
ShowFilterButton is off by default because a worksheet holds one auto-filter. On by default,
creating a second table would silently take the buttons off the first.
Banded rows
A table’s stripes count rows the way sheet-wide banding does — visible rows only — so a filtered table stays evenly striped instead of showing two clean rows side by side. Inside a table the table’s own banding wins, and the sheet’s stripe stops at the table’s edge.
Excel keeps banding inside the table style, which is why this is the striping that round-trips through xlsx (a sheet-wide stripe has no home in the format, and stays reogrid-json only).
Fill order
Strongest first:
- Conditional format
- The cell’s own fill (the cell → row → column style chain)
- The table style
- The sheet stripe (outside the table)
The table joins the inheritance chain as Root ◁ Table ◁ Column ◁ Row ◁ Cell. Being weaker than
anything the cell says for itself is what lets you paint one row yellow and break the banding
just there. Borders work the same way: a hand-drawn border replaces the table’s edge by edge.
Querying
bool any = ws.HasTables;
// the table covering a cell, or null
TableDefinition? at = ws.GetTableAt(5, 2);
// by name (case-insensitive, as in Excel)
TableDefinition? byName = ws.GetTable("Sales");
// the rows carrying data — the range less its header and totals rows
RangePosition data = at?.DataRange ?? default;
// what the table contributes to a cell (the layer below the cell's own style)
var contributed = ws.GetTableStyle(5, 2);
// hand-drawn borders layered over the table's own
CellBorders borders = ws.GetEffectiveBorders(5, 2);
_ = (any, byName, data, contributed, borders);
Built-in styles, and your own
Excel stores only the name — an application is expected to know what TableStyleMedium7 looks
like. V5 generates all 60 from the name: the built-ins run in rows of seven, a neutral column
followed by the six theme accents, so the name gives up both the family and the accent. The palette
is then derived with the same HSL tint curve OOXML uses, which lands on the Office swatches
(“Lighter 40%” and friends).
An unknown name renders with the default palette but is never renamed, so a file carrying a style V5 does not know can be opened, saved and reopened in Excel looking exactly as it did.
// every built-in name (Light 1–21 / Medium 1–28 / Dark 1–11)
foreach (string name in TableStyles.BuiltinNames())
{
TableStyleDefinition def = TableStyles.Resolve(name);
_ = (def.HeaderFill, def.RowStripeFill, def.WholeBorderColor);
}
// register a palette of your own (this may also override a built-in name)
TableStyles.Register(new TableStyleDefinition
{
Name = "BrandTable",
HeaderFill = 0xFF1F3864,
HeaderColor = 0xFFFFFFFF,
HeaderBold = true,
RowStripeFill = 0xFFEAF0F8,
WholeBorderColor = 0xFF1F3864,
});
Inserting and deleting rows and columns
A table follows the data it describes: inserting above it moves it, inserting inside it grows it, and deleting a column cuts that column out of the column list too. A table whose every row is deleted goes with them.
I/O
| Format | What is written |
|---|---|
| reogrid-json | tables (reogrid-web’s JsonTableDefinition shape) |
| XLSX | xl/tables/tableN.xml + the [Content_Types].xml override + the sheet’s _rels + <tableParts>, numbered workbook-wide |
Not there yet
- Totals-row aggregates (
SUBTOTAL) — the row is coloured and round-trips, nothing computes - Structured references (
=Table1[Column]) - Auto-expand when an adjacent cell is edited