Skip to content

Recoveredd/hex-grid-kit

hex-grid-kit

npm version License: MPL-2.0 CI

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: Demo · GitHub

Package quality

  • TypeScript types are generated from the source.
  • ESM-only package with no runtime dependencies.
  • Marked as side-effect free for bundlers.
  • Browser-friendly implementation with no Node-only APIs.
  • CI runs npm ci, typecheck, build, and test.
  • Tested on Node.js 20 and 22 with GitHub Actions.

Demo

Try the interactive demo: packages.wasta-wocket.fr/hex-grid-kit.

The demo runs the published npm package in the browser and shows shape switching, cube and axial labels, fixed or responsive SVG sizing, per-cell fills, selection, neighbors, rings and line helpers.

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-kit

Create 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 only

Cell 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 ci
  • npm run typecheck
  • npm test
  • npm run build

License

MPL-2.0

About

Framework-agnostic SVG hex grid toolkit with cube coordinates and interactive helpers

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors