hex-grid-kit
v0.1.0
Published
Build interactive SVG hex grids with cube coordinates, hit testing and framework-agnostic helpers.
Maintainers
Readme
hex-grid-kit
Build interactive SVG hex grids with cube coordinates, hit testing and framework-agnostic helpers.
hex-grid-kit is a clean-room toolkit for apps that need a hexagonal board without committing to a specific framework. It gives you the coordinate math, generated cells, SVG geometry and a small DOM mount helper, so you can integrate a selectable hex grid in a browser UI quickly.
Links: GitHub
Quick start
<div id="board"></div>
<script type="module">
import { mountHexGrid } from 'hex-grid-kit';
mountHexGrid(document.querySelector('#board'), {
shape: 'hexagon',
radius: 3,
cellSize: 32,
selectable: true,
showCoordinates: true,
onCellClick(cell) {
console.log(cell.id, cell.coord);
}
});
</script>Why this package
Use honeycomb-grid if you mainly need a mature headless coordinate library.
Use hex-grid-kit when you want a practical interactive board layer:
- cube-first coordinate helpers, with axial
{ q, r }shorthand when you want it; - hexagon, rectangle, parallelogram and custom grid shapes;
- SVG polygon generation with safe labels and attributes;
- hit testing from pointer coordinates to cells;
- neighbors, ranges, rings and straight hex lines;
- optional DOM mount helper for selection and pointer callbacks.
Install
npm install hex-grid-kitCreate a grid
import { createHexGrid } from 'hex-grid-kit';
const grid = createHexGrid({
shape: 'hexagon',
radius: 3,
cellSize: 28,
orientation: 'pointy'
});
console.log(grid.cells.length); // 37
console.log(grid.neighborsOf('0,0,0').map((cell) => cell.id));
console.log(grid.range('0,0,0', 2).map((cell) => cell.id));Render SVG
import { createHexGrid, renderHexGridSvg } from 'hex-grid-kit';
const grid = createHexGrid({
shape: 'rectangle',
columns: 8,
rows: 5,
cellSize: 24
});
const svg = renderHexGridSvg(grid, {
title: 'Level editor board',
showCoordinates: true,
selectedIds: ['0,0,0', '1,0,-1'],
highlightedIds: grid.line('0,0,0', '4,0,-4').map((cell) => cell.id)
});
document.querySelector('#board')!.innerHTML = svg;Mount an interactive board
import { mountHexGrid } from 'hex-grid-kit';
const board = mountHexGrid(document.querySelector('#board')!, {
shape: 'hexagon',
radius: 4,
cellSize: 30,
selectable: true,
multiSelect: true,
showCoordinates: true,
onCellClick(cell) {
console.log('clicked', cell.id, cell.coord);
},
onSelectionChange(ids) {
console.log('selected cells', ids);
}
});
board.setSelected(['0,0,0', '1,-1,0']);
// Later:
board.update({ radius: 5 });
board.destroy();The mount helper only needs a normal DOM element. It is intentionally small, so React, Vue, Svelte or plain JavaScript apps can either use it directly or render grid.cells themselves.
Framework integration
hex-grid-kit does not own your UI state. In framework apps, you can keep selection and hover state in your component, then render from the generated cells:
import { createHexGrid, getHexGridViewBox } from 'hex-grid-kit';
const grid = createHexGrid({ shape: 'hexagon', radius: 3, cellSize: 28 });
export function Board({ selectedId, onSelect }) {
return (
<svg viewBox={getHexGridViewBox(grid)}>
{grid.cells.map((cell) => (
<polygon
key={cell.id}
points={cell.points}
data-hex-id={cell.id}
className={cell.id === selectedId ? 'hex selected' : 'hex'}
onClick={() => onSelect(cell)}
/>
))}
</svg>
);
}Hit testing
const cell = grid.hitTest({ x: pointerX, y: pointerY });
if (cell) {
console.log(cell.id, cell.coord.q, cell.coord.r, cell.coord.s);
}hitTest converts pixels back to cube coordinates and verifies that the point is inside the hex polygon. This matters when you use spacing to create visible gaps between cells.
Grid shapes
createHexGrid({ shape: 'hexagon', radius: 2 });
createHexGrid({ shape: 'rectangle', columns: 10, rows: 6 });
createHexGrid({ shape: 'parallelogram', columns: 10, rows: 6 });
createHexGrid({
shape: 'custom',
coordinates: [
{ q: 0, r: 0, s: 0 },
{ q: 1, r: 0, s: -1 },
{ q: 1, r: -1, s: 0 }
]
});Partial and irregular boards
You do not have to generate a complete board. Use shape: 'custom' when your app only needs specific cells:
const grid = createHexGrid({
shape: 'custom',
cellSize: 32,
coordinates: [
{ q: 0, r: 0, s: 0 },
{ q: 1, r: -1, s: 0 },
{ q: 1, r: 0, s: -1 },
{ q: 0, r: 1, s: -1 },
{ q: -1, r: 1, s: 0 }
]
});This is useful for irregular maps, unlocked areas, tactical boards, level editors and dashboards where only part of the hex space exists. Neighbor, ring, range and line helpers automatically return only cells that are present in the generated grid when you call them through the grid instance:
grid.neighborsOf('0,0,0'); // existing neighboring cells only
grid.range('0,0,0', 2); // existing cells inside the range onlyCell data
Attach app-specific data when you want fills, labels, click handlers or framework components to read terrain, cost, ownership or any other domain state from the cell itself.
const grid = createHexGrid({
shape: 'custom',
coordinates: [
{ q: 0, r: 0, s: 0, data: { terrain: 'castle', cost: 1 } },
{ q: 1, r: -1, s: 0, data: { terrain: 'forest', cost: 2 } }
]
});
const svg = grid.toSvg({
cellFill(cell) {
return cell.data?.terrain === 'forest' ? '#86efac' : '#dbeafe';
},
renderLabel(cell) {
return String(cell.data?.cost ?? '');
}
});Generated shapes can also compute data from each coordinate:
createHexGrid({
shape: 'hexagon',
radius: 4,
data(coord) {
return { zone: coord.s === 0 ? 'road' : 'plain' };
}
});Coordinates
The public API is cube-first:
type HexCoordinateInput = {
q: number;
r: number;
s?: number;
};Canonical cell IDs use q,r,s, for example 0,0,0 or 1,-1,0. When s is omitted, it is computed as -q - r, so { q: 1, r: -1 } remains a convenient shorthand for { q: 1, r: -1, s: 0 }. If you pass s, the package validates that q + r + s === 0.
Useful helpers:
import {
cubeToPixel,
pixelToCube,
formatHexCoord,
getHexNeighbors,
hexDistance,
hexLine,
hexRange,
hexRing
} from 'hex-grid-kit';Styling
By default, SVG output is responsive: the grid keeps its internal cellSize geometry, but the browser can scale the SVG visually through CSS.
Use svgSize: 'fixed' when the rendered SVG should keep the intrinsic size derived from cellSize, spacing, grid shape and padding:
const grid = createHexGrid({
shape: 'hexagon',
radius: 4,
cellSize: 32
});
const svg = grid.toSvg({
svgSize: 'fixed'
});This writes width and height attributes on the SVG. The total rendered size comes from grid.bounds + padding, so a larger board takes more space instead of shrinking cells to fit the container.
When rendering your own SVG, use the helpers instead of rebuilding the viewBox calculation:
import { getHexGridSvgMetrics, getHexGridViewBox } from 'hex-grid-kit';
const viewBox = getHexGridViewBox(grid);
const { width, height } = getHexGridSvgMetrics(grid);By default, rendered SVG includes small built-in styles. Disable them when you want full control:
const svg = grid.toSvg({
includeStyles: false,
classPrefix: 'game-board',
selectedIds: ['0,0,0']
});The generated SVG uses predictable classes:
.hex-grid__cell.hex-grid__cell--selected.hex-grid__cell--highlighted.hex-grid__cell--disabled.hex-grid__label
Per-cell fills and images
Use cellFill when each cell needs its own terrain, state or visual marker:
const svg = grid.toSvg({
cellFill(cell) {
if (cell.id === '0,0,0') {
return { type: 'image', href: '/tiles/castle.png' };
}
if (cell.id === '1,0,-1') {
return '#86efac';
}
return undefined;
}
});String returns are treated as color fills. Image fills generate the SVG <defs><pattern /></defs> automatically and use fill="url(#...)" on the matching hexagon.
grid.toSvg({
cellFill(cell) {
return {
type: 'image',
href: `/tiles/${cell.id}.png`,
preserveAspectRatio: 'xMidYMid slice'
};
}
});Return { type: 'none' } when a specific cell should be transparent.
Safety
Labels, titles and attributes rendered through renderHexGridSvg are escaped. The package does not evaluate user-provided markup.
CI
The GitHub Actions workflow runs on Node.js 20 and 22:
npm cinpm run typechecknpm testnpm run build
License
MPL-2.0
