Skip to content

Bot-Rakshit/chessiro-canvas

Repository files navigation

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

Quick 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-chessboard defaults (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-canvas
import { 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.js mutates the same Chess instance in place.
  • Do not key useMemo/useEffect off the chess object reference for legal moves, check square, turn state, etc.
  • Key derived UI state from fen (or move history), because fen changes 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-canvas
import { 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_FEN
  • INITIAL_GAME_FEN
  • readFen(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 benchmark

Run browser benchmark (Playwright + Chromium, local vs origin/main vs react-chessboard):

npm run benchmark:browser

Quick browser benchmark:

npm run benchmark:browser:quick

Method:

  • 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) and scripts/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:browser

Results (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 benchmark

License

MIT

About

Lightweight, high-performance React chessboard inspired by chessground, with drag-and-drop, premoves, arrows, overlays, and zero runtime dependencies

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors