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.
Scripts
Section titled “Scripts”npm installnpm run build # → dist-lib/rabbit.js (ESM, ~47 kB, ~15 kB gzipped)npm testFor a visual harness that exercises Rabbit alongside the rest of the
PROMOTE stack (Tabia, Motif), see ../dev/ in the workspace.
Pieces
Section titled “Pieces”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).
Events
Section titled “Events”Subscribe with board.on(event, handler) → unsubscribe.
| Event | Payload | When |
|---|---|---|
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. |
shapes | Array<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.
Theming
Section titled “Theming”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;}| Variable | Default | Notes |
|---|---|---|
--rabbit-light | #f0d9b5 | Light squares. |
--rabbit-dark | #b58863 | Dark squares. Accents derive from this. |
--rabbit-selected | hsl(from --rabbit-dark h 75% 55%) | Selected-square overlay. |
--rabbit-last-move | hsl(from --rabbit-dark calc(h + 50) 50% 55%) | Last-move highlight color. |
--rabbit-check | hsl(from --rabbit-dark 8 75% 50%) | Check-glow color. |
--rabbit-selection-opacity | 0.5 | Override per-board via setOptions. |
--rabbit-hint-color | #000 | Move-hint dots. |
--rabbit-hint-opacity | 0.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.
Sizing
Section titled “Sizing”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); }Shapes (arrows, circles, fills, glow)
Section titled “Shapes (arrows, circles, fills, glow)”Two layers of shapes coexist:
- Programmatic —
board.setShapes([...]). The consumer owns these (move annotations, engine output, etc.). - User — drawn by right-click drag (Escape clears). Read via
board.getUserShapes(), subscribe viaon('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.
PGN drawables
Section titled “PGN drawables”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.
Variants
Section titled “Variants”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.
Promotion
Section titled “Promotion”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.
Data shapes
Section titled “Data shapes”The strings and objects Rabbit accepts from its caller. Rabbit doesn’t validate these — pass coherent data and the board renders it.
Square
Section titled “Square”${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).
PieceCode
Section titled “PieceCode”${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.
Position
Section titled “Position”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).
LegalMoves
Section titled “LegalMoves”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.
SnapTargets
Section titled “SnapTargets”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.
Shape (annotation)
Section titled “Shape (annotation)”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.
Board shape (non-rectangular layouts)
Section titled “Board shape (non-rectangular layouts)”For variants with holes or walls, pass shape to createBoard as
any of:
shape: new Set(['a1', 'a2', 'b1', 'b2']) // explicit squaresshape: ['a1', 'a2', 'b1', 'b2'] // arrayshape: (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.
PiecesConfig
Section titled “PiecesConfig”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.
API surface
Section titled “API surface”// FactorycreateBoard(config) → board
// Construction stateboard.element // root <svg>board.ready // Promise<void> — resolves after first paintboard.preload(codes) // load extra piece SVGs without re-rendering
// Positionboard.getPosition() // { object, fen }board.setPosition(pos, { duration?, legalMoves?, snapTargets? })
// Orientationboard.getOrientation()board.setOrientation('white' | 'black' | 'flip', { animate? })board.flip({ animate? })
// Selectionboard.getSelected()board.select(sq | null)
// Shapesboard.setShapes(shapes)board.getShapes()board.getUserShapes()board.clearUserShapes()
// Optionsboard.setOptions(updates) // any key from DEFAULTS, plus brushModifiers/renderers
// Eventsboard.on(event, handler) → unsubscribe
// Cleanupboard.destroy()Re-exports
Section titled “Re-exports”import { createBoard, DEFAULTS, resolvePieces, STANDARD_CHESS_CODES, parseFEN, serializeFEN, normalizePosition, STARTING_POSITION, shapesFromPgnDrawables, shapeKey, brushFromModifiers, DEFAULT_BRUSH_MODIFIERS, DEFAULT_RENDERERS,} from '@promote/rabbit';