Examples
Case 3: Pixel Art Drawing Interface

🎨 Pixel Art Drawing Interface

This demo showcases drawing simple pictures using Excel grid paper.

A 50x50 grid is provided for drawing basic shapes and patterns.

Selected Color:

Implementation Guide

📄 View Source Code

Imperative Sheet Access with useSheetRef and useStoreRef

useSheetRef and useStoreRef give you imperative access to the internal sheet state and Redux-like store outside the normal React data flow.

const sheetRef = useSheetRef();
const storeRef = useStoreRef();
 
// Pass them as props to GridSheet
<GridSheet sheetRef={sheetRef} storeRef={storeRef} ... />

After the grid mounts, sheetRef.current exposes { sheet, apply } and storeRef.current exposes { store, dispatch }.

Reading the Current Selection with clip

clip(store) returns the currently selected area as { top, bottom, left, right }. This is the entry point for any operation that acts on the user's selection.

const area = clip(store);
if (!area) return;
 
for (let row = area.top; row <= area.bottom; row++) {
  for (let col = area.left; col <= area.right; col++) {
    const cellAddress = p2a({ y: row, x: col });
    diff[cellAddress] = { style: { backgroundColor: selectedColor } };
  }
}

p2a({ y, x }) converts a zero-based coordinate into a cell address string like "C3".

Writing Style Changes Imperatively with sheet.update

To apply changes programmatically (e.g., from a button click), build a diff object keyed by cell address and call sheet.update({ diff }). Pass the result to sync to commit it to the store.

const { sheet, apply } = sheetRef.current;
const diff: CellsByAddressType = {
  A1: { style: { backgroundColor: '#FF0000' } },
  B1: { style: { backgroundColor: '#FF0000' } },
};
apply(sheet.update({ diff }));

This pattern lets you drive cell mutations from outside the grid — for example, from a color picker or a fill button.

Guaranteeing Grid Size with ensured

By default, GridSheet only allocates rows/columns for cells that have data. Use the ensured option in buildInitialCells to reserve a minimum grid size regardless of data.

buildInitialCells({
  cells: {
    defaultRow: { height: 25 },
    defaultCol: { width: 25 },
    ...getSavedData(),
  },
  ensured: {
    numRows: 50,
    numCols: 50,
  },
})

This is essential for a blank drawing canvas where the user should be able to click any cell even before any data exists.

Persisting Cell Styles to localStorage

To save only the relevant data (background colors), iterate over the sheet's cell range, read cell.style.backgroundColor, and store a compact mapping of address → color.

const coloredCells: { [key: string]: string } = {};
for (let row = sheet.top; row <= sheet.bottom; row++) {
  for (let col = sheet.left; col <= sheet.right; col++) {
    const cell = sheet.getCell({ y: row, x: col });
    if (cell?.style?.backgroundColor) {
      coloredCells[p2a({ y: row, x: col })] = cell.style.backgroundColor;
    }
  }
}
localStorage.setItem('demo3', JSON.stringify({ cells: coloredCells }));

On load, reconstruct CellsByAddressType from the saved map and pass it into buildInitialCells as spread cells.

Auto-saving on Every Change with onChange

useSpellbook accepts an onChange callback that fires after every cell mutation. Wiring the save logic there means the drawing is persisted automatically without requiring an explicit save button.

const book = useSpellbook({
  onChange: saveData,
});

Initial Pixel Art as CellsByAddressType

The heart shape pre-drawn on first load is defined as a plain CellsByAddressType object, where each key is a cell address and the value sets a backgroundColor.

const myHeart: CellsByAddressType = {
  C3: { style: { backgroundColor: 'red' } },
  D3: { style: { backgroundColor: 'red' } },
  // ...
  B5: { style: { backgroundColor: 'pink' } },
  // ...
};

This object is spread directly into the cells option of buildInitialCells, making it trivial to define any initial pixel art pattern.