Badges and Checkboxes
Status columns and boolean columns almost always end up hand-styled: one color per status value, a ✓/✗ mapping for booleans, repeated in every export that touches the field. Badge and checkbox columns move that mapping into the schema, next to the column it belongs to.
type: "badge" and type: "checkbox" write ordinary cell values with generated styles — no embedded objects, no compatibility surprises. They work in buffered and streaming exports, including report-mode sub-row expansion.
Badge columns
Badge columns are useful for status, tier, priority, and workflow-state fields:
import { createExcelSchema } from "typed-xlsx";
type Product = {
sku: string;
status: "Live" | "Low stock" | "Launch";
};
const schema = createExcelSchema<Product>()
.column("sku", {
accessor: "sku",
})
.column("status", {
type: "badge",
accessor: "status",
variants: {
Live: {
style: {
fill: { color: { rgb: "DCFCE7" } },
font: { color: { rgb: "166534" }, bold: true },
},
},
"Low stock": {
style: {
fill: { color: { rgb: "FEF3C7" } },
font: { color: { rgb: "92400E" }, bold: true },
},
},
Launch: {
label: "New",
style: {
fill: { color: { rgb: "DBEAFE" } },
font: { color: { rgb: "1D4ED8" }, bold: true },
},
},
},
})
.build();
Each variants key is matched against String(value). A variant can be a full CellStyle, or { label, style } when the displayed text should differ from the source value — above, Launch rows display as New.
When the accessor value is a literal union, variant keys are type-checked against that union. Without defaultVariant, every union member must have a variant. With defaultVariant, variants can cover only the special cases and the fallback handles the rest:
import { createExcelSchema } from "typed-xlsx";
type Ticket = {
status: "New" | "In progress" | "Blocked" | "Done";
};
const schema = createExcelSchema<Ticket>()
.column("status", {
type: "badge",
accessor: "status",
variants: {
Blocked: {
style: {
fill: { color: { rgb: "FEE2E2" } },
font: { color: { rgb: "991B1B" }, bold: true },
},
},
Done: {
style: {
fill: { color: { rgb: "DCFCE7" } },
font: { color: { rgb: "166534" }, bold: true },
},
},
},
defaultVariant: {
style: {
fill: { color: { rgb: "F1F5F9" } },
font: { color: { rgb: "475569" } },
},
},
})
.build();
Fallback styling
When the source field is an open string (statuses coming from an API, user-defined tags), unknown values would render unstyled. Use defaultVariant to give them one shared fallback:
import { createExcelSchema } from "typed-xlsx";
type Ticket = {
id: string;
priority: string; // open set — new priorities may appear
};
const schema = createExcelSchema<Ticket>()
.column("id", { accessor: "id" })
.column("priority", {
type: "badge",
accessor: "priority",
variants: {
Critical: {
style: {
fill: { color: { rgb: "FEE2E2" } },
font: { color: { rgb: "991B1B" }, bold: true },
},
},
High: {
style: {
fill: { color: { rgb: "FEF3C7" } },
font: { color: { rgb: "92400E" }, bold: true },
},
},
},
defaultVariant: {
style: {
fill: { color: { rgb: "F1F5F9" } },
font: { color: { rgb: "475569" } },
},
},
})
.build();
Known priorities get their declared colors; anything else renders with the neutral fallback instead of plain text.
Checkbox columns
Checkbox columns render boolean values as centered checkbox glyphs while keeping the underlying cell value formula-friendly:
import { createExcelSchema } from "typed-xlsx";
type Task = {
title: string;
done: boolean;
};
const schema = createExcelSchema<Task>()
.column("done", {
type: "checkbox",
accessor: "done",
})
.column("title", {
accessor: "title",
})
.build();
By default, checked values render as ☑, unchecked values render as ☐, and null or
undefined renders as an empty cell. Under the hood, checked cells store 1 and unchecked cells
store 0; the glyphs come from the generated cell format.
That means later formula columns can reference checkbox columns directly:
import { createExcelSchema } from "typed-xlsx";
type Task = {
title: string;
done: boolean;
};
const schema = createExcelSchema<Task>()
.column("done", {
type: "checkbox",
accessor: "done",
})
.column("state", {
formula: ({ refs, fx }) => fx.if(refs.column("done").eq(1), "Done", "Open"),
})
.column("title", {
accessor: "title",
})
.build();
In Excel this behaves like IF(A2=1, "Done", "Open"); SUM, COUNTIF, and comparisons also
work because the cell stores numeric truthy/falsey values instead of literal glyph text.
Override the labels when a workbook needs different display text:
import { createExcelSchema } from "typed-xlsx";
type Task = {
title: string;
done: boolean | null;
};
const schema = createExcelSchema<Task>()
.column("done", {
type: "checkbox",
accessor: "done",
checkedLabel: "Yes",
uncheckedLabel: "No",
emptyLabel: "N/A",
})
.column("title", {
accessor: "title",
})
.build();
Checkbox columns are display renderers, not interactive Excel form controls. They produce portable numeric cell values and styles, so unsupported spreadsheet clients still preserve the truthy/falsey data even if they ignore the display format.
Sub-row Expansion
Both renderers support array-valued accessors in report mode:
import { createExcelSchema } from "typed-xlsx";
type Order = {
orderId: string;
lines: Array<{ sku: string; shipped: boolean }>;
};
const schema = createExcelSchema<Order>()
.column("orderId", {
accessor: "orderId",
})
.column("sku", {
accessor: (row) => row.lines.map((line) => line.sku),
})
.column("shipped", {
type: "checkbox",
accessor: (row) => row.lines.map((line) => line.shipped),
})
.build();
The checkbox values expand alongside the other array-valued columns.