Worksheet Widget
A Flutter widget that brings Excel-like spreadsheet functionality to your app.

Display and edit tabular data with smooth scrolling, pinch-to-zoom, and cell selection - all running at 60fps even with hundreds of thousands of rows.
Try It In 30 Seconds
import 'package:flutter/material.dart';
import 'package:worksheet/worksheet.dart';
void main() => runApp(MaterialApp(home: MySpreadsheet()));
class MySpreadsheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
final data = SparseWorksheetData(rowCount: 100, columnCount: 10, cells: {
(0, 0): 'Name'.cell,
(0, 1): 'Amount'.cell,
(1, 0): 'Apples'.cell,
(1, 1): 42.cell,
(2, 1): '=2+42'.formula,
(3, 1): Cell.text('test'),
});
return Scaffold(
body: WorksheetTheme(
data: const WorksheetThemeData(),
child: Worksheet(
data: data,
rowCount: 100,
columnCount: 10,
),
),
);
}
}
That's it! You now have a scrollable, zoomable spreadsheet with row/column headers.
Add Selection and Editing
Want users to select and edit cells? Add a controller and callbacks:
class EditableSpreadsheet extends StatefulWidget {
@override
State<EditableSpreadsheet> createState() => _EditableSpreadsheetState();
}
class _EditableSpreadsheetState extends State<EditableSpreadsheet> {
final _data = SparseWorksheetData(rowCount: 1000, columnCount: 26);
final _controller = WorksheetController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: WorksheetTheme(
data: const WorksheetThemeData(),
child: Worksheet(
data: _data,
controller: _controller,
rowCount: 1000,
columnCount: 26,
onCellTap: (cell) {
print('Tapped ${cell.toNotation()}'); // "A1", "B5", etc.
},
onEditCell: (cell) {
// Double-tap triggers edit - implement your editor UI
print('Edit ${cell.toNotation()}');
},
),
),
);
}
@override
void dispose() {
_controller.dispose();
_data.dispose();
super.dispose();
}
}
Now you can:
- Click cells to select them
- Use arrow keys to navigate
- Track selection via
_controller.selectedRange - Zoom with pinch gestures or
_controller.setZoom(1.5)
Format Your Numbers
Display values as currency, percentages, dates, and more using Excel-style format codes:
final data = SparseWorksheetData(rowCount: 100, columnCount: 10, cells: {
(0, 0): 'Revenue'.cell,
(0, 1): Cell.number(1234.56, format: CellFormat.currency), // "$1,234.56"
(1, 0): 'Growth'.cell,
(1, 1): Cell.number(0.085, format: CellFormat.percentage), // "9%"
(2, 0): 'Date'.cell,
(2, 1): Cell.date(DateTime(2024, 1, 15), format: CellFormat.dateIso), // "2024-01-15"
(3, 0): 'Precision'.cell,
(3, 1): Cell.number(3.14159, format: CellFormat.scientific), // "3.14E+00"
(4, 0): 'Duration'.cell,
(4, 1): Cell.duration(Duration(hours: 1, minutes: 30), format: CellFormat.duration), // "1:30:00"
});
27 built-in presets cover common formats. For custom codes, create your own:
const custom = CellFormat(type: CellFormatType.number, formatCode: '#,##0.000');
Format with locale-aware separators and currency symbols:
// German locale: period for thousands, comma for decimals
final result = CellFormat.currency.formatRich(
CellValue.number(1234.56),
locale: FormatLocale.deDe,
);
// result.text == "1.234,56 €"
Automatic Type & Format Detection
Type values into cells and they're stored as the right type with the right display format — not plain text:
// These are detected automatically during editing and paste:
// "2025-01-15" → CellValue.date(DateTime(2025, 1, 15)) format: dateIso
// "Jan 15, 2025" → CellValue.date(DateTime(2025, 1, 15)) format: dateShortLong
// "$1,234.56" → CellValue.number(1234.56) format: currency
// "42%" → CellValue.number(0.42) format: percentage
// "1,234" → CellValue.number(1234) format: integer
// "1:30:05" → CellValue.duration(Duration(h:1,m:30,s:5)) format: duration
// "42" → CellValue.number(42) (no format — plain number)
// Configure date format preferences for ambiguous dates:
Worksheet(
data: data,
dateParser: AnyDate.fromLocale('en-US'), // month/day/year
)
AnyDate and DateParserInfo are re-exported from package:worksheet/worksheet.dart.
Preserving the Format You Typed
When you type a value like $1,234.56, 42%, 1/15/2024, or 1:30:05, the cell displays it in the format you typed — not as a raw number or ISO date. The widget auto-detects the format and stores it as a CellFormat:
// Configure locale for currency symbols and ambiguous dates (e.g., 01/02/2024)
Worksheet(
data: data,
formatLocale: FormatLocale.enUs, // US: $ currency, month/day/year (default)
// formatLocale: FormatLocale.enGb, // UK: £ currency, day/month/year
// formatLocale: FormatLocale.deDe, // DE: € currency, comma decimals
)
Supports dates (ISO, US, EU, named months), currencies (locale-aware symbols), percentages, thousands-separated numbers, and durations (H:mm:ss, H:mm).
Rich Text and Cell Merging
Style individual words within a cell and merge cells into regions:
final data = SparseWorksheetData(rowCount: 100, columnCount: 10, cells: {
// Rich text: inline bold + colored text in one cell
(0, 0): Cell.text('Total Revenue', richText: const [
TextSpan(text: 'Total ', style: TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: 'Revenue', style: TextStyle(color: Color(0xFF4472C4))),
]),
// Multi-line text with word wrap
(1, 0): Cell.text('Line 1\nLine 2',
style: const CellStyle(wrapText: true)),
});
// Merge cells A1:D1 into a single wide cell
data.mergeCells(CellRange(0, 0, 0, 3));
Inline editing supports Ctrl+B/I/U for formatting and Alt+Enter for newlines in wrap-enabled cells.
Style Your Data
Add colors, bold text, and conditional formatting:
// Header row styling
const headerStyle = CellStyle(
backgroundColor: Color(0xFF4472C4),
textColor: Color(0xFFFFFFFF),
fontWeight: FontWeight.bold,
textAlignment: CellTextAlignment.center,
);
// Apply to cells
_data.setStyle(const CellCoordinate(0, 0), headerStyle);
_data.setStyle(const CellCoordinate(0, 1), headerStyle);
// Add borders with line styles (solid, dashed, dotted, double)
_data.setStyle(
const CellCoordinate(0, 0),
const CellStyle(
borders: CellBorders(
bottom: BorderStyle(width: 2.0, lineStyle: BorderLineStyle.solid),
),
),
);
// Highlight negative numbers in red
final value = _data.getCell(CellCoordinate(row, col));
if (value != null && value.isNumber && value.asDouble < 0) {
_data.setStyle(
CellCoordinate(row, col),
const CellStyle(textColor: Color(0xFFCC0000)),
);
}
Handle Large Datasets
The widget uses sparse storage and tile-based rendering, so this works smoothly:
// Excel-sized grid: 1 million rows, 16K columns
final data = SparseWorksheetData(
rowCount: 1048576,
columnCount: 16384,
);
// Only populated cells use memory
for (var row = 0; row < 50000; row++) {
data[(row, 0)] = Cell.text('Row ${row + 1}');
}
// Memory usage: ~50K cells, not 17 billion empty cells
Why This Widget?
Built for Performance
- Tile-based rendering: Only visible cells are drawn, cached as GPU textures
- 60fps scrolling: Smooth even with 100K+ populated cells
- 10%-400% zoom: Pinch to zoom with automatic level-of-detail
- O(log n) lookups: Binary search for row/column positions
Built for Real Apps
- Sparse storage: Memory scales with data, not grid size
- Full selection: Single cell, ranges, entire rows/columns
- Cell merging: Merge ranges into single cells with merge-aware rendering
- Rich text: Inline bold, italic, underline, color within a single cell
- Multi-line text: Word wrap with
wrapTextstyle, Alt+Enter for newlines - Keyboard navigation: Arrow keys, Tab, Enter, Home/End, clipboard, and more — fully customizable via Flutter's Shortcuts/Actions
- Automatic type detection: Numbers, booleans, dates, and formulas detected from text input via
CellValue.parse() - Resize support: Drag column/row borders to resize
- Theming: Full control over colors, fonts, headers — built-in light and dark mode presets
Built with Quality
- SOLID principles: Clean separation of concerns
- Test coverage: 87%+ with unit, widget, and performance tests
- TDD workflow: Tests written before implementation
Documentation
| Guide | Description |
|---|---|
| Getting Started | Installation, basic setup, enabling editing |
| Cookbook | Practical recipes for common tasks |
| Performance | Tile cache tuning, large dataset strategies |
| Theming | Colors, fonts, headers, selection styles |
| Testing | Unit tests, widget tests, benchmarks |
| API Reference | Quick reference for all classes and methods |
| Architecture | Deep dive into the rendering pipeline |
Installation
Add to your pubspec.yaml:
dependencies:
worksheet: ^2.2.0
Then run:
flutter pub get
Keyboard Shortcuts
All shortcuts work out of the box. You can override or extend them via the shortcuts and actions parameters.
| Key | Action |
|---|---|
| Arrow keys | Move selection |
| Shift + Arrow | Extend selection |
| Tab / Shift+Tab | Move right/left |
| Enter / Shift+Enter | Move down/up |
| Home / End | Start/end of row |
| Ctrl+Home / Ctrl+End | Go to A1 / last cell |
| Page Up / Page Down | Move up/down by 10 rows |
| F2 | Edit current cell |
| Escape | Collapse range to single cell |
| Ctrl+A | Select all |
| Ctrl+C / Ctrl+X / Ctrl+V | Copy / Cut / Paste |
| Ctrl+D / Ctrl+R | Fill down / Fill right |
| Alt+Enter | Insert newline (when cell has wrapText) |
| Delete / Backspace | Clear selected cells |
| Ctrl+\ | Clear formatting (keep values) |
Customizing Shortcuts
Worksheet(
data: data,
// Override: make Enter do nothing
shortcuts: {
const SingleActivator(LogicalKeyboardKey.enter): const DoNothingAndStopPropagationIntent(),
},
// Override: custom action for Delete
actions: {
ClearCellsIntent: CallbackAction<ClearCellsIntent>(
onInvoke: (_) { print('Custom delete!'); return null; },
),
},
)
See DefaultWorksheetShortcuts.shortcuts for the full list of default bindings.
Quick API Overview
// Data - map literal construction with record coordinates
final data = SparseWorksheetData(
rowCount: 1000,
columnCount: 26,
cells: {
(0, 0): 'Hello'.cell,
(0, 1): 42.cell,
},
);
// Bracket access with (row, col) records
data[(1, 0)] = 'World'.cell;
data[(1, 1)] = Cell.number(99, style: const CellStyle(fontWeight: FontWeight.bold));
final cell = data[(0, 0)]; // Cell(value: 'Hello', style: null)
// Extensions for quick cell creation
'Hello'.cell // Cell with text value
42.cell // Cell with numeric value
true.cell // Cell with boolean value
DateTime.now().cell // Cell with date value
Duration(hours: 1).cell // Cell with duration value
'=SUM(A1:A10)'.formula // Cell with formula
// Cell constructors for full control (when you need style or format)
Cell.text('Hello', style: headerStyle)
Cell.number(42.5, format: CellFormat.currency)
Cell.boolean(true)
Cell.date(DateTime.now(), format: CellFormat.dateIso)
Cell.duration(Duration(hours: 1, minutes: 30), format: CellFormat.duration)
Cell.withStyle(headerStyle) // style only, no value
// Controller
final controller = WorksheetController();
controller.selectCell(const CellCoordinate(5, 3));
controller.selectRange(CellRange(0, 0, 10, 5));
controller.setZoom(1.5); // 150%
controller.scrollTo(x: 500, y: 1000, animate: true);
Running the Example
cd example
flutter run
The example app demonstrates:
- 50,000 rows of sample sales data
- Cell editing with double-tap
- Column/row resizing
- Zoom slider (10%-400%)
- Keyboard navigation
Running Tests
flutter test # Run all tests
flutter test --coverage # With coverage report
flutter test test/core/ # Run specific directory
License
MIT License - see LICENSE for details.
Libraries
- worksheet
- High-performance Flutter worksheet widget with Excel-like functionality.