Create an XLSX Workbook from Scratch
XlsxWriter builds a brand-new Excel .xlsx file in memory and writes it to disk or to a byte buffer — no template, no Excel install, no COM automation. It is the right tool when you want full control over sheets, cells, styling, merges, and column widths rather than mapping through the format-agnostic IR.
The writer is part of the same Rust core that extracts XLSX at 5.0ms mean with a 100% pass rate on valid Office files, and it is exposed in Python, Rust, Go, C#, and JavaScript (native Node).
How do I create an XLSX file from scratch?
Construct an XlsxWriter, add a sheet, write cells by (row, col) (both 0-based), then save. Cell values may be str, int, float, bool, or None. The same shape works across every binding.
Rust
use office_oxide::xlsx::write::{XlsxWriter, CellData, CellStyle, NumberFormat};
fn main() -> office_oxide::Result<()> {
let mut wb = XlsxWriter::new();
let mut sheet = wb.add_sheet("Sales");
// Bold header row
let header = CellStyle::new().bold().background("D3D3D3");
sheet.set_cell_styled(0, 0, CellData::String("Item".into()), header.clone());
sheet.set_cell_styled(0, 1, CellData::String("Amount".into()), header);
// Data rows
sheet.set_cell(1, 0, CellData::String("Widget".into()));
sheet.set_cell(1, 1, CellData::Number(1500.0));
sheet.set_cell(2, 0, CellData::String("Gadget".into()));
sheet.set_cell(2, 1, CellData::Number(2400.0));
// SUM formula with currency formatting (omit the leading '=')
let currency = CellStyle::new().number_format(NumberFormat::Currency);
sheet.set_cell_styled(3, 1, CellData::Formula("SUM(B2:B3)".into()), currency);
// Merge a title banner across two columns
sheet.merge_cells(4, 0, 1, 2);
sheet.set_column_width(0, 20.0);
sheet.set_column_width(1, 15.0);
wb.save("sales.xlsx")?;
Ok(())
}
Python
from office_oxide import XlsxWriter
wb = XlsxWriter()
sheet = wb.add_sheet("Sales") # -> 0 (the sheet index)
# Header row — bold, light-grey fill
wb.set_cell_styled(sheet, 0, 0, "Item", bold=True, bg_color="D3D3D3")
wb.set_cell_styled(sheet, 0, 1, "Amount", bold=True, bg_color="D3D3D3")
# Data rows: row, col are 0-based
wb.set_cell(sheet, 1, 0, "Widget")
wb.set_cell(sheet, 1, 1, 1500.0)
wb.set_cell(sheet, 2, 0, "Gadget")
wb.set_cell(sheet, 2, 1, 2400.0)
# Widen the columns (Excel character units)
wb.set_column_width(sheet, 0, 20.0)
wb.set_column_width(sheet, 1, 15.0)
wb.save("sales.xlsx")
JavaScript
import { writeFileSync } from 'node:fs';
import { XlsxWriter } from 'office-oxide';
const wb = new XlsxWriter();
const sheet = wb.addSheet('Sales'); // 0-based index
// Header row — bold, grey fill (6-char hex string or null)
wb.setCellStyled(sheet, 0, 0, 'Item', true, 'D3D3D3');
wb.setCellStyled(sheet, 0, 1, 'Amount', true, 'D3D3D3');
// Data rows: value may be null, string, number, or boolean
wb.setCell(sheet, 1, 0, 'Widget');
wb.setCell(sheet, 1, 1, 1500.0);
wb.setCell(sheet, 2, 0, 'Gadget');
wb.setCell(sheet, 2, 1, 2400.0);
wb.mergeCells(sheet, 3, 0, 1, 2); // title banner: 1 row x 2 cols
wb.setColumnWidth(sheet, 0, 20.0);
wb.setColumnWidth(sheet, 1, 15.0);
wb.save('sales.xlsx');
// Or export to a Buffer:
const data = wb.toBytes();
writeFileSync('sales-copy.xlsx', data);
wb.close(); // release the native handle
Go
package main
import (
"os"
officeoxide "github.com/yfedoseev/office_oxide/go"
)
func main() {
wb := officeoxide.NewXlsxWriter()
defer wb.Close()
sheet := wb.AddSheet("Sales") // uint32, 0-based index
// Header row — bold with a grey fill
wb.SetCellStyled(sheet, 0, 0, "Item", true, "D3D3D3")
wb.SetCellStyled(sheet, 0, 1, "Amount", true, "D3D3D3")
// Data rows: value may be nil, string, float64, int, or bool
wb.SetCell(sheet, 1, 0, "Widget")
wb.SetCell(sheet, 1, 1, 1500.0)
wb.SetCell(sheet, 2, 0, "Gadget")
wb.SetCell(sheet, 2, 1, 2400.0)
wb.MergeCells(sheet, 3, 0, 1, 2) // title banner: 1 row x 2 cols
wb.SetColumnWidth(sheet, 0, 20.0)
wb.SetColumnWidth(sheet, 1, 15.0)
if err := wb.Save("sales.xlsx"); err != nil {
panic(err)
}
// Or export to bytes:
data, err := wb.ToBytes()
if err != nil {
panic(err)
}
_ = os.WriteFile("sales-copy.xlsx", data, 0o644)
}
C#
using OfficeOxide;
using var wb = new XlsxWriter();
uint sheet = wb.AddSheet("Sales"); // 0-based index
// Header row — bold, grey fill (6-char hex, no '#')
wb.SetCellStyled(sheet, 0, 0, "Item", bold: true, bgColor: "D3D3D3");
wb.SetCellStyled(sheet, 0, 1, "Amount", bold: true, bgColor: "D3D3D3");
// Data rows: value may be null, string, double, int, long, or bool
wb.SetCell(sheet, 1, 0, "Widget");
wb.SetCell(sheet, 1, 1, 1500.0);
wb.SetCell(sheet, 2, 0, "Gadget");
wb.SetCell(sheet, 2, 1, 2400.0);
wb.MergeCells(sheet, 3, 0, 1, 2); // title banner: 1 row x 2 cols
wb.SetColumnWidth(sheet, 0, 20.0);
wb.SetColumnWidth(sheet, 1, 15.0);
wb.Save("sales.xlsx");
// Or export to a byte[]:
byte[] data = wb.ToBytes();
File.WriteAllBytes("sales-copy.xlsx", data);
C
OfficeXlsxWriterHandle *w = office_xlsx_writer_new();
uint32_t s = office_xlsx_writer_add_sheet(w, "Sales"); /* 0-based index */
/* value_type in the writer: EMPTY=0, STRING=1, NUMBER=2 (no BOOLEAN) */
/* header row — bold + 6-char hex bg ("D3D3D3") or NULL */
office_xlsx_sheet_set_cell_styled(w, s, 0, 0, OFFICE_CELL_STRING, "Item", 0.0, true, "D3D3D3");
office_xlsx_sheet_set_cell_styled(w, s, 0, 1, OFFICE_CELL_STRING, "Amount", 0.0, true, "D3D3D3");
/* data rows */
office_xlsx_sheet_set_cell(w, s, 1, 0, OFFICE_CELL_STRING, "Widget", 0.0);
office_xlsx_sheet_set_cell(w, s, 1, 1, OFFICE_CELL_NUMBER, NULL, 1500.0);
office_xlsx_sheet_set_cell(w, s, 2, 0, OFFICE_CELL_STRING, "Gadget", 0.0);
office_xlsx_sheet_set_cell(w, s, 2, 1, OFFICE_CELL_NUMBER, NULL, 2400.0);
office_xlsx_sheet_merge_cells(w, s, 3, 0, 1, 2); /* title banner: row_span/col_span >= 1 */
office_xlsx_sheet_set_column_width(w, s, 0, 20.0); /* Excel char units */
office_xlsx_sheet_set_column_width(w, s, 1, 15.0);
int err = 0;
office_xlsx_writer_save(w, "sales.xlsx", &err);
/* or: uint8_t *b = office_xlsx_writer_to_bytes(w, &out_len, &err); ... office_oxide_free_bytes */
office_xlsx_writer_free(w);
To hand the bytes to a web response or an object store instead of touching disk, use to_bytes() (shown in Python below):
Python
data = wb.to_bytes() # -> bytes (a complete .xlsx ZIP)
with open("sales.xlsx", "wb") as f:
f.write(data)
Method signatures (Python)
| Method | Signature |
|---|---|
| Construct | XlsxWriter() |
| Add sheet | add_sheet(name: str) -> int |
| Set cell | set_cell(sheet: int, row: int, col: int, value: None|str|bool|int|float) -> None |
| Set styled cell | set_cell_styled(sheet: int, row: int, col: int, value, bold: bool, bg_color: str|None = None) -> None |
| Merge | merge_cells(sheet: int, row: int, col: int, row_span: int, col_span: int) -> None |
| Column width | set_column_width(sheet: int, col: int, width: float) -> None |
| Save | save(path) -> None |
| Export | to_bytes() -> bytes |
add_sheet returns the new sheet’s 0-based index — pass it as the sheet argument to every other call. Rows and columns are 0-based, so cell A1 is (row=0, col=0) and B1 is (row=0, col=1).
How do I merge cells and build a title banner?
merge_cells(sheet, row, col, row_span, col_span) merges a rectangular range anchored at (row, col). Both spans must be >= 1; the merged value comes from the anchor cell.
Python
from office_oxide import XlsxWriter
wb = XlsxWriter()
s = wb.add_sheet("Report")
# A title that spans columns A through C of the first row
wb.set_cell_styled(s, 0, 0, "Q3 Revenue Report", bold=True, bg_color="FFE699")
wb.merge_cells(s, 0, 0, 1, 3) # 1 row tall, 3 columns wide
# Sub-header
wb.set_cell_styled(s, 1, 0, "Region", bold=True)
wb.set_cell_styled(s, 1, 1, "Q3", bold=True)
wb.set_cell_styled(s, 1, 2, "QoQ %", bold=True)
wb.set_cell(s, 2, 0, "NA")
wb.set_cell(s, 2, 1, 1_200_000)
wb.set_cell(s, 2, 2, 0.18)
wb.set_column_width(s, 0, 18.0)
wb.save("report.xlsx")
Signatures by language
Rust — the Rust crate exposes a richer builder. add_sheet returns a borrowed SheetData handle, and cells take a CellData value (String, Number, Boolean, Formula, or Empty) plus an optional CellStyle. CellStyle is a builder supporting bold(), italic(), background(), number_format(), align(), and more. Key signatures (on SheetData):
pub fn set_cell(&mut self, row: usize, col: usize, value: CellData) -> &mut Self
pub fn set_cell_styled(&mut self, row: usize, col: usize, value: CellData, style: CellStyle) -> &mut Self
pub fn merge_cells(&mut self, row: usize, col: usize, row_span: usize, col_span: usize) -> &mut Self
pub fn set_column_width(&mut self, col: usize, width: f64) -> &mut Self
and on XlsxWriter: add_sheet(name: &str) -> SheetData<'_>, save(path) -> Result<()>, and write_to<W: Write + Seek>(writer) -> Result<()> for in-memory output. An index-based mirror (sheet_set_cell, sheet_set_cell_styled, sheet_merge_cells, sheet_set_column_width) is also available when you prefer the same flat shape the other bindings use.
Python — XlsxWriter(), add_sheet(name: str) -> int, set_cell(sheet: int, row: int, col: int, value: None|str|bool|int|float) -> None, set_cell_styled(sheet: int, row: int, col: int, value, bold: bool, bg_color: str|None = None) -> None, merge_cells(sheet: int, row: int, col: int, row_span: int, col_span: int) -> None, set_column_width(sheet: int, col: int, width: float) -> None, save(path) -> None, to_bytes() -> bytes.
JavaScript — addSheet(name), setCell(sheet, row, col, value), setCellStyled(sheet, row, col, value, bold, bgColor = null), mergeCells(sheet, row, col, rowSpan, colSpan), setColumnWidth(sheet, col, width), save(path), toBytes(), close().
Go — NewXlsxWriter() *XlsxWriter, AddSheet(name string) uint32, SetCell(sheet, row, col uint32, value any), SetCellStyled(sheet, row, col uint32, value any, bold bool, bgColor string), MergeCells(sheet, row, col, rowSpan, colSpan uint32), SetColumnWidth(sheet, col uint32, width float64), Save(path string) error, ToBytes() ([]byte, error). Pass "" as bgColor for no fill.
C# — AddSheet(string name) -> uint, SetCell(uint sheet, uint row, uint col, object? value), SetCellStyled(uint sheet, uint row, uint col, object? value, bool bold, string? bgColor = null), MergeCells(uint sheet, uint row, uint col, uint rowSpan, uint colSpan), SetColumnWidth(uint sheet, uint col, double width), Save(string path), ToBytes() -> byte[]. XlsxWriter implements IDisposable.
C — office_xlsx_writer_new() -> OfficeXlsxWriterHandle*, office_xlsx_writer_add_sheet(w, name) -> uint32_t, office_xlsx_sheet_set_cell(w, sheet, row, col, value_type, value_str, value_num), office_xlsx_sheet_set_cell_styled(w, sheet, row, col, value_type, value_str, value_num, bold, bg_color), office_xlsx_sheet_merge_cells(w, sheet, row, col, row_span, col_span), office_xlsx_sheet_set_column_width(w, sheet, col, width), office_xlsx_writer_save(w, path, &err), office_xlsx_writer_to_bytes(w, &out_len, &err), office_xlsx_writer_free(w). value_type is OFFICE_CELL_EMPTY (0), OFFICE_CELL_STRING (1), or OFFICE_CELL_NUMBER (2) — the writer has no boolean cell type.
Note: XLSX-writer-from-scratch is exposed in the native bindings (Rust, Python, JavaScript/Node, Go, C#) and the C ABI. The browser WASM build is read-only — it has no writer classes — so build workbooks with one of the above and stream the bytes.
Styling reference
Across the Python, Go, C#, JavaScript, and C bindings, the styled setter exposes two knobs:
bold— a boolean that applies bold font weight to the cell.bg_color/bgColor— a 6-character RGB hex string with no leading#(for example"D3D3D3"for light grey,"FFE699"for amber). PassNone/""/nullfor no fill.
The Rust CellStyle builder is the full surface — italic, underline, font color, font size, font name, number formats (General, Integer, Decimal2, Currency, Percent, Percent2, Date, DateTime), horizontal alignment, and text wrapping. The other bindings expose the two most common options directly; for richer styling from those languages, build via the IR or post-process.
FAQ
Are rows and columns 0-based or 1-based?
Both row and col are 0-based integers. Cell A1 is (row=0, col=0), B1 is (row=0, col=1), and A2 is (row=1, col=0). The sheet index returned by add_sheet is also 0-based.
Do I need Excel or any Microsoft runtime installed?
No. XlsxWriter emits a valid OOXML .xlsx ZIP entirely in Rust. There is no COM automation, no JVM, and no system dependency — the same engine reads XLSX at 5.0ms mean with a 100% pass rate on valid Office files.
How do I get the file as bytes instead of writing to disk?
Call to_bytes() (Python/C#/JS) or ToBytes() (Go), which returns a complete .xlsx byte buffer. In Rust use write_to(writer) with any Write + Seek target such as a Cursor<Vec<u8>>. This is ideal for HTTP responses and object-store uploads.
Can I write formulas?
Yes, in Rust via CellData::Formula("SUM(B2:B3)") (omit the leading =). The flat bindings (Python/Go/C#/JS) currently expose string, number, boolean, and empty cell values through set_cell.
How do I add a second worksheet?
Call add_sheet again — each call appends a sheet and returns the next 0-based index. Pass that index as the sheet argument to subsequent set_cell / merge_cells / set_column_width calls.
See also
- Create documents from IR — one schema, three target formats (DOCX/XLSX/PPTX)
- Edit XLSX cells in place — modify cells in an existing workbook with
EditableDocument - Extract data from XLSX — read a workbook into the structured IR