chessiro-canvas
v0.1.31
Published
Lightweight, high-performance React chessboard component
Downloads
1,133
Maintainers
Readme
chessiro-canvas
Lightweight React chessboard with low overhead interaction primitives inspired by chessground.
Built for https://chessiro.com, but can be used by all
- Zero runtime dependencies
- TypeScript-first API
- Drag, click-move, arrows, marks, premoves, promotion, overlays
- Built for controlled usage in analysis and coaching apps
Install
npm install chessiro-canvasQuick Start
Your container must define width, and board height follows width (square board).
import { ChessiroCanvas, INITIAL_FEN } from 'chessiro-canvas';
export default function App() {
return (
<div style={{ width: 520 }}>
<ChessiroCanvas position={INITIAL_FEN} />
</div>
);
}ChessiroCanvas is a controlled component. For playable boards, your onMove must update
position after validating the move (typically via chess.js or chessops).
Piece Rendering (Default + Custom)
ChessiroCanvas ships with embedded default SVG pieces and renders them by default with no asset hosting setup.
<ChessiroCanvas position={fen} />Piece license note:
- Bundled default piece artwork is generated from
react-chessboarddefaults (MIT license). - You can replace it any time via
pieceSet.path.
Use pieceSet.path only when you want to override with your own hosted piece set.
<ChessiroCanvas
position={fen}
pieceSet={{
id: 'alpha',
name: 'Alpha',
path: '/pieces/alpha', // expects /pieces/alpha/wp.svg ... /bk.svg
}}
/>If pieces appear as broken images, upgrade to the latest package version.
Customize Legal Move UI
Use squareVisuals to customize legal dots, capture rings, premove hints, marks, and check overlay.
<ChessiroCanvas
position={fen}
dests={dests}
squareVisuals={{
legalDot: 'rgba(30, 144, 255, 0.55)',
legalDotOutline: 'rgba(255, 255, 255, 0.95)',
legalCaptureRing: 'rgba(30, 144, 255, 0.8)',
premoveDot: 'rgba(155, 89, 182, 0.55)',
premoveCaptureRing: 'rgba(155, 89, 182, 0.75)',
selectedOutline: 'rgba(255, 255, 255, 1)',
markOverlay: 'rgba(244, 67, 54, 0.6)',
markOutline: 'rgba(244, 67, 54, 0.9)',
}}
/>Customize Other UI Layers
<ChessiroCanvas
position={fen}
showMargin={true}
marginRadius={16}
boardRadius={14}
arrowVisuals={{
lineWidth: 0.2,
opacity: 1,
markerWidth: 5,
markerHeight: 5,
}}
notationVisuals={{
fontFamily: 'JetBrains Mono, monospace',
fontSize: 11,
onBoardFontSize: 11,
opacity: 0.95,
}}
promotionVisuals={{
panelColor: 'rgba(20, 24, 36, 0.98)',
titleColor: '#f2f6ff',
optionBackground: 'rgba(255, 255, 255, 0.08)',
optionTextColor: '#f2f6ff',
cancelTextColor: '#cbd5e1',
}}
overlayVisuals={{
background: 'rgba(2, 6, 23, 0.85)',
color: '#f8fafc',
borderRadius: '6px',
fontSize: '11px',
}}
/>boardRadius controls inner board corners, and marginRadius controls outer margin corners.
Both work independently, so you can style rounded inner + outer frames together.
For notation sizing, notationVisuals.fontSize and notationVisuals.onBoardFontSize accept either number or CSS string.
Integration With chess.js
npm install chess.js chessiro-canvasimport { useMemo, useState } from 'react';
import { Chess } from 'chess.js';
import { ChessiroCanvas, type Dests, type Square } from 'chessiro-canvas';
export function ChessJsBoard() {
const [chess] = useState(() => new Chess());
const [fen, setFen] = useState(() => chess.fen());
const dests = useMemo<Dests>(() => {
const map = new Map<Square, Square[]>();
const moves = chess.moves({ verbose: true });
for (const move of moves) {
const from = move.from as Square;
const to = move.to as Square;
const current = map.get(from);
if (current) current.push(to);
else map.set(from, [to]);
}
return map;
}, [fen]);
return (
<ChessiroCanvas
position={fen}
turnColor={chess.turn()}
movableColor={chess.turn()}
dests={dests}
onMove={(from, to, promotion) => {
const result = chess.move({ from, to, promotion });
if (!result) return false;
setFen(chess.fen());
return true;
}}
/>
);
}Important for chess.js users:
chess.jsmutates the sameChessinstance in place.- Do not key
useMemo/useEffectoff thechessobject reference for legal moves, check square, turn state, etc. - Key derived UI state from
fen(or move history), becausefenchanges on every accepted move.
Correct dependency pattern:
const [chess] = useState(() => new Chess());
const [fen, setFen] = useState(() => chess.fen());
const dests = useMemo(() => {
const map = new Map();
for (const move of chess.moves({ verbose: true })) {
const list = map.get(move.from) ?? [];
list.push(move.to);
map.set(move.from, list);
}
return map;
}, [fen]); // <- use fen, not [chess]Integration With chessops
npm install chessops chessiro-canvasimport { useMemo, useState } from 'react';
import { Chess } from 'chessops/chess';
import { chessgroundDests } from 'chessops/compat';
import { parseFen, makeFen } from 'chessops/fen';
import { parseUci } from 'chessops/util';
import { ChessiroCanvas, INITIAL_GAME_FEN } from 'chessiro-canvas';
export function ChessopsBoard() {
const [pos, setPos] = useState(() =>
Chess.fromSetup(parseFen(INITIAL_GAME_FEN).unwrap()).unwrap(),
);
const fen = useMemo(() => makeFen(pos.toSetup()), [pos]);
const dests = useMemo(() => chessgroundDests(pos), [pos]);
const turn = pos.turn === 'white' ? 'w' : 'b';
return (
<ChessiroCanvas
position={fen}
turnColor={turn}
movableColor={turn}
dests={dests}
onMove={(from, to, promotion) => {
const uci = `${from}${to}${promotion ?? ''}`;
const move = parseUci(uci);
if (!move || !pos.isLegal(move)) return false;
const next = pos.clone();
next.play(move);
setPos(next);
return true;
}}
/>
);
}INITIAL_FEN is piece-placement only (UI-friendly). For engine integrations, use INITIAL_GAME_FEN so castling rights are present.
Features
- FEN-based board rendering
- Built-in default piece set shipped with the package
- Click-to-move and drag-to-move
- Legal move dots and capture rings
- Premoves with optional external event hooks
- Right-click arrows and marks
- Last-move, check, and custom square highlights
- Move-quality badge support
- Promotion chooser
- Text overlays with custom renderer
- Keyboard callbacks (
ArrowLeft,ArrowRight,Home,End,F,X,Escape) - Theme, piece set, and custom piece renderer support
Core API
ChessiroCanvas props
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| position | string | start position | FEN (piece placement or full FEN; placement is parsed) |
| orientation | 'white' \| 'black' | 'white' | Board orientation |
| interactive | boolean | true | Disables move interactions when false |
| turnColor | 'w' \| 'b' | undefined | Needed for turn-aware move/premove flow |
| movableColor | 'w' \| 'b' \| 'both' | undefined | Restricts which side can move |
| onMove | (from, to, promotion?) => boolean | undefined | Return true to accept move |
| dests | Map<Square, Square[]> | undefined | Legal destinations per square |
| lastMove | { from: string; to: string } \| null | undefined | Last move highlight |
| check | string \| null | undefined | King-in-check square |
| premovable | PremoveConfig | undefined | Enables premove and callbacks |
| arrows | Arrow[] | [] | Controlled arrows |
| onArrowsChange | (arrows) => void | undefined | Arrow updates |
| markedSquares | string[] | internal | Controlled marks |
| onMarkedSquaresChange | (squares) => void | undefined | Mark updates |
| arrowBrushes | Partial<ArrowBrushes> | default set | Override arrow colors |
| arrowVisuals | Partial<ArrowVisuals> | undefined | Customize arrow width, opacity, marker size, and arrow margin |
| snapArrowsToValidMoves | boolean | true | Queen/knight snap behavior |
| theme | BoardTheme | built-in theme | Board colors |
| pieceSet | PieceSet | bundled default pieces | Optional custom piece asset path config |
| pieces | Record<string, () => ReactNode> | undefined | Custom piece renderer map |
| showMargin | boolean | true | Margin frame for notation |
| marginThickness | number | 24 | Margin px |
| marginRadius | number \| string | 4 | Outer margin frame corner radius |
| boardRadius | number \| string | 0 | Board corner radius (works with or without margin) |
| showNotation | boolean | true | Coordinate labels |
| notationVisuals | Partial<NotationVisuals> | undefined | Customize notation font family, size, weight, color, and offsets |
| highlightedSquares | Record<string, string> | {} | Arbitrary square background colors |
| squareVisuals | Partial<SquareVisuals> | undefined | Customize legal/premove indicators, marks, selected outline, and check overlay |
| moveQualityBadge | MoveQualityBadge \| null | undefined | Badge icon on square |
| allowDragging | boolean | true | Drag interaction toggle |
| allowDrawingArrows | boolean | true | Right-click arrows/marks toggle |
| showAnimations | boolean | true | Piece animation toggle |
| animationDurationMs | number | 200 | Piece animation length |
| blockTouchScroll | boolean | false | Prevent scrolling on touch interaction |
| overlays | TextOverlay[] | [] | Text overlays |
| overlayRenderer | (overlay) => ReactNode | undefined | Custom overlay renderer |
| overlayVisuals | Partial<OverlayVisuals> | undefined | Customize default overlay bubble style (when overlayRenderer is not provided) |
| onSquareClick | (square) => void | undefined | Square click callback |
| onClearOverlays | () => void | undefined | Called when board clears current ply overlays |
| promotionVisuals | Partial<PromotionVisuals> | undefined | Customize promotion dialog backdrop, panel, option buttons, and text colors |
| onPrevious onNext onFirst onLast onFlipBoard onShowThreat onDeselect | callbacks | undefined | Keyboard callback hooks |
| className | string | undefined | Wrapper class |
| style | CSSProperties | undefined | Wrapper style |
Exported helpers
INITIAL_FENINITIAL_GAME_FENreadFen(fen)/writeFen(pieces)premoveDests(square, pieces, color)preloadPieceSet(path)DEFAULT_ARROW_BRUSHES
Examples
Controlled legal moves (dests)
import { useMemo } from 'react';
import { ChessiroCanvas, type Dests, type Square } from 'chessiro-canvas';
function Board({ fen, legalMovesByFrom, onMove }) {
const dests = useMemo<Dests>(() => {
const map = new Map<Square, Square[]>();
for (const [from, toList] of Object.entries(legalMovesByFrom)) {
map.set(from as Square, toList as Square[]);
}
return map;
}, [legalMovesByFrom]);
return (
<div style={{ width: 560 }}>
<ChessiroCanvas position={fen} dests={dests} onMove={onMove} />
</div>
);
}Controlled arrows and marks
import { useState } from 'react';
import { ChessiroCanvas, type Arrow } from 'chessiro-canvas';
function AnalysisBoard({ fen }: { fen: string }) {
const [arrows, setArrows] = useState<Arrow[]>([]);
const [marks, setMarks] = useState<string[]>([]);
return (
<div style={{ width: 560 }}>
<ChessiroCanvas
position={fen}
arrows={arrows}
onArrowsChange={setArrows}
markedSquares={marks}
onMarkedSquaresChange={setMarks}
/>
</div>
);
}Theme and piece assets
<ChessiroCanvas
position={fen}
theme={{
id: 'wood',
name: 'Wood',
darkSquare: '#8B5A2B',
lightSquare: '#F0D9B5',
margin: '#5C3B1F',
lastMoveHighlight: '#E7C15D',
selectedPiece: '#A86634',
}}
pieceSet={{
id: 'alpha',
name: 'Alpha',
path: '/pieces/alpha',
}}
/>Benchmark vs react-chessboard
Latest benchmark file: benchmarks/latest.json
Latest browser benchmark file: benchmarks/browser/latest.json
Run Node benchmark:
npm run benchmarkRun browser benchmark (Playwright + Chromium, local vs origin/main vs react-chessboard):
npm run benchmark:browserQuick browser benchmark:
npm run benchmark:browser:quickMethod:
- Environment: Node
v25.6.1, macOS arm64, Apple M4 (10 cores), 16 GB RAM - 8 measured rounds + 2 warmup rounds
- 300 position updates per round
- Position updates replay multiple real move-playthrough scenarios (castling, captures, endgames, promotion)
- Same board size (
640px) and animations disabled for both libraries - Metrics: mount wall time, update wall time, React Profiler update duration, bundle gzip
- Harnesses:
scripts/benchmark.mjs(Node) andscripts/benchmark-playwright.mjs(browser)
Run a subset of scenarios:
BENCH_SCENARIOS=italian-castling,sicilian-captures npm run benchmark
BENCH_SCENARIOS=italian-castling,sicilian-captures npm run benchmark:browserResults (generated on 2026-02-24 UTC):
| Metric | chessiro-canvas | react-chessboard | Delta | | --- | ---: | ---: | ---: | | Mount wall time (mean) | 3.13 ms | 14.23 ms | 78.0% faster | | Update wall time (mean, 300 renders) | 277.42 ms | 733.11 ms | 62.2% faster | | Update wall per render (mean) | 0.92 ms | 2.44 ms | 62.2% faster | | React Profiler update duration (mean) | 0.22 ms | 1.33 ms | 83.4% faster | | Bundle ESM gzip | 31.41 KB | 37.38 KB | 16.0% smaller |
Notes:
- Numbers will vary by machine, Node version, and benchmark config.
- This benchmark is for relative comparison under the same harness, not an absolute browser FPS claim.
Development
npm install
npm run dev
npm run docs:dev
npm run build
npm run typecheck
npm run benchmarkLicense
MIT
