Why I Built the Plywise Chessboard Library From Scratch
In this piece
I wanted to use the board from Lichess.
When I started building Plywise, Chessground was the obvious place to look. I liked what it could do. The license did not fit the package I wanted to ship: Chessground is distributed under GPL-3.0-or-later, while I needed an MIT-licensed component.
I installed one alternative and tried it. It worked, but it did not feel as polished or as quick as I wanted for Plywise. I did not run a fair comparison, so I am describing my experience rather than claiming that one board is universally faster.
I had reached a common library decision:
- accept a component that did not fit;
- spend time adapting it to fit;
- or build the small thing I needed.
I chose to build the smaller component I needed.
The board is an 8×8 playground
Canvas seemed promising: the board has only 64 squares, and its fixed 8×8 geometry makes a drawing surface easy to imagine.
For this board, Canvas would have added a second rendering model, a separate hit-testing path, and more decisions about how Plywise should observe and control it.
I wanted a plain board with a clean API. The board needed to display a position, respond to pointer input, animate pieces, and expose the interaction data Plywise needs to make chess-rule decisions.
The board should not compute chess.
Plywise owns chess rules and game state. The renderer owns pointer-coordinate conversion, input validation, DOM reconciliation, and visual positions. Those computations stop before chess meaning: the renderer never decides whether a move is legal.
Plywise already owns that work:
- parsing FEN and PGN;
- applying chess rules;
- calculating legal destinations;
- maintaining the game tree;
- deciding what an annotation means;
- accepting or rejecting a move intention.
The renderer receives that result and draws the board the player sees and uses.
Chessground already showed me this kind of boundary. I built because Plywise needed the right license, control, and scope.
The smallest useful contract
The core package accepts a position as its input.
The contract looks like this:
Plywise rules and state
│
│ Position, destinations, marks, annotations
▼
Plywise Chessboard
│
│ InteractionEvent
▼
Plywise decides what happens nextWhen a player presses a piece, the renderer reports a selection. When the player drags to a destination, it reports a move intention. Each event records the attempted action and its location. Plywise’s rules layer decides what that event means for the game.
The caller supplies the legal destinations and handles each event. The caller can send the next position or call the controller’s approved move method after accepting a move. The caller can leave the position unchanged after rejecting it, because the renderer never changes authoritative state on its own.
For example, the core seam is small enough to show directly:
import {
createChessboard,
type Chessboard,
type Destinations,
type InteractionEvent,
type Position,
} from "@plywise/chessboard";
import "@plywise/chessboard/style.css";
function mountBoard(
host: HTMLElement,
position: Position,
destinations: Destinations,
): Chessboard {
let board: Chessboard;
board = createChessboard(host, {
position,
interaction: {
destinations,
onEvent(event: InteractionEvent) {
if (event.type === "move") board.move(event.from, event.to);
},
},
});
return board;
}Here, destinations comes from Plywise’s rules layer. The same contract handles castling and promotion: Plywise decides the legal move and resulting position, and the renderer receives the resulting state. If keyboard input matters, interaction.keyboard: true adds cursor navigation while keeping the event callback and ownership boundary intact.
Imagine a drag from e2 to e4. The board resolves the pointer coordinates, checks that e4 appears in the supplied destinations, and emits an event such as { type: "move", from: "e2", to: "e4", origin: "drag" }. Plywise then applies its rules and produces the next position. A promotion changes the piece in that position; castling updates both pieces. The renderer receives the resulting map and renders it.
The board can handle both moves through the same path. The rules layer handles side to move, en passant, check, promotion choice, castling rights, and game metadata. The coordinate pair carries intent; Plywise supplies the meaning.
Those inputs give the renderer a complete contract and let the product evolve without adding chess rules to the board.
The direction of data stays explicit:
- state flows from the product into the board;
- interaction intent flows from the board back to the product.
The same ownership applies to annotations. The board draws an arrow or circle and reports the gesture. Plywise interprets it as engine output, a user’s mark, a training hint, or something to ignore.
The controller itself stays small:
createChessboard(host, config)mounts the board;set(update)forwards controlled changes;move(from, to)applies a move the caller has approved;destroy()removes the DOM subtree and releases the instance.
Plywise remains the single source of truth for chess state. The visual component receives positions and reports intentions; it does not maintain a second chess model.
Performance by refusing unnecessary work
The first useful version felt fast, close to the board implementation I had already built inside Plywise.
The repository has a reproducible benchmark script and a committed report. Those results belong to a particular package version, browser, machine, and test setup; they do not establish a comparison with other boards.
The implementation keeps the renderer’s work small and targeted.
The checkerboard is CSS, so the renderer creates piece nodes and only the marks it needs. Arrows and circles share one SVG annotation layer.
A normal move reuses the existing piece node. The renderer changes its visual coordinates, and CSS handles the transition. A capture removes the captured piece while the moving piece keeps its identity. When a new update arrives during a transition, it retargets that transition instead of queueing another animation.
That identity rule is easy to miss when looking at a screenshot. It is one of the reasons the board feels like a board instead of a sequence of rebuilt images. A move changes the position of a piece that already exists.
Pointer input stays direct. Pointer Events handle mouse, touch, and pen through one path, while pointer capture keeps a drag attached after the pointer leaves its bounds. The renderer calculates the square from the current board rectangle and orientation instead of searching the DOM for the square below the pointer.
The React package creates one core renderer instance, forwards prop changes through it, and keeps the latest interaction callback available without recreating the board. React describes the board; the core renderer owns its DOM, so pointer movement does not trigger a React render.
These choices keep the renderer scoped to the job of an 8×8 board. Other chess products may need different boundaries; this one does not need a general-purpose game engine in its rendering layer.
A library that is easy to install
The framework-agnostic package is the simplest place to start:
npm install @plywise/chessboardIt has zero runtime dependencies. Import the core renderer and its opt-in stylesheet, pass a position, and connect the interaction callback to the rules layer already in your application.
React applications can install the thin adapter alongside the core:
npm install @plywise/chessboard @plywise/chessboard-reactThe adapter follows the same contract. It creates the core renderer and presents it as a declarative component at the application boundary. React does not own the board’s individual squares or pieces.
The live API reference includes runnable stories for controlled moves, presentation marks, annotations, layers, and gesture colours. The Playground covers the wider configuration, including themes and piece sets. Use the reference for complete configuration and TypeScript types.
When this boundary fits
Plywise Chessboard fits when your application already has a position and needs a controlled surface for displaying it.
This ownership model fits when:
- your application owns FEN or PGN parsing;
- your rules layer calculates legal destinations;
- your product needs to keep the game tree and metadata;
- you want to decide which interactions become moves;
- you need the board to work without tying its core to a UI framework;
- you want a React adapter without putting drag state in React.
Choose a different boundary if you want one widget to parse notation, calculate legal moves, manage premoves, own the game tree, and interpret every annotation. In that design, the board becomes the product.
When the board emits a move intention, the application must accept or reject it and keep the rules in its own layer.
The same question as storage, at a different boundary
In my earlier article, I looked at what a chess database needs to store. The useful question was smaller than “How do we preserve every character of the notation?” A machine often needs the move, the position, and the context required to reconstruct the rest.
For a related example of keeping data work in its own layer, I wrote about building a PGN merger in F# with streaming input and explicit outcomes.
The storage article asks what a machine needs to keep. The board asks what a renderer needs to receive:
Which position and interaction data does the renderer need?
It needs pieces on squares, orientation, display marks, caller-owned annotations, and enough input to turn a gesture into an intention. The rules remain in the application.
The lesson
For Plywise, the cleanest chessboard API is the one that refuses to play chess.
Plywise can change its parser, game model, engine integration, annotation metadata, or storage format while the board keeps the same rendering contract. A different application can supply its own rules layer.
The goal was a board that felt fast in use and stayed simple to integrate. Its clear ownership boundary is what makes the component reusable.
If your application already owns the chess, install the package and see whether this ownership boundary fits. Start with @plywise/chessboard, add @plywise/chessboard-react when you need the adapter, and use the documentation for the full API.
Filed under
Explore this subject