typed-xlsx
Styling

Themes

Package a visual system into theme slots, apply it to a schema or a table, and know which style wins when layers overlap.

A theme is a named set of styles for the parts every report has: the title, headers, group headers, summary rows and body cells. Define it once, apply it to a schema or to a single table, and columns stop repeating the same CellStyle objects.

How styles stack

Every cell's style is merged from up to six layers. A higher layer overrides the properties it sets and keeps the rest from below.

  1. wins
  2. 1Conditional stylesconditionalStyleExcel applies it on top whenever the rule is true
  3. 2Column stylestyle, headerStylestatic, or computed per row with ({ row, ctx }) =>
  4. 3Table defaultstable(…, { defaults })per-export overrides of any slot
  5. 4Table themetable(…, { theme })one workbook, several looks
  6. 5Schema theme.theme(theme)the report's default look
  7. 6Library defaultsbuilt inthin borders, bold headers
  8. fallback

The pages for each layer: Cell Styles, Dynamic Styles, Conditional Styles.

Start from a preset

spreadsheetThemes ships five ready themes: classic (the default), slate, forest, rose and ocean.

import { 
createExcelSchema
,
spreadsheetThemes
} from "typed-xlsx";
const
schema
=
createExcelSchema
<{
owner
: string;
amount
: number }>()
.
theme
(
spreadsheetThemes
.
forest
)
.
column
("owner", {
accessor
: "owner" })
.
column
("amount", {
accessor
: "amount" })
.
build
();

Change the tokens

Every theme is built from tokens: a palette (titleFill, headerFill, headerText, summaryFill, inputFill, hyperlink, the sparkline colors and more) and two border styles. Overriding a token recolors every slot that uses it, so a brand theme is often just a few colors.

import { 
defineSpreadsheetTheme
} from "typed-xlsx";
const
brand
=
defineSpreadsheetTheme
({
tokens
: {
colors
: {
titleFill
: "0E3B26",
headerFill
: "17603D",
headerText
: "FFFFFF",
summaryFill
: "E7F4EC",
}, }, });

Override slots

Slots are the styled parts of a table: title, groupHeader, groupHeaderFiller, header, summary, and the body states cellBase, cellLocked, cellUnlocked, cellHidden and hyperlink. A slot takes a CellStyle, or a function of the tokens when it should follow the palette. .extend() derives a variant that changes only what it lists.

import { 
createExcelSchema
,
defineSpreadsheetTheme
,
createWorkbook
} from "typed-xlsx";
type
Deal
= {
amount
: number;
owner
: string };
const
boardTheme
=
defineSpreadsheetTheme
({
tokens
: {
colors
: {
headerFill
: "0B1220",
headerText
: "F8FAFC" } },
slots
: {
title
: ({
tokens
}) => ({
fill
: {
color
: {
rgb
:
tokens
.
colors
.
headerFill
} },
font
: {
color
: {
rgb
:
tokens
.
colors
.
headerText
},
bold
: true,
size
: 14 },
}),
summary
: {
font
: {
bold
: true } },
}, }); const
watchlistTheme
=
boardTheme
.
extend
({
slots
: {
title
: {
fill
: {
color
: {
rgb
: "450A0A" } } },
groupHeader
: {
fill
: {
color
: {
rgb
: "FECACA" } },
font
: {
color
: {
rgb
: "9F1239" } } },
}, }); const
schema
=
createExcelSchema
<
Deal
>()
.
theme
(
boardTheme
)
.
column
("owner", {
accessor
: "owner" })
.
column
("amount", {
accessor
: "amount" })
.
build
();
createWorkbook
().
sheet
("Board").
table
("deals", {
title
: "Executive Watchlist",
rows
: [],
schema
,
theme
:
watchlistTheme
,
});

The watchlist keeps the board's header and summary and only changes the title and group header.

Tokens are read when slots are resolved. defineSpreadsheetTheme({ tokens }) recolors every default slot, but tokens passed to .extend() only reach the slots passed in that same call. To recolor a whole theme, define a new one from tokens.

Reuse a slot anywhere

theme.slot(name) returns the slot's CellStyle, so a column can borrow it. Pass a second argument to adjust it.

import { 
defineSpreadsheetTheme
} from "typed-xlsx";
const
theme
=
defineSpreadsheetTheme
();
theme
.
slot
("header");
theme
.
slot
("header", {
alignment
: {
horizontal
: "left" } });

Pivot slots

Themes also style pivot tables through pivotBody, pivotFrame, pivotHeader, pivotSubheading, pivotTotal and pivotStripe. Each is bound to a table slot by default, such as pivotHeader to header, so a pivot matches the tables of its theme, and keeps following them when you extend the theme. Pivot slots take colours, font styles, fills and borders, which is what an Excel pivot style can change.

Schema theme or table theme

Put the theme on the schema when the report should always look the same. Put it on the table when one workbook shows the same schema in several looks, for example a neutral sheet and a red watchlist.

import { 
createExcelSchema
,
defineSpreadsheetTheme
,
createWorkbook
} from "typed-xlsx";
type
Deal
= {
amount
: number;
owner
: string };
const
baseTheme
=
defineSpreadsheetTheme
({
slots
: {
header
: {
font
: {
bold
: true,
color
: {
rgb
: "F8FAFC" } },
fill
: {
color
: {
rgb
: "1E293B" } },
}, }, }); const
warmTheme
=
baseTheme
.
extend
({
slots
: {
header
: {
fill
: {
color
: {
rgb
: "7C2D12" } } } },
}); const
schema
=
createExcelSchema
<
Deal
>()
.
theme
(
baseTheme
)
.
column
("owner", {
accessor
: "owner" })
.
column
("amount", {
accessor
: "amount" })
.
build
();
const
workbook
=
createWorkbook
();
workbook
.
sheet
("Board").
table
("deals", {
rows
: [],
schema
});
workbook .
sheet
("Watchlist")
.
table
("deals-watch", {
rows
: [],
schema
,
theme
:
warmTheme
});

Themes don't protect cells

Themes and conditional styles are visual. Locking and hiding cells is set through style.protection on the column and only takes effect on a protected sheet. See Protection.

Copyright © 2026 Cyprien Thao. Released under the MIT License.