@gavin-lynch/excelwind
v0.1.3
Published
JSX-based Excel generator with Tailwind-style styling
Readme
Excelwind
Excelwind is a declarative, JSX-based Excel generator with Excel formula support, Tailwind-style styling, Row/Column merging, Templating, and more.
It lets you build .xlsx files with a custom JSX runtime, ExcelJS under the hood, and a Tailwind-style className API for styling.
Is is designed for developer-friendly spreadsheet generation, styling, and templating.
What You Get
- Declarative JSX for workbooks, worksheets, rows, cells, groups, images, and templates
- Tailwind-style utility classes via
className - Direct access to formatting, formulas, merges, named ranges, processors, and images
- Template-based workflows that start from existing
.xlsxfiles - A custom JSX runtime with TypeScript/LSP support and no React dependency
- Example workbooks and screenshots that show real output
Table Of Contents
- Installation
- Quick Start
- Why Excelwind
- JSX Runtime Setup
- Core Concepts
- Styling
- Mapped Style Properties
- Formats
- Formulas
- Merges
- Processors
- Templates
- Images
- Components
- Examples
- API Summary
- Validation And Render Contract
- Docs, Tests, And Local Development
- Project Structure
- License
Installation
bun add @gavin-lynch/excelwindQuick Start
/** @jsxImportSource @gavin-lynch/excelwind */
import { writeFile } from 'node:fs/promises';
import { Workbook, Worksheet, Row, Cell, render } from '@gavin-lynch/excelwind';
const workbook = await render(
<Workbook>
<Worksheet name="Sheet1">
<Row>
<Cell value="Hello" className="font-bold" />
<Cell value="World" className="text-right" />
</Row>
</Worksheet>
</Workbook>,
);
await writeFile('hello.xlsx', Buffer.from(await workbook.xlsx.writeBuffer()));Why Excelwind
Excelwind is useful when you want spreadsheet output that is:
- easier to compose than manual ExcelJS row/cell mutation
- easier to style than raw ExcelJS style objects everywhere
- structured like a component tree instead of a giant imperative script
- still capable of advanced Excel features such as merges, formulas, templates, named ranges, and images
The project is especially suited to:
- reports
- exports from application data
- invoices and branded sheets
- dashboards and matrix-like layouts
- spreadsheets where JSX composition is a better fit than direct worksheet mutation
JSX Runtime Setup
Excelwind uses a custom JSX runtime. It is not React.
At the top of your .tsx file, add:
/** @jsxImportSource @gavin-lynch/excelwind */TypeScript should use the automatic JSX runtime style. A typical configuration looks like:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@gavin-lynch/excelwind"
}
}This tells TypeScript to use Excelwind's runtime exports from:
@gavin-lynch/excelwind/jsx-runtime@gavin-lynch/excelwind/jsx-dev-runtime
Excelwind also ships custom JSX type declarations so editors and LSPs understand that your JSX produces Excelwind nodes rather than React elements.
Core Concepts
At render time, Excelwind turns a JSX tree into an ExcelJS.Workbook.
The main authoring model is:
Workbookas the rootWorksheetfor sheetsColumnfor column-wide settingsRowfor row structureCellfor values, formats, formulas, merges, and cell-level imagesGroupfor shared styling, processors, and named rangesImagefor worksheet-level or cell-level image placementTemplatefor importing and expanding existing.xlsxfiles
The render pipeline validates the JSX tree before rendering, then writes the final workbook through ExcelJS.
Styling
className is the canonical styling API.
<Cell value="Total" className="font-bold bg-blue-600 text-white text-right" />For manual conversion, you can also use excelwindClasses():
import { excelwindClasses } from '@gavin-lynch/excelwind';
excelwindClasses('font-bold bg-blue-600 text-white text-right');Supported styling categories
Background colors
Use bg-{color}-{shade} to set a solid fill.
excelwindClasses('bg-blue-600');
excelwindClasses('bg-slate-200');Text colors
Use text-{color}-{shade} to set font.color.
excelwindClasses('text-white');
excelwindClasses('text-emerald-700');Font sizes
| Class | Size (pt) |
| --- | --- |
| text-xs | 10 |
| text-sm | 11 |
| text-base | 12 |
| text-lg | 14 |
| text-xl | 16 |
| text-2xl | 20 |
| text-3xl | 24 |
| text-4xl | 30 |
Font styles
| Class | Effect |
| --- | --- |
| font-bold | font.bold = true |
| font-italic | font.italic = true |
| font-underline | font.underline = true |
Alignment
Horizontal:
text-lefttext-centertext-right
Vertical:
align-topalign-middlealign-centeralign-bottom
Wrapping:
text-wraptext-nowrap
Borders
Borders are composed from multiple class fragments:
- sides:
border,border-t,border-r,border-b,border-l,border-x,border-y - style:
border-thin,border-thick,border-dotted,border-dashed,border-double - color:
border-{color}-{shade}
Examples:
excelwindClasses('border border-gray-300');
excelwindClasses('border-b border-dashed border-amber-600');
excelwindClasses('border-x border-thick');Styling rules and precedence
classNameis preferred over manually writing style objects for common cases- style merging happens in this order: column -> group -> row -> cell
stylestill works and overrides the equivalent values fromclassName- unsupported classes throw an error so typos fail fast
Mapped Style Properties
excelwindClasses() maps only these ExcelJS style fields:
font.sizefont.boldfont.italicfont.underlinefont.colorfill.typefill.patternfill.fgColoralignment.horizontalalignment.verticalalignment.wrapTextborder.{top|right|bottom|left}.styleborder.{top|right|bottom|left}.color
It does not set numFmt. Use the format prop for number and date formatting.
Formats
Number and date formatting are handled with the format prop, not className.
<Column format='"$"#,##0.00' />
<Cell value={new Date()} format="yyyy-mm-dd" />Format precedence
- cell format wins over row, group, and column formats
- row or group formats apply only when a cell does not override them
- column formats provide convenient defaults for entire columns
Formats are written through ExcelJS numFmt and interpreted by Excel when the workbook is opened.
Formulas
Use the formula prop on Cell.
<Cell formula="SUM(B2:B10)" value={1234} />Cached results
- if
valueis also provided, it becomes the cached result Excel can show before recalculation - if
valueis omitted, Excel computes the result when the file is opened
Formula strings are passed through to ExcelJS and Excel; Excelwind does not implement its own formula engine.
Named ranges can make formulas easier to read:
<Column id="Salaries" format='"$"#,##0.00' />
<Cell formula="SUM(Salaries)" format='"$"#,##0.00' />Merges
Excelwind supports merged layouts directly with colSpan and rowSpan on Cell.
<Row>
<Cell value="Quarterly Sales Report 2024" colSpan={5} className="text-center font-bold" />
</Row>
<Row>
<Cell value="Top Performers" rowSpan={2} className="align-center font-bold" />
<Cell value="North America" colSpan={2} />
<Cell value="570,000" colSpan={2} className="text-right" />
</Row>What merges are good for
- title rows that span the full width of a report
- multi-level headers
- summary blocks and dashboard cards
- vertically grouped category labels
Merge placement behavior
colSpanreserves cells to the rightrowSpanreserves the same columns on following rows- later cells are placed into the next available column automatically
- covered cells should not be authored explicitly; Excelwind skips over them while rendering
Merge styling tips
- use borders intentionally if you want a visible grid around merged sections
- use
align-centeroralign-middleon large merged headers - use row or group styles when several merged cells share the same appearance
Processors
Processors let you intercept nodes during render and return modified nodes.
They are useful for:
- zebra striping
- conditional styling
- value-based transformation
- reusable render-time rules that you do not want to repeat in every JSX node
Example:
import { isRow, mergeDeep, type AnyNode, type Processor, type ProcessorContext } from '@gavin-lynch/excelwind';
const zebraStripe: Processor = (node: AnyNode, ctx: ProcessorContext) => {
if (!isRow(node) || ctx.rowIndex === undefined) {
return node;
}
if (ctx.rowIndex % 2 === 1) {
return {
...node,
props: {
...node.props,
style: mergeDeep(node.props.style, {
fill: {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'F3F4F6' },
},
}),
},
};
}
return node;
};
<Group processor={zebraStripe}>{data.map((item) => <Row>{/* ... */}</Row>)}</Group>;Processor context
ProcessorContext provides:
rowIndexcolumnIndexrow
Processors are commonly attached to Group so they apply to a repeated section of the tree.
Templates
Template loads an existing .xlsx file and expands a placeholder row for each data item.
<Template
src="./template.xlsx"
data={{
columns: [
{ id: 'name', names: ['Name'] },
{ id: 'price', names: ['Price'] },
],
rows: [
{ name: 'Widget', price: 10 },
{ name: 'Gadget', price: 20 },
],
}}
/>How template expansion works
- Excelwind finds the header row that matches
columns[].names - the next row becomes the data template row
- that row is duplicated once per object in
data.rows - formulas in the template row are preserved and offset as rows expand
Good use cases for templates
- invoices
- branded forms
- layouts that are easier to design visually in Excel first
- reports where static sheet structure already exists
Current scope and limitation
Template expansion currently focuses on row-based placeholder duplication beneath a matched header row.
That means:
- row formulas are preserved and offset
- row-driven data expansion works
- placeholders elsewhere in the sheet are not automatically replaced yet
This is visible in the template example screenshot later in this README.
Images
Image can be placed directly under Worksheet or nested inside Cell.
Worksheet-level image:
<Worksheet name="Report">
<Image src="./logo.png" extension="png" range="A1:C3" />
</Worksheet>Cell-level image:
<Row>
<Cell value="Logo">
<Image src="./logo.png" extension="png" />
</Cell>
</Row>Positioned image:
<Image
src="./logo.png"
extension="png"
position={{ tl: { col: 0, row: 0 }, ext: { width: 120, height: 48 } }}
/>Image notes
- images can come from
src,Buffer, or base64-backed content - if
positionis omitted for a cell image, Excelwind estimates a default size from row height and column width - worksheet-level images are useful for banners and logos
- cell-level images are useful for catalog rows or record-specific thumbnails
Components
<Workbook>
Root container for every Excelwind tree.
<Workbook>
<Worksheet name="Sheet1">...</Worksheet>
</Workbook>Notes:
- must be the root element
- rendered with
render()
<Worksheet>
Defines a single sheet and its ExcelJS worksheet properties.
<Worksheet name="Sheet1" properties={{ tabColor: { argb: 'FF0000' } }}>
...
</Worksheet>Props:
namerequiredpropertiesoptional
Direct children may be:
ColumnRowGroupTemplate- worksheet-level
Image
<Column>
Defines column-wide settings.
<Column width={20} format='"$"#,##0.00' className="text-right" />
<Column id="StartDates" width={15} format="yyyy-mm-dd" className="text-center" />Props:
widthformatclassNamestyleid
If id is set, Excelwind creates a full-height named range for that column.
<Row>
Groups cells into a single worksheet row.
<Row height={24} className="bg-gray-50">
<Cell value="Hello" className="font-bold" />
</Row>Props:
heightclassNamestyleformatid
If id is set, Excelwind creates a named range for the rendered row.
<Cell>
The atomic unit of worksheet content.
<Cell value="Text" className="text-left" />
<Cell value={123} format='"$"#,##0.00' className="text-right" />
<Cell formula="SUM(A1:A10)" value={1234} />
<Cell value="Merged" colSpan={2} rowSpan={2} className="text-center" />Props:
valueformulaformatclassNamestylecolSpanrowSpanid
Cell can also contain child Image nodes.
<Group>
Container for shared styling, formatting, processors, and named ranges.
<Group className="bg-gray-100" processor={zebraStripe}>
<Row>...</Row>
<Row>...</Row>
</Group>Useful behaviors:
- propagates
classNameandstyleto descendants - can run a processor across rows or cells in the subtree
- can create a named range if
idis set - can be nested
- can appear inside rows to style a subset of cells
If a Group has an id, its named range spans all rows rendered inside that group from column A to the last used column on the sheet.
<Image>
Embeds images into worksheets or cells.
<Image src="./logo.png" extension="png" range="A1:C3" />
<Image
buffer={base64String}
extension="png"
position={{ tl: { col: 0, row: 0 }, ext: { width: 64, height: 64 } }}
tooltip="Company Logo"
/>Common props:
srcbufferextensionrangepositiontooltiphyperlink
<Template>
Imports and expands a template workbook section.
<Template
src="template.xlsx"
data={{
columns: [
{ id: 'name', names: ['Name'] },
{ id: 'price', names: ['Price'] },
],
rows: [
{ name: 'Widget', price: 100 },
],
}}
/>Use Template when sheet layout should begin from an existing Excel file rather than pure JSX.
Examples
All examples write .xlsx files into examples/output/.
Run them all:
Bun is the primary local workflow.
bun run examplesOr individually:
bun run example:basic
bun run example:styling
bun run example:dynamic
bun run example:processors
bun run example:merged
bun run example:templates
bun run example:images
bun run example:complex-merge01. Basic workbook structure
- source:
examples/01-basic.tsx - demonstrates the minimum viable workbook:
Workbook,Worksheet,Column,Row, andCell - useful as the smallest end-to-end rendering example
02. Styling with className
- source:
examples/02-styling.tsx - demonstrates shared header styling, row styling, borders, alignment,
Grouppropagation, and formatted totals - best example for the Tailwind-style styling layer
03. Dynamic rows, formats, formulas, and named ranges
- source:
examples/03-dynamic-data.tsx - demonstrates array-driven rows, date and currency formats, named column ranges, and formulas like
SUM(Salaries) - good model for production export workflows
04. Processors and conditional styling
- source:
examples/04-processors.tsx - demonstrates zebra striping via processors and conditional status styling
- best example for render-time transformation patterns
05. Merged cells and report layouts
- source:
examples/05-merged-cells.tsx - demonstrates practical
colSpanandrowSpanlayouts in a report - shows titles, summary bands, and vertically merged labels
06. Templates and post-template content
- source:
examples/06-templates.tsx - demonstrates importing an invoice template, expanding line-item rows, and appending JSX content below the template
- also shows the current template limitation: non-row placeholders elsewhere in the sheet remain unchanged
07. Worksheet and cell-level images
- source:
examples/07-images.tsx - demonstrates base64-backed images, file-backed images, and positioned images inside rows
- good reference for catalog or branded-sheet workflows
08. Advanced merge stress test
- source:
examples/08-complex-merge.tsx - stresses mixed
rowSpanandcolSpancombinations in one sheet - useful as a merge regression example and layout edge-case reference
Best examples by topic
- styling:
examples/02-styling.tsx - dynamic data + formulas:
examples/03-dynamic-data.tsx - processors:
examples/04-processors.tsx - practical merges:
examples/05-merged-cells.tsx - templates:
examples/06-templates.tsx - images:
examples/07-images.tsx - merge edge cases:
examples/08-complex-merge.tsx
API Summary
Main exports
render(root)-> returns anExcelJS.WorkbookexcelwindClasses(classString)-> returns a partial ExcelJS style object- components:
Workbook,Worksheet,Column,Row,Cell,Group,Image,Template - utilities:
mergeDeep,isRow,isCell,isGroup,isColumn,isImage,isWorksheet,isWorkbook
Public types
ProcessorProcessorContextWorkbookPropsWorksheetPropsColumnPropsRowPropsCellPropsGroupPropsImagePropsTemplateProps
Entry point
Current top-level exports come from src/index.ts:
export * from './types';
export * from './components';
export * from './utils';
export { renderToWorkbook as render } from './renderRows';
export * from './className';Validation And Render Contract
- the JSX tree is validated before render
- invalid parent-child relationships throw early
Workbookmust be the root elementclassNameis the canonical styling prop forColumn,Group,Row, andCellrender()returns anExcelJS.Workbook, so writing the final file still uses ExcelJS methods likeworkbook.xlsx.writeBuffer()
Docs, Tests, And Local Development
Build the library
bun run buildRun examples
bun run examplesRun tests
bun run testLint and format
bun run lint
bun run lint:fix
bun run formatRun docs locally
bun run docs:devBuild docs
bun run docs:buildProject Structure
excelwind/
├── src/
│ ├── index.ts
│ ├── components.tsx
│ ├── renderRows.ts
│ ├── className.ts
│ ├── types.ts
│ ├── utils.ts
│ ├── validate.ts
│ ├── jsx-types.d.ts
│ └── jsx-runtime/
│ ├── jsx-runtime.ts
│ └── jsx-dev-runtime.ts
├── tests/
├── examples/
│ ├── 01-basic.tsx
│ ├── 02-styling.tsx
│ ├── 03-dynamic-data.tsx
│ ├── 04-processors.tsx
│ ├── 05-merged-cells.tsx
│ ├── 06-templates.tsx
│ ├── 07-images.tsx
│ ├── 08-complex-merge.tsx
│ ├── expected/
│ ├── output/
│ └── assets/
├── docs/
└── package.jsonLicense
MIT
