Badges and Checkboxes
Status columns and boolean columns almost always end up hand-styled: one color per status value, native checkbox setup 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" writes ordinary styled values. type: "checkbox" writes boolean cells with Excel's native checkbox cell control metadata, so users can toggle the cells in Excel and formulas can consume the resulting TRUE / FALSE values. Both renderers 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.
label can also be a callback. That callback receives the row, the matched badge value, and the schema ctx, which is useful when labels come from an i18n function at export time:
import { createExcelSchema } from "typed-xlsx";
import type { BadgeVariantLabelContext } from "typed-xlsx";
type Product = {
sku: string;
status: "Live" | "Launch";
};
type SchemaContext = {
t: (key: "status.live" | "status.launch") => string;
};
type StatusLabelContext = BadgeVariantLabelContext<Product, Product["status"], SchemaContext>;
const schema = createExcelSchema<Product, SchemaContext>()
.column("sku", { accessor: "sku" })
.column("status", {
type: "badge",
accessor: "status",
variants: {
Live: {
label: ({ ctx }: StatusLabelContext) => ctx.t("status.live"),
style: {
fill: { color: { rgb: "DCFCE7" } },
font: { color: { rgb: "166534" }, bold: true },
},
},
Launch: {
label: ({ ctx }: StatusLabelContext) => ctx.t("status.launch"),
style: {
fill: { color: { rgb: "DBEAFE" } },
font: { color: { rgb: "1D4ED8" }, bold: true },
},
},
},
})
.build();
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 native Excel checkbox cells:
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();
Checked values serialize as boolean TRUE, unchecked values serialize as boolean FALSE, and null / undefined renders as an empty cell. Excel clients that support native cell checkboxes let users toggle the cell directly inside the workbook.
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(true), "Done", "Open"),
})
.column("title", {
accessor: "title",
})
.build();
In Excel this behaves like IF(A2=TRUE, "Done", "Open"). If the user toggles the checkbox in Excel, dependent formulas recalculate from the updated boolean value.
Checkbox support is implemented as native Excel cell-control metadata, not as drawing objects or legacy form controls. Spreadsheet clients that do not understand that metadata may show plain TRUE / FALSE values, but the underlying boolean data remains intact.
Disabled checkbox cells
Checkbox columns do not need a separate disabled option. In Excel, editability comes from sheet protection plus each cell's style.protection.locked value. Unlock the checkbox cells users may toggle, leave the rest locked, and protect the sheet:
import { createExcelSchema, createWorkbook } from "typed-xlsx";
type Task = {
title: string;
done: boolean;
canToggle: boolean;
};
const schema = createExcelSchema<Task>()
.column("done", {
type: "checkbox",
accessor: "done",
style: ({ row }) =>
row.canToggle ? { protection: { locked: false } } : { protection: { locked: true } },
})
.column("title", {
accessor: "title",
})
.build();
const workbook = createWorkbook();
workbook
.sheet("Tasks", {
protection: {
selectLockedCells: false,
selectUnlockedCells: true,
},
})
.table("tasks", {
schema,
rows: [{ title: "Publish launch notes", done: false, canToggle: true }],
});
That supports static locked checkboxes and row- or context-based locked checkboxes at export time. Formula-based disabling is different: Excel conditional formatting can change visual styles, but it cannot toggle locked or hidden, so checkbox editability does not recalculate from formulas after the workbook is opened.
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.