Canvas-Sheet
A lightweight, high-performance spreadsheet component built on the HTML5 Canvas API for modern web applications. Unlike other canvas-based spreadsheet libraries, Canvas-Sheet uses a schema-based approach that gives you strong typing, validation, and custom editors for each data type.
Demo: https://admin-remix.github.io/canvas-sheet
Features
- Schema-based data model with typed columns, validation, and field-specific configuration
- Canvas-based rendering for superior performance with large datasets
- Virtual scrolling to efficiently handle thousands of rows
- Multiple data types including text, number, date, boolean, and select/dropdown
- Cell editing with type-specific editors
- Selection and range operations (select cells, rows, copy/paste)
- Keyboard navigation for efficient data entry
- Resizable rows and columns
- Customizable styling with numerous appearance options
- Zero dependencies - pure JavaScript implementation
Installation
npm install canvas-sheet
Basic Usage
<div id="spreadsheet-container" style="width: 100%; height: 500px;"></div>
import { Spreadsheet } from "canvas-sheet";
import "canvas-sheet/dist/spreadsheet.css";
const schema = {
id: { type: "number", decimal: false, label: "ID" },
name: {
type: "text",
required: true,
maxlength: 50,
label: "Full Name",
},
email: {
type: "email",
required: true,
label: "Email Address",
},
isActive: { type: "boolean", label: "Active" },
};
const data = [
{ id: 1, name: "John Doe", email: "john@example.com", isActive: true },
{ id: 2, name: "Jane Smith", email: "jane@example.com", isActive: false },
{ id: 3, name: "Bob Johnson", email: "bob@example.com", isActive: true },
];
const spreadsheet = new Spreadsheet(
"spreadsheet-container",
schema,
data,
{
headerHeight: 40,
defaultRowHeight: 36,
font: "14px Arial",
}
);
Advanced Examples
Using Dropdown/Select Fields
You can dynamically control which cells are disabled based on row data
const schema = {
status: {
type: "select",
label: "Status",
values: [
{ id: 1, name: "Active" },
{ id: 2, name: "Pending" },
{ id: 3, name: "Inactive" },
],
disabled: (rowData, rowIndex) => {
console.log("Row index", rowIndex);
return rowData.isRestricted && rowData.locationId === 1;
},
},
};
Dynamic Data Updates
const currentData = spreadsheet.getData();
spreadsheet.setData(newData);
spreadsheet.updateCell(rowIndex, "fieldName", newValue);
Cell Update and Selection Callbacks
You can implement custom logic when cells are updated, including adding loading states and validation:
const spreadsheet = new Spreadsheet("spreadsheet-container", schema, data, {
onCellsUpdate: (rows: CellUpdateEvent[]) => {
for (const row of rows) {
if (
row.columnKeys.includes("email") &&
row.data.email &&
row.data.email.endsWith("@sample.net")
) {
spreadsheet.updateCell(row.rowIndex, "loading:email", true);
setTimeout(() => {
spreadsheet?.updateCells([
{ rowIndex: row.rowIndex, colKey: "loading:email", value: null },
{
rowIndex: row.rowIndex,
colKey: "error:email",
value: `Account ${row.data.email} does not exist`,
},
]);
}, 2000);
}
}
},
onCellSelected: ({ rowIndex, colKey, rowData }: CellEvent) => {
console.log("Selected", rowIndex, colKey, rowData[colKey]);
},
});
Custom Date Picker Support
The native date picker is used by default. Enable the customDatePicker
option which will trigger the onEditorOpen
callback each time a date field is opened. When user selects a date from your custom date picker, call the setValueFromCustomEditor
method to set the value of the cell which restores focus to the spreadsheet automatically.
import { CellEventWithBounds, Spreadsheet } from "canvas-sheet";
let spreadsheet: Spreadsheet | null = null;
let selectedCellForEditor: CellEventWithBounds | null = null;
function openDatePicker(event: CellEventWithBounds) {
selectedCellForEditor = { ...event };
const selectedDate = event.rowData[event.colKey];
const positionX = event.bounds.x;
const positionY = event.bounds.y;
const width = event.bounds.width;
const height = event.bounds.height;
console.log("open custom date picker", selectedDate, "at", {
positionX,
positionY,
width,
height,
});
}
function closeDatePicker(value: string) {
spreadsheet?.setValueFromCustomEditor(
selectedCellForEditor.rowIndex,
selectedCellForEditor.colKey,
value
);
selectedCellForEditor = null;
}
spreadsheet = new Spreadsheet("spreadsheet-container", schema, data, {
customDatePicker: true,
onEditorOpen: (event: CellEventWithBounds) => {
openDatePicker(event);
},
});
Configuration Options
Canvas-Sheet is highly customizable with many options:
const options = {
defaultColumnWidth: 120,
defaultRowHeight: 30,
minColumnWidth: 50,
maxColumnWidth: 500,
minRowHeight: 25,
maxRowHeight: 100,
headerHeight: 36,
rowNumberWidth: 50,
font: '14px Arial',
headerFont: 'bold 14px Arial',
textColor: '#333',
cellBgColor: '#ffffff',
activeCellBgColor: '#edf3ff',
selectedRowBgColor: '#f5f8ff',
selectedRangeBgColor: '#e8f0ff',
headerTextColor: '#333',
headerBgColor: '#f5f5f5',
gridLineColor: '#e0e0e0',
textAlign: 'left',
padding: 8,
verbose: false
customDatePicker: false,
onEditorOpen: (event: CellEventWithBounds) => void,
onColumnDelete: (colIndex: number, schema: ColumnSchema) => void,
onRowDeleted: (rows: DataRow[]) => void,
onCellSelected: (event: CellEvent) => void,
onCellsUpdate: (rows: CellUpdateEvent[]) => void,
};
Column Schema Options
Each column can have type-specific configuration:
const schema = {
name: {
type: "text",
required: true,
maxlength: 50,
label: "Name",
},
amount: {
type: "number",
decimal: true,
label: "Amount",
},
isActive: {
type: "boolean",
label: "Active",
},
createdAt: {
type: "date",
label: "Created Date",
},
status: {
type: "select",
label: "Status",
values: [
{ id: 1, name: "Active" },
{ id: 2, name: "Pending" },
{ id: 3, name: "Inactive" },
],
disabled: (data) => {
return data.status === 3 && !data.isActive;
},
},
};
Events and Interaction
Canvas-Sheet handles many events automatically:
- Click to select cells
- Double-click/tab/enter to edit cells
- Click and drag or shift click to select ranges
- Keyboard navigation (arrow keys, tab, enter, escape)
- Copy/paste support
- Column/row resizing
- Press delete on a column header to delete the column (if removable is true)
- Press delete on selected rows to delete the rows
Browser Support
Canvas-Sheet works in all modern browsers that support HTML5 Canvas.
License
MIT