Skip to content

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.

PythonXlsxWriter(), 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.

JavaScriptaddSheet(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().

GoNewXlsxWriter() *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.

Coffice_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). Pass None / "" / null for 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