If you've ever needed to generate a chess diagram programmatically — for a blog, a PDF report, or a static site — you've probably hit the same wall I did.
Most solutions require either a browser environment, a headless Chromium instance, or a canvas polyfill that pulls in half of npm. For something as simple as "turn a FEN string into an image," that's a lot of overhead.
So I extracted the rendering core from my chess editor into a standalone package: @chessvision-org/chess-vision. Zero dependencies. Works in Node.js, Deno, Bun, and the browser. No DOM, no canvas, no network calls.
The basics
npm install @chessvision-org/chess-vision
import { generateDiagram } from '@chessvision-org/chess-vision';
const svg = generateDiagram({
fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
size: 400,
showCoords: true,
});
svg is a self-contained SVG string — inline it in HTML, write it to a file, embed it in a PDF. Pieces are inlined as SVG paths (CBurnett / Lichess style), so no external resources are needed.
generateDiagram throws if the FEN string is invalid. Validate first if you're working with untrusted input:
import { generateDiagram, validateFEN, getFENValidationError } from '@chessvision-org/chess-vision';
if (!validateFEN(fen)) {
console.error(getFENValidationError(fen)); // e.g. 'Board must have 8 ranks'
} else {
const svg = generateDiagram({ fen, size: 400 });
}
Real use cases
Static site or blog
If you write about chess and want diagrams in your posts, generate them at build time:
import { writeFileSync } from 'fs';
import { generateDiagram } from '@chessvision-org/chess-vision';
const positions = [
{
fen: 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4',
file: 'italian.svg',
},
{
fen: 'rnbqkb1r/pp3ppp/3ppn2/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 0 6',
file: 'sicilian.svg',
},
];
for (const { fen, file } of positions) {
writeFileSync(`./diagrams/${file}`, generateDiagram({ fen, size: 480, showCoords: true }));
}
The output SVGs embed directly in Markdown, MDX, or HTML with no image hosting needed.
Server-side rendering
Generate diagrams on request without spinning up a browser:
import express from 'express';
import { generateDiagram, validateFEN } from '@chessvision-org/chess-vision';
const app = express();
app.get('/diagram', (req, res) => {
const fen = String(req.query.fen ?? '');
if (!validateFEN(fen)) return res.status(400).send('Invalid FEN');
const svg = generateDiagram({ fen, size: 400, showCoords: true });
res.setHeader('Content-Type', 'image/svg+xml');
res.send(svg);
});
Astro / Next.js at build time
// In an Astro component or Next.js getStaticProps
import { generateDiagram } from '@chessvision-org/chess-vision';
const svg = generateDiagram({
fen: 'r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3',
size: 360,
showCoords: true,
flipped: false,
});
// Render directly — no img tag, no src, no upload
// <Fragment set:html={svg} /> ← Astro
// <div dangerouslySetInnerHTML={{ __html: svg }} /> ← React (SVG is from your own code, not user input)
Themes and customization
The package ships 20 built-in board themes (classic, ocean, wood, forest, navy, marble, and more):
import { generateDiagram, getBoardTheme, listThemeIds, themeCoordinateColor } from '@chessvision-org/chess-vision';
listThemeIds();
// → ['classic', 'brown', 'wood', 'sand', 'slate', 'marble', 'blue', 'ocean',
// 'green', 'forest', 'mint', 'purple', 'lavender', 'red', 'coral',
// 'sunset', 'pink', 'burgundy', 'navy', 'ice']
const theme = getBoardTheme('ocean')!;
// → { name: 'Ocean', light: '#c9e4f5', dark: '#4a90a4' }
const svg = generateDiagram({
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
size: 400,
lightSquare: theme.light,
darkSquare: theme.dark,
showCoords: true,
coordColor: themeCoordinateColor(theme), // → 'white' or 'black', whichever is more legible
});
Or pass any hex colors directly:
const svg = generateDiagram({
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
size: 400,
lightSquare: '#e8f4f8',
darkSquare: '#7eb8da',
flipped: true, // Black's perspective
showFrame: true, // thin outer frame
coordColor: 'white', // coordinate label color
label: 'Starting position', // aria-label on the SVG
});
FEN utilities
The package also ships a full FEN parser if you need to work with positions directly:
import {
parseFEN,
boardToFEN,
movePiece,
getPieceAt,
countPieces,
materialBalance,
findKing,
STARTING_FEN,
} from '@chessvision-org/chess-vision';
// Parse to an 8×8 matrix (row 0 = rank 8, row 7 = rank 1)
const board = parseFEN(STARTING_FEN);
board[7][4]; // → 'K' (white king on e1)
getPieceAt(board, 'e1'); // → 'K'
getPieceAt(board, 'd8'); // → 'q'
// Move a piece — pure, returns a new board, never mutates
const next = movePiece(board, 'e2', 'e4');
boardToFEN(next); // → updated piece placement string
countPieces(board); // → { P: 8, N: 2, B: 2, R: 2, Q: 1, K: 1, p: 8, … }
materialBalance(board); // → 0 (equal material)
findKing(board, 'w'); // → 'e1'
Full FEN record
Parse and serialize all six FEN fields:
import { parseFENRecord, buildFENRecord, toggleActiveColor } from '@chessvision-org/chess-vision';
const record = parseFENRecord('rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 5 12');
// → { board, activeColor: 'b', castling: 'KQkq', enPassant: 'e3', halfmove: 5, fullmove: 12 }
buildFENRecord(record);
// → 'rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 5 12'
toggleActiveColor(record).activeColor; // → 'w'
Why SVG
- Scales to any resolution without quality loss
- Self-contained — 23 piece sets inlined as paths, no external resources
- Embeds directly in HTML and most PDF libraries
- Converts to raster easily with
sharpif you need PNG
If you need high-res PNG for print, the web app (chessvision.org) handles that — up to 1200 DPI with accurate physical dimensions in centimeters.
Links
- npm: @chessvision-org/chess-vision
- GitHub: chessvision-org/chess-vision-utils
- License: AGPL-3.0
Open to feedback on the API — if something feels awkward to use or there's a use case I haven't covered, I'd like to know.
Top comments (0)