A canvas grid with a zooming camera, where every cell is drawn by your code.
- Zooming camera — pan, wheel-zoom,
fit()the whole grid, animated focus on a single cell - Layered cell drawers — a cell is an ordered stack of
{ data, drawer }layers, drawn bottom→top - Background layers — gradients, hover highlights, routed paths between cells
- Sticky cells — pin a row, a column or a single cell to the viewport edge
- Header / footer decor — strips above or below a cell that never reflow the layout
- HTML focus overlay — real DOM (selectable text, links, forms) over the focused cell
Zero runtime dependencies. ESM only. TypeScript types included.
npm i active-gridimport {ActiveGrid, type CellDrawer} from 'active-grid';
const label: CellDrawer<string> = {
draw(text, {rect}, ctx) {
ctx.fillStyle = '#1b2a35';
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
ctx.fillStyle = '#e0fbfc';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, rect.x + rect.width / 2, rect.y + rect.height / 2);
},
};
const grid = new ActiveGrid({rows: 4, cols: 6, cellWidth: 120, cellHeight: 60, gap: 4});
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 6; col++) {
grid.setCell(row, col, {data: `${row},${col}`, drawer: label});
}
}
grid.attach(document.getElementById('grid') as HTMLCanvasElement);The canvas must have a parent element and a CSS size — the grid tracks it with a ResizeObserver
and fits the whole grid on the first frame.
<div style="position: relative; width: 100%; height: 100vh">
<canvas id="grid" style="width: 100%; height: 100%"></canvas>
</div>| Concept | API |
|---|---|
| The grid | new ActiveGrid(config: GridConfig), attach(canvas), detach() |
| Cell content | setCell(row, col, ...layers) — each layer is a CellLayer<T> = { data, drawer } |
| Drawing | CellDrawer<T> — draw, optional layoutWidth/layoutHeight, onHover, onClick |
| Cell state | CellContext — rect, scale, pointer, hovered, selected, focused, sticky, decor |
| Backgrounds | setBackground(...) with verticalGradient(from, to), cellHover(color), cellPath(config) |
| Sticky cells | setSticky(row, col, { horizontal, vertical }) |
| Decor | setHeader(row, col, height, ...layers), setFooter(...) |
| Camera | fit(), setScale(1..100), revealCell(row, col), focusCell(row, col), clearFocus() |
| Focus overlay | setFocusLayout(layout: FocusLayout) — returns HTML for the focused cell, or null |
| Keyboard | setKeyboard(controller), defaultKeyboardController, focusKeyboard, selectAndShow |
| Events | onHoverChange, onSelectionChange, onFocusChange, onScaleChange |
Omit cellWidth/cellHeight to size columns and rows from the widest/tallest layer that implements
layoutWidth/layoutHeight.
setFocusLayout returns HTML shown over the focused cell. The panel is created for you, next to the canvas, with the
class ag-focus-overlay:
grid.setFocusLayout(({row, col}) => `<article><h3>Cell ${row},${col}</h3><p>Selectable text.</p></article>`);The library ships no stylesheet — only layout styles are set inline. Style the panel yourself:
.ag-focus-overlay {
padding: 12px 16px;
border: 1px solid #2c4152;
border-radius: 6px;
background: #12202b;
color: #e0fbfc;
}npm run dev # demo at http://localhost:5173
npm test # vitest
npm run build # ESM bundle + .d.ts into dist/
npm run build:demo # deployable demo into dist-demo/The demo under demo/ is the reference for everything canvas-related: one sample per file, with drawers, keyboard
controllers and focus layouts kept separate.
A justfile wraps the same commands (just dev, just test, just build) if you have
just installed — it is optional.
ARCHITECTURE.md is the file map and how the pieces fit; CONTRIBUTING.md has the full command list, conventions, invariants and the release process.
MIT © Eugene Levenetc