V4 V5

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.xml part 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 };
OptionDefaultMeaning
NameTable1, 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
StyleTableStyleMedium2Built-in style name. An unknown one is never renamed (see below)
HeaderRowCount10 or 1 — an Excel table has no multi-row header
TotalsRowCount00 or 1. The row is coloured and round-trips, but the aggregates are not implemented
ShowRowStripestrueBanded rows
ShowColumnStripesfalseBanded columns
ShowFirstColumn / ShowLastColumnfalseEmphasise the first / last column
ShowFilterButtonfalseFilter dropdowns on the header row (creates the sheet’s auto-filter over the header and data rows — never the totals row, which stays in view whatever the filter hides, as in Excel)

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.

The default TableStyleMedium2: filter buttons on the header, a stripe on every other row starting with the first data row, rules between the rows, and a totals row under a double rule

The figure is the default TableStyleMedium2 with ShowFilterButton and TotalsRowCount = 1. The totals-row formulas (SUBTOTAL(109,B2:B8) here) were put in by the application, not by V5 (see below).

Banded rows

As in Excel, the stripe starts on the first data row (rows 1, 3, 5, … of the data carry the stripe colour). Rows are counted the way sheet-wide banding counts them — 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:

  1. Conditional format
  2. The cell’s own fill (the cell → row → column style chain)
  3. The table style
  4. 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.

Inside the table, the parts of the style stack in Excel’s order (later wins): whole table → column stripes → row stripes → last column → first column → header row → totals row. Each part overrides only what it sets, so a header row with no fill of its own (Medium 22–28, for instance) shows the whole-table fill through it.

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. What each name looks like is published: ECMA-376 defines the 60 built-ins in presetTableStyles.xml, and V5 carries those definitions as they are, so the rows that get a stripe, the edges that get a rule and the text that turns bold all match Excel.

Six built-in styles: Light 1, Light 10, Light 21, Medium 2 (the default), Medium 14 and Dark 3

The built-ins are less regular than their names suggest. The Light family alone comes in three shapes — rules above and below with the text in a darker shade (1–7), a filled header over ruled bands (8–14), and a full grid (15–21) — and the Dark 9–11 headers take a colour from outside their own accent. Colours resolve against Office’s default theme (2013 onward) with the same HSL tint curve OOXML uses, landing within one or two RGB units of the Office swatches (Medium 2’s stripe is #DAE3F3 against Office’s #D9E1F2).

An unknown name renders with the default palette but is never renamed — though only the name survives. V5 neither reads nor writes custom style definitions (what Excel keeps in styles.xml as <tableStyles>), so a custom style made in Excel loses its definition when V5 saves the file, and Excel then shows the table unstyled. A palette registered with TableStyles.Register below is the same: only its name reaches the file, and in Excel the table has no styling. For files meant for Excel, use one of the 60 built-ins.

// 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,
});

HeaderFill, RowStripeFill and the rest are shorthands. Underneath, a style is a set of TableStyleElements — Excel’s tableStyleElement: WholeTable, HeaderRow, TotalRow, FirstColumn, LastColumn, FirstRowStripe, SecondRowStripe, FirstColumnStripe, SecondColumnStripe — each with a fill, a text colour, bold, its four outer edges and the rules inside it (Horizontal / Vertical).

// to set line weights and the rules between rows, write it the way Excel's tableStyleElement does
TableStyles.Register(new TableStyleDefinition
{
	Name = "BrandLedger",
	WholeTable = new TableStyleElement
	{
		Horizontal = new BorderEdge(BorderLineStyle.Solid, 0xFFB4C6E7, 1),   // between the rows
		Bottom = new BorderEdge(BorderLineStyle.Solid, 0xFF1F3864, 2),       // the table's bottom edge
	},
	HeaderRow = new TableStyleElement { Fill = 0xFF1F3864, Color = 0xFFFFFFFF, Bold = true },
	FirstRowStripe = new TableStyleElement { Fill = 0xFFEAF0F8 },           // from the first data row
	TotalRow = new TableStyleElement
	{
		Bold = true,
		Top = new BorderEdge(BorderLineStyle.Double, 0xFF1F3864, 3),
	},
});

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

FormatWhat is written
reogrid-jsontables (reogrid-web’s JsonTableDefinition shape)
XLSXxl/tables/tableN.xml + the [Content_Types].xml override + the sheet’s _rels + <tableParts>, numbered workbook-wide

In both formats the table’s colours are not written into the cells. Excel ranks a cell’s own format above the table style, so colours written into the cells would stay put: a sort would carry the stripes off with the rows, and restyling the table would no longer reach them.

For XLSX, three rules keep the file one Excel will open. Break any one of them and Excel refuses the file outright, “repair” included (checked against the desktop Excel):

  • The table’s filter (ShowFilterButton) is written inside the table part, not as the sheet’s <autoFilter> — a sheet filter that overlaps a table stops the file opening. On load, a table’s filter comes back as the sheet’s filter (unless the sheet declared one of its own)
  • The filter range stops above the totals row
  • Whatever the totals row holds is declared on its column: text as totalsRowLabel, a formula or a value as totalsRowFunction="custom" + <totalsRowFormula> (the cell keeps its own formula as well)

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
  • Reading and writing custom table-style definitions (<tableStyles> in styles.xml)
Was this article helpful?