Skip to content

Rabbit

A pure-SVG chess board. Small, fast, framework-free, variant-aware, built as a rendering substrate — consumers compose any chess UI on top.

import { createBoard } from '@promote/rabbit';
const board = createBoard({
mount: document.getElementById('app'),
pieces: 'https://your.cdn/pieces/cburnett', // or { name, svgs } / loader fn
position: 'start',
orientation: 'white',
lastMove: { from: 'e2', to: 'e4' },
check: 'e8',
});
board.on('intent', ({ from, to, promotion }) => {
// validate + apply, then:
board.setPosition(newPositionOrFen);
});

No CSS import needed — createBoard installs its styles into a @layer rabbit cascade layer on first call, so any unlayered theme you write wins automatically without specificity wars.

npm install
npm run build # → dist-lib/rabbit.js (ESM, ~47 kB, ~15 kB gzipped)
npm test

For a visual harness that exercises Rabbit alongside the rest of the PROMOTE stack (Tabia, Motif), see ../dev/ in the workspace.

pieces is required. Three forms, picked to fit how your project ships SVGs:

// URL prefix — files at `${prefix}/${code}.svg` (lazy fetch, cached).
pieces: '/assets/pieces/cburnett'
// Inline — handy when you import SVGs as strings via your bundler.
pieces: { name: 'cburnett', svgs: { wK: '<svg…>', wQ: '<svg…>', /* … */ } }
// Loader — async function(codes) → { [code]: svg } | { svgs: {…} }.
pieces: async (codes) => fetchMyPieces(codes)

Piece codes are ${color}${LETTER}wK, bQ, plus fairy-stockfish extensions like w+P (shogi-style promoted) and wQ~ (Crazyhouse promoted-pawn marker).

Subscribe with board.on(event, handler) → unsubscribe.

EventPayloadWhen
intent{ from, to, piece, promotion? }User completed a move gesture (drag-drop or tap).
pick{ square, piece }User pressed down on a piece (start of drag/tap).
drop{ from, to, piece, valid }User released a drag — valid reflects legality.
select{ square, piece }Selection changed (incl. square: null on clear).
cancel{ reason }Gesture aborted (escape, off-board, illegal, same-square, pointer-cancel, promotion-cancel).
promotion{ from, to, color }Pawn reached the back rank in promotion: 'manual' mode — render your own picker.
shapesArray<Shape>User shapes changed (right-click draw, Escape clear).
load-error{ codes, error }Piece fetch failed. codes: array of PieceCodes that didn’t load. error: the rejected exception (loader threw) or null (loader returned partial result, e.g. URL prefix with 404s).

The board never mutates its own position from a gesture; intent is the consumer’s signal to validate, apply, and call setPosition.

Set CSS variables anywhere — :root, a board’s mount element, or a parent class. Because Rabbit’s styles live in @layer rabbit, any unlayered rule you write wins.

:root {
--rabbit-dark: #769656; /* the master color — accents derive from it */
--rabbit-light: #eeeed2;
}
VariableDefaultNotes
--rabbit-light#f0d9b5Light squares.
--rabbit-dark#b58863Dark squares. Accents derive from this.
--rabbit-selectedhsl(from --rabbit-dark h 75% 55%)Selected-square overlay.
--rabbit-last-movehsl(from --rabbit-dark calc(h + 50) 50% 55%)Last-move highlight color.
--rabbit-checkhsl(from --rabbit-dark 8 75% 50%)Check-glow color.
--rabbit-selection-opacity0.5Override per-board via setOptions.
--rabbit-hint-color#000Move-hint dots.
--rabbit-hint-opacity0.25

Many of these are also driveable as createBoard options (light, dark, selected, hintColor, etc.) — passing them at construction or via setOptions writes the same custom property on the board’s root SVG.

The board fills its mount. Size the mount from outside; Rabbit sets aspect-ratio from the board’s files / ranks so non-square boards (e.g. Capablanca 10×8) render without letterboxing.

#app { width: min(90vmin, 640px); }

Two layers of shapes coexist:

  • Programmaticboard.setShapes([...]). The consumer owns these (move annotations, engine output, etc.).
  • User — drawn by right-click drag (Escape clears). Read via board.getUserShapes(), subscribe via on('shapes', ...).

Shape forms:

{ type: 'arrow', from: 'e2', to: 'e4', brush, bent? } // bent: auto for knights
{ type: 'circle', on: 'e4', brush }
{ type: 'fill', on: 'e4', brush }
{ type: 'glow', on: 'e8', brush, radius? }
{ type: 'raw', path: '<svg-path-d>', brush, fill?, stroke? }

A brush is { color, opacity, strokeWidth }. Defaults live in DEFAULT_BRUSH_MODIFIERS; the four modifier slots (none, shift, alt, shiftAlt) drive right-click-draw colors and PGN drawable mapping.

Rabbit ships a converter for the lichess/ChessBase [%cal …] / [%csl …] annotations that most PGN parsers surface:

import { shapesFromPgnDrawables } from '@promote/rabbit';
// annotations = { arrows: ['Gd2d4', 'Re4e5'], squares: ['Yg5'] }
board.setShapes(shapesFromPgnDrawables(annotations));

Color codes map to Rabbit’s modifier palette (G→green, R→red, B→blue, Y→orange), so PGN-derived arrows match user-drawn arrows of the corresponding modifier — a board layered with both reads coherently.

boardSize and shape let Rabbit render arbitrary board geometries:

createBoard({
mount,
pieces,
boardSize: { files: 10, ranks: 8 }, // Capablanca
position: 'rnabqkbcnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNABQKBCNR',
});

For non-rectangular boards, pass shape as a Set<square>, Array<square>, or function(file, rank) → bool. FEN strings with * (walls) or _ (holes) derive the shape automatically.

Default mode (promotion: 'auto') shows Rabbit’s built-in picker — a vertical strip anchored to the destination square. 'manual' suppresses the picker and emits promotion so consumers can render their own UI; once the user picks, the consumer applies the move and calls setPosition.

promotionOptions defaults to ['Q', 'R', 'B', 'N']. Variants extend this — S-Chess adds H, E; Antichess adds K.

The strings and objects Rabbit accepts from its caller. Rabbit doesn’t validate these — pass coherent data and the board renders it.

${file}${rank} — files are a through z, then aa through az for boards wider than 26. Ranks are integer strings starting at 1. Examples: 'e4', 'h8', 'aa10' (Grand-and-wider variants).

${color}${LETTER} — color is w or b. Standard chess letters are K Q R B N P (wK, bN, …). Fairy-stockfish extensions like w+P (shogi-style promoted) and wQ~ (Crazyhouse promoted-pawn marker) are accepted.

type Position = { [square: Square]: PieceCode };
// { e1: 'wK', e8: 'bK', e2: 'wP', ... }

A bare object listing only occupied squares. setPosition accepts either this form or a FEN string.

{ color: string, opacity: number, strokeWidth: number }

color is any CSS color (hex, named, hsl(…), var(--…)). opacity is 0..1. strokeWidth is in user units (fractions of a square — 0.16 is about one-sixth of a square).

type LegalMoves = { [from: Square]: Square[] };
// { e2: ['e3', 'e4'], g1: ['f3', 'h3'], ... }

A drop only fires intent if the destination appears in the from-square’s array; otherwise the piece snaps back and cancel fires with reason: 'illegal'. Pass null (the default) to skip legality checking — every drop fires intent. Move-hint dots and snap-back behavior both consume this.

Same shape as LegalMoves. When present, the drag snap-back layer uses these in preference to LegalMoves. Useful when “where a piece can drop” differs from “what’s legal” — e.g. a teaching mode that highlights hint squares without restricting moves.

See the Shapes section above for the discriminated-union form. Programmatic shapes go through board.setShapes([…]); user shapes are read via board.getUserShapes() and the shapes event.

For variants with holes or walls, pass shape to createBoard as any of:

shape: new Set(['a1', 'a2', 'b1', 'b2']) // explicit squares
shape: ['a1', 'a2', 'b1', 'b2'] // array
shape: (file, rank) => (file + rank) % 2 === 0 // predicate (file 0-indexed)

If position is a FEN string, * (wall) and _ (hole) characters derive the shape automatically. Explicit shape config wins over FEN-derived.

See the Pieces section. All three accepted forms (URL prefix, inline {name, svgs}, loader function) resolve to an internal { name, fetchCodes(codes) → Promise<{[code]: svgString}> } shape that the initial load and board.preload(codes) both call.

// Factory
createBoard(config) → board
// Construction state
board.element // root <svg>
board.ready // Promise<void> — resolves after first paint
board.preload(codes) // load extra piece SVGs without re-rendering
// Position
board.getPosition() // { object, fen }
board.setPosition(pos, { duration?, legalMoves?, snapTargets? })
// Orientation
board.getOrientation()
board.setOrientation('white' | 'black' | 'flip', { animate? })
board.flip({ animate? })
// Selection
board.getSelected()
board.select(sq | null)
// Shapes
board.setShapes(shapes)
board.getShapes()
board.getUserShapes()
board.clearUserShapes()
// Options
board.setOptions(updates) // any key from DEFAULTS, plus brushModifiers/renderers
// Events
board.on(event, handler) → unsubscribe
// Cleanup
board.destroy()
import {
createBoard, DEFAULTS, resolvePieces, STANDARD_CHESS_CODES,
parseFEN, serializeFEN, normalizePosition, STARTING_POSITION,
shapesFromPgnDrawables, shapeKey, brushFromModifiers,
DEFAULT_BRUSH_MODIFIERS, DEFAULT_RENDERERS,
} from '@promote/rabbit';