@composize/excel is a DSL for creating Excel files through a composable, declarative API.
It provides an intuitive interface to create complex Excel workbooks without directly dealing with the complexities of the underlying ExcelJS library.
npm install @composize/excel
Here's a simple example of how to create an Excel file using the DSL:
import { cell, row, workbook } from '@composize/excel';
workbook(() => {
  row(() => {
    cell('title1');
    cell('title2');
  });
  row(() => {
    cell('value1');
    cell('value2');
  });
});
The @composize/excel DSL follows a hierarchical structure that mirrors Excel's document model, with composable functions representing each level of the hierarchy.
graph LR
    workbook --> worksheet
    worksheet --> row
    row --> cell
    row --> borderedCell
    row --> centeredCell
    advanceThe @composize/excel DSL uses a functional composition pattern where parent elements accept callback functions that define their children. This creates a natural nesting structure that makes the code's visual layout reflect the structure of the resulting Excel document.
The @composize/excel DSL includes the following key features:
| Function | Description | Parameters | Returns | 
|---|---|---|---|
| workbook() | Creates a new workbook | composable: (it: Workbook) => void | Workbook | 
| worksheet() | Adds a new worksheet to the current workbook | name: string, composable: (it: Worksheet) => void | Worksheet | 
| row() | Adds a new row to the current worksheet | composable: (it: Row) => void | Row | 
| cell() | Adds a cell to the current row | value: string, options?: CellOptions | Cell | 
| borderedCell() | Adds a cell with thin borders to the current row | value: string, options?: CellOptions | Cell | 
| centeredCell() | Adds a centered cell to the current row | value: string, options?: CellOptions | Cell | 
| fillSolid() | Creates a solid fill style for cells | fgColor: string, bgColor?: string | FillPattern | 
| advance() | Advance to row or column | delta?: number | void | 
The cell() functions accept an optional options parameter that allows for cell styling and merging:
| Option | Type | Description | 
|---|---|---|
| colSpan | number | Number of columns to merge | 
| rowSpan | number | Number of rows to merge | 
| numFmt | string | Number format string | 
| font | Font | Font properties (size, name, color, etc.) | 
| alignment | Alignment | Text alignment properties | 
| border | Borders | Cell border properties | 
| fill | Fill | Cell background fill properties | 
| protection | Protection | Cell protection properties | 
The simplest way to create an Excel file is by nesting function calls that mirror the structure of your desired spreadsheet:
import { workbook, row, cell } from '@composize/excel';
const book = workbook(() => {
  row(() => {
    cell('Title 1');
    cell('Title 2');
  });
  row(() => {
    cell('Value 1');
    cell('Value 2');
  });
});
// Save the workbook to a file
book.xlsx.writeFile('./example.xlsx');
This creates a simple 2x2 table with headers and values.
By default, if no worksheet is explicitly created, the DSL automatically creates a worksheet named "Sheet1":
const book = workbook(() => {
  row(() => {
    cell('Data without explicit worksheet');
  });
});
You can explicitly create named worksheets using the worksheet() function:
const book = workbook(() => {
  worksheet('First Sheet', () => {
    row(() => {
      cell('Data in first sheet');
    });
  });
  worksheet('Second Sheet', () => {
    row(() => {
      cell('Data in second sheet');
    });
  });
});
The @composize/excel DSL supports merging cells both horizontally (using colSpan) and vertically (using rowSpan):
To merge cells horizontally, use the colSpan option:
workbook(() => {
  row(() => {
    cell('Wide Header', { colSpan: 3 });
  });
  row(() => {
    cell('Data 1');
    cell('Data 2');
    cell('Data 3');
  });
});
This creates a header cell that spans across three columns:
+--------+--------+--------+
|       Wide Header        |
+--------+--------+--------+
| Data 1 | Data 2 | Data 3 |
+--------+--------+--------+
To merge cells vertically, use the rowSpan option:
workbook(() => {
  row(() => {
    cell('Tall Cell', { rowSpan: 2 });
    cell('Top Cell');
  });
  row(() => {
    // No need to set the first cell, it's already covered by the merged cell
    cell('Bottom Cell');
  });
});
This creates a cell that spans two rows in the first column:
+--------+--------+-------------+
|                 | Top Cell    |
|    Tall Cell    +-------------+
|                 | Bottom Cell |
+--------+--------+-------------+
You can combine both horizontal and vertical merging:
workbook(() => {
  row(() => {
    cell('Header 1', { rowSpan: 2, colSpan: 2 });
    cell('Header 2');
  });
  row(() => {
    cell('Header 3');
  });
  row(() => {
    cell('Data 1');
    cell('Data 2', { rowSpan: 2, colSpan: 2 });
  })
  row(() => {
    cell('Data 3');
  })
});
This creates a complex merged structure:
+--------+--------+----------+
|                 | Header 2 |
|     Header 1    +----------+
|                 | Header 3 |
+--------+--------+----------+
| Data 1 |                   |
+--------+      Data 2       +
| Data 3 |                   |
+--------+-------------------+
The advance() function allows you to move the current position by a specified number of rows or columns. By default, it advances by one row or column:
row(() => {
  cell('Cell 1');
  advance();   // → Move back one column
  cell('Cell 3');
});
advance();     // ↓ Move down two rows
row(() => {
  cell('Cell 1');
  advance(2);  // → Move back one column
  cell('Cell 4');
  advance(-3); // ← Move forward three column
  cell('Cell 2');
});
This creates the following layout:
+--------+--------+--------+--------+
| Cell 1 |        | Cell 3 |        |
+--------+--------+--------+--------+
|        |        |        |        |
+--------+--------+--------+--------+
| Cell 1 | Cell 2 |        | Cell 4 |
+--------+--------+--------+--------+
The cell() function accepts styling options from ExcelJS's Cell object:
cell('Styled Text', {
  font: { bold: true, size: 14 },
  alignment: { vertical: 'middle', horizontal: 'center' },
});
You can apply background fills to cells using the fill option. For example, to apply a solid fill:
cell('Solid Fill', {
  fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: Color.Red } }
});
You can create fill patterns using the fillSolid() helper function:
cell('Solid Fill', {
  fill: fillSolid(Color.Red)
});
For convenience, the DSL provides a borderedCell() function that applies thin borders to all sides of a cell:
row(() => {
  borderedCell('Bordered Content');
  borderedCell('Custom Format', { numFmt: '0.00' });
});
The borderedCell() function accepts the same options as cell(), but automatically adds thin borders.
For convenience, the DSL also provides a centeredCell() function, which builds on top of borderedCell() and applies centered alignment to the cell.
row(() => {
  centeredCell('Centered Text');
  centeredCell('Custom Format', { numFmt: '0.00' });
});
The centeredCell() function accepts the same options as borderedCell(), but automatically adds centered alignment.
You can protect cells from editing using the protection option:
cell('Protected Cell', {
  protection: { locked: true }
});
The @composize/excel DSL automatically adjusts column widths based on the content of each cell. For merged cells, auto-fitting enables text wrapping instead of increasing width.
Predefined ARGB color values for Excel styling:
| Constant | Value | Color | 
|---|---|---|
| Color.DarkRed | ffc00000 | #c00000 | 
| Color.Red | ffff0000 | #ff0000 | 
| Color.Orange | ffffc000 | #ffc000 | 
| Color.Yellow | ffffff00 | #ffff00 | 
| Color.LightGreen | ff92d050 | #92d050 | 
| Color.Green | ff00b050 | #00b050 | 
| Color.LightBlue | ff00b0f0 | #00b0f0 | 
| Color.Blue | ff0070c0 | #0070c0 | 
| Color.DarkBlue | ff002060 | #002060 | 
| Color.Purple | ff7030a0 | #7030a0 | 
Standardized font sizes organized by usage context:
| Category | Constant | Value | 
|---|---|---|
| Caption | FontSize.Caption | 8 | 
| Footnote | FontSize.Footnote | 9 | 
| Body Text | FontSize.BodySmall | 10 | 
| FontSize.Body | 11 | |
| FontSize.BodyLarge | 12 | |
| Subheading | FontSize.Subheading | 14 | 
| Titles | FontSize.TitleSmall | 16 | 
| FontSize.Title | 18 | |
| FontSize.TitleLarge | 20 | |
| Headings | FontSize.HeadingSmall | 22 | 
| FontSize.Heading | 24 | |
| FontSize.HeadingLarge | 26 | |
| Display | FontSize.DisplaySmall | 28 | 
| FontSize.Display | 36 | |
| FontSize.DisplayLarge | 48 | |
| FontSize.DisplayXL | 72 | 
Here's a more complex example that demonstrates many features of the @composize/excel DSL:
const headers = ['Category', 'Q1', 'Q2', 'Q3'];
const data = [
  { category: 'Revenue', q1: 10000, q2: 12000, q3: 15000 },
  { category: 'Expenses', q1: 8000, q2: 8500, q3: 9000 },
  { category: 'Profit', q1: 2000, q2: 3500, q3: 6000 }
];
const book = workbook(() => {
  worksheet('Report', () => {
    // Header row with merged title
    row(() => {
      centeredCell('Quarterly Report', {
        colSpan: headers.length,
        font: { bold: true, size: FontSize.TitleSmall }
      });
    });
    // Column headers
    row(() => {
      for (const header of headers) {
        borderedCell(header, { font: { bold: true } });
      }
    });
    // Data rows
    for (const item of data) {
      row(() => {
        borderedCell(item.category);
        borderedCell(item.q1, { numFmt: '$#,##0' });
        borderedCell(item.q2, { numFmt: '$#,##0' });
        borderedCell(item.q3, { numFmt: '$#,##0' });
      });
    }
  });
});
book.xlsx.writeFile('quarterly_report.xlsx');
Learn about the latest improvements.
Do you love ✨ composize ✨ ? Star for this project!
Thanks to JetBrains for supporting us free open source licenses.