XlsxComposer
XlsxComposer is a small Ruby library built on top of WriteXLSX that makes the creation of complex and dynamic Excel spreadsheets easier, more readable, and more maintainable through cursor-based navigation and composition.
It does not try to replace WriteXLSX.
Instead, it provides a higher-level abstraction focused on navigation, composition, and formatting, which are usually the hardest parts when spreadsheets stop being static.
Motivation
WriteXLSX does an excellent job at generating .xlsx files.
However, when spreadsheets become dynamic, code tends to become harder to read and maintain:
- manual row and column index management
- lots of hard-coded coordinates
- duplicated formatting logic
- fragile templates that only work for fixed layouts
XlsxComposer introduces a cursor-based approach to spreadsheet writing:
- move through the sheet with
next_row,next_col,go_to, etc. - reuse the same template for variable-length data
- define and reuse formats easily
- compose spreadsheets incrementally instead of calculating coordinates upfront
Installation
Add this line to your application's Gemfile:
gem "xlsx_composer"And then execute:
bundle installBasic usage
require "xlsx_composer"
workbook = XlsxComposer.workbook("example.xlsx")
sheet = XlsxComposer::Worksheet.new(workbook, "Demo")
sheet.go_to("1", "A")
sheet.write_row(["Year", "Revenue"], sheet.bold)
sheet.next_row
sheet.write_row([2024, 1000], sheet.thousands_mark)
sheet.next_row
sheet.write_row([2025, 1250], sheet.thousands_mark)
workbook.closeCursor-based navigation
Instead of calculating coordinates manually, you move a cursor across the sheet.
Available navigation methods
go_to(row, col) # go_to("3", "B")
go_to_row(row)
go_to_col(col)
next_row
previous_row
next_col
previous_col
next_rows(n)
previous_rows(n)
next_cols(n)
previous_cols(n)- Rows are 1-based
- Columns use Excel notation (
A,Z,AA,BA, etc.)
Dynamic templates
The main idea behind XlsxComposer is that the same spreadsheet template can generate different shapes of data.
Example: contracts with variable durations
Imagine a Contract entity that produces yearly results:
- some contracts last 3 years
- others last 5, 10, or more
- the layout is the same, only the number of rows changes
class ContractWorksheet < XlsxComposer::Base
def worksheet_name
"Contract Summary"
end
def run(contract)
write_row(["Year", "Value"], bold)
contract.years.each do |year|
next_row
write_row([year.number, year.value], thousands_mark)
end
end
endThe same template works regardless of how many years the contract has.
Composing a worksheet using multiple templates
For more complex spreadsheets, it is common to split a single worksheet into multiple logical sections, each one handled by a dedicated service or template.
XlsxComposer supports this by allowing multiple composers to write into the same worksheet instance.
Orchestrating a worksheet
The main composer is responsible for creating the worksheet and coordinating the order in which sections are written.
class ReportWorksheet < XlsxComposer::Base
def initialize(workbook, report)
@report = report
super(workbook: workbook)
end
def worksheet_name
"Report"
end
def run
write_header
write_summary
write_details
end
private
attr_reader :report
def write_header
HeaderSection.new(composer_worksheet, report).run
end
def write_summary
SummarySection.new(composer_worksheet, report).run
end
def write_details
DetailsSection.new(composer_worksheet, report).run
end
endWriting a section using an existing worksheet
Each section receives the same XlsxComposer::Worksheet instance and writes
to it using the same cursor-based API.
class HeaderSection < XlsxComposer::Base
def initialize(worksheet, report)
@report = report
super(worksheet: worksheet)
end
def run
go_to("1", "A")
write_row(["Report title"], bold)
next_row
write_row([report.title])
end
private
attr_reader :report
endFormatting
Built-in formats
XlsxComposer ships with a set of common, reusable formats:
bold
bold_center
bold_title
bold_percent
thousands_mark
currency_brl
font_size_ten
font_size_ten_thousands_mark
font_size_ten_currency_brl
font_size_ten_percentFormats are cached per worksheet and reused internally by WriteXLSX.
Usage example:
write_row(["Total"], bold)
write_row([1000], currency_brl)Adding custom formats
You can define additional formats per worksheet class using add_format.
class ReportWorksheet < XlsxComposer::Base
add_format :header, bold: 1, size: 14, align: "center"
add_format :highlight, bold: 1, align: "center", bg_color: "yellow"
def worksheet_name
"Report"
end
def run
write_row(["Report"], header)
next_row
write_row(["Important"], highlight)
end
endDynamic formats
Formats can also be defined using a block:
add_format(:conditional) do
if some_condition?
{ bold: 1, color: "red" }
else
{ bold: 1 }
end
endGlobal vs class-specific formats
-
Global formats live in
XlsxComposer::Formatterand are available everywhere. -
Class-specific formats are defined via
add_formatand scoped to that worksheet composer.
Both are cached on the underlying worksheet and backed by WriteXLSX Format objects.
Why not just WriteXLSX?
You absolutely can — and should — use WriteXLSX directly for simple spreadsheets.
XlsxComposer becomes valuable when:
- spreadsheets are generated from templates
- layouts are dynamic
- rows and columns are added conditionally or iteratively
- formatting logic starts spreading across the codebase
- readability and maintainability matter
Think of XlsxComposer as a composition layer, not a replacement.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/hugopassos/xlsx_composer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the XlsxComposer project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.