Documentation Index Fetch the complete documentation index at: https://mintlify.com/lichess-org/lila/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Lichess’s frontend is built with TypeScript and Snabbdom , a lightweight virtual DOM library. The UI is organized as a pnpm monorepo with 35+ packages, each handling specific features. The architecture emphasizes performance, modularity, and real-time updates.
Technology Stack
Core Technologies
TypeScript 5.9+ : Type-safe JavaScript development
Snabbdom 3.5.1 : Minimal virtual DOM library for efficient rendering
esbuild : Ultra-fast JavaScript bundler
pnpm : Fast, disk-efficient package manager
Chessground : Custom chess board UI component
Sass : CSS preprocessing
Key Libraries
// package.json
{
"dependencies" : {
"@lichess-org/chessground" : "^10.0.2" ,
"@lichess-org/pgn-viewer" : "^2.5.9" ,
"snabbdom" : "3.5.1" ,
"chessops" : "^0.15" ,
"typescript" : "^5.9.3"
}
}
Monorepo Structure
The UI is organized as a pnpm workspace defined in /pnpm-workspace.yaml:
ui/
├── .build/ # Build system (esbuild configuration)
├── @types/ # TypeScript type definitions
├── analyse/ # Analysis board
├── round/ # Live game interface
├── lobby/ # Game lobby
├── tournament/ # Tournament UI
├── puzzle/ # Tactics trainer
├── study/ # Study/analysis sharing (via analyse)
├── site/ # Core site functionality
├── lib/ # Shared utilities and components
├── bits/ # Small reusable UI components
└── [30+ more packages]
Package Dependencies
Packages declare dependencies using workspace protocol:
// ui/analyse/package.json
{
"name" : "analyse" ,
"dependencies" : {
"lib" : "workspace:*" ,
"@lichess-org/chessground" : "^10.0.2" ,
"snabbdom" : "3.5.1"
}
}
Virtual DOM with Snabbdom
Why Snabbdom?
Lichess chose Snabbdom for its:
Minimal size : ~200 lines of core code
High performance : Fast diffing algorithm
Flexibility : Extensible module system
Simplicity : Easy to understand and debug
Snabbdom Basics
Snabbdom uses a hyperscript function h() to create virtual DOM nodes:
import { h , type VNode } from 'snabbdom' ;
// Create virtual DOM node
const vnode : VNode = h ( 'div.game-board' , {
attrs: { 'data-game-id' : gameId },
on: { click: handleClick }
}, [
h ( 'span.player' , playerName ),
h ( 'div.board' , renderBoard ())
]);
View Functions
UI components are pure functions returning VNodes:
// ui/analyse/src/view/main.ts
import { type VNode } from 'lib/view' ;
import type AnalyseCtrl from '../ctrl' ;
export default function ( deps ?: typeof studyDeps ) {
return function ( ctrl : AnalyseCtrl ) : VNode {
if ( ctrl . nvui ) return ctrl . nvui . render ( deps );
else if ( deps && ctrl . study ) return studyView ( ctrl , ctrl . study , deps );
else return analyseView ( ctrl , deps );
};
}
function analyseView ( ctrl : AnalyseCtrl , deps ?: typeof studyDeps ) : VNode {
const ctx = viewContext ( ctrl , deps );
return renderMain (
ctx ,
renderBoard ( ctx ),
renderTools ( ctx ),
renderControls ( ctrl ),
renderUnderboard ( ctx )
);
}
Rendering Cycle
State change : User action or server event updates controller state
Redraw trigger : Controller calls redraw() function
View render : View function generates new VNode tree
Diff & patch : Snabbdom diffs against previous VNode and patches DOM
// Typical controller pattern
export class AnalyseCtrl {
redraw : () => void ; // Injected by bootstrap
makeMove ( move : Move ) : void {
this . tree . addMove ( move );
this . redraw (); // Triggers re-render
}
}
UI Package Architecture
Core Pattern: MVC-style Controllers
Most UI packages follow an MVC-inspired pattern:
analyse/
├── src/
│ ├── ctrl.ts # Main controller class
│ ├── interfaces.ts # TypeScript interfaces
│ ├── view/
│ │ ├── main.ts # Root view function
│ │ ├── controls.ts # Sub-views
│ │ └── components.ts
│ ├── socket.ts # WebSocket integration
│ ├── boot.ts # Initialization
│ └── analyse.ts # Entry point
Controller Example
From ui/analyse/src/ctrl.ts:
import { TreeWrapper } from 'lib/tree' ;
import { CevalCtrl } from 'lib/ceval' ;
import { Socket } from './socket' ;
export default class AnalyseCtrl {
data : AnalyseData ;
tree : TreeWrapper ;
ceval : CevalCtrl ;
socket : Socket ;
ground : ChessgroundApi ; // Chess board
redraw : () => void ;
constructor ( opts : AnalyseOpts , redraw : () => void ) {
this . data = opts . data ;
this . redraw = redraw ;
this . tree = makeTree ( opts . treeParts );
this . ceval = new CevalCtrl ( opts . ceval , this );
this . socket = makeSocket ( opts . socketSend , this );
}
jump ( path : Tree . Path ) : void {
this . tree . setPath ( path );
this . updateBoard ();
this . redraw ();
}
private updateBoard () : void {
const node = this . tree . getCurrentNode ();
this . ground . set ({
fen: node . fen ,
lastMove: node . uci
});
}
}
Bootstrap Pattern
Each page bootstraps its UI from server-provided data:
// ui/analyse/src/boot.ts
import { init as snabbdomInit } from 'snabbdom' ;
import makeCtrl from './ctrl' ;
import view from './view' ;
export function boot ( opts : AnalyseOpts ) {
const patch = snabbdomInit ([ ... snabbdomModules ]);
let vnode : VNode ;
const redraw = () => {
vnode = patch ( vnode , view ( ctrl ));
};
const ctrl = makeCtrl ( opts , redraw );
const element = document . querySelector ( '.analyse' ) ! ;
vnode = patch ( element , view ( ctrl ));
}
// Server embeds bootstrap code:
// <script>LichessAnalyse.boot({...data})</script>
Shared Library (ui/lib)
The lib package provides common utilities used across all UI packages:
// ui/lib/src/index.ts exports:
// Functional helpers
export const prop = < T >( value : T ) : Prop < T > => { ... };
export const toggle = ( initial : boolean ) : Toggle => { ... };
export const defined = < T >( v : T | undefined ) : v is T => v !== undefined ;
// Async utilities
export const requestIdleCallback = ( f : () => void ) => { ... };
export const debounce = < F extends ( ... args : any []) => void >(
func : F , wait : number
) => { ... };
// View helpers
export { h , type VNode } from 'snabbdom' ;
export const onInsert = ( f : ( el : HTMLElement ) => void ) => { ... };
// ui/lib/src/game/
export const playable = ( game : Game ) : boolean =>
game . status . id < 25 && ! game . player . spectator ;
export const validUci = ( fen : string , uci : Uci ) : boolean => { ... };
export const fenToEpd = ( fen : string ) : string => { ... };
Client-side Stockfish integration: // ui/lib/src/ceval/
export class CevalCtrl {
start ( path : Tree . Path , nodes : Tree . Node []) : void {
this . worker . postMessage ({
type: 'start' ,
fen: nodes [ nodes . length - 1 ]. fen ,
moves: this . getMoves ( nodes )
});
}
}
Chessground Integration
Chessground is Lichess’s custom-built chess board component:
import { Chessground } from '@lichess-org/chessground' ;
import type { Api as ChessgroundApi } from '@lichess-org/chessground/api' ;
export function makeBoard ( element : HTMLElement ) : ChessgroundApi {
return Chessground ( element , {
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR' ,
orientation: 'white' ,
movable: {
free: false ,
color: 'white' ,
dests: new Map () // Legal moves
},
events: {
move : ( orig , dest ) => handleMove ( orig , dest )
}
});
}
Chessground features:
SVG-based piece rendering
Touch and mouse input
Premoves and conditional moves
Animation and highlighting
Mobile-optimized
Build System
The custom build system in ui/.build/ uses esbuild :
Package Configuration
Each package defines build configuration in package.json:
{
"name" : "analyse" ,
"build" : {
"bundle" : "src/**/analyse.*ts" ,
"sync" : {
"node_modules/*stockfish*/*.{js,wasm}" : "/public/npm"
},
"hash" : [ "/public/images/board/*.svg" ]
}
}
Build Properties
bundle : Entry points for esbuild
Matches analyse.ts, analyse.study.ts, etc.
Output: /public/compiled/analyse.[hash].js
sync : Copy assets to public directory
Useful for large files (Stockfish WASM)
Watch mode syncs on changes
hash : Content-hash assets for CDN caching
Creates symlinks in /public/hashed/
Manifest tracks hashes for server
Build Commands
ui/build # Build all packages
ui/build -w # Watch mode
ui/build analyse # Build specific package
ui/build --help # Show options
Code Splitting
esbuild automatically splits shared code into chunks:
public/compiled/
├── analyse.abc123.js # Analyse entry point
├── round.def456.js # Round entry point
└── lib.789xyz.js # Shared lib chunk
Browsers cache shared chunks across pages.
TypeScript Configuration
Base TypeScript config in ui/tsconfig.base.json:
{
"compilerOptions" : {
"target" : "ES2022" ,
"module" : "ES2022" ,
"moduleResolution" : "bundler" ,
"strict" : true ,
"noUncheckedIndexedAccess" : true ,
"paths" : {
"lib/*" : [ "./lib/src/*" ],
"bits/*" : [ "./bits/src/*" ]
}
}
}
Each package extends base config with package-specific settings.
State Management
Lichess doesn’t use Redux or similar state libraries. State is managed in controllers:
Local State Pattern
export class PuzzleCtrl {
// Direct properties
mode : 'play' | 'view' = 'play' ;
streak : number = 0 ;
// Reactive properties (Prop pattern)
loading = prop ( false );
showSolution = toggle ( false );
// Methods modify state and trigger redraw
solve () : void {
this . mode = 'view' ;
this . streak ++ ;
this . showSolution ( true );
this . redraw ();
}
}
Communication Between Components
Components communicate via:
Direct calls : Parent controller calls child methods
Callbacks : Child triggers parent callback
PubSub : lib/pubsub for global events
import { pubsub } from 'lib/pubsub' ;
// Publish event
pubsub . emit ( 'game.move' , move );
// Subscribe to event
pubsub . on ( 'game.move' , ( move ) => handleMove ( move ));
Keys : Use keys for list items to enable efficient reordering
Memoization : Cache expensive view computations
Lazy rendering : Defer off-screen content
// Keys for efficient list updates
const moves = game . moves . map (( move , i ) =>
h ( 'move' , { key: move . id }, renderMove ( move ))
);
Code splitting : Shared chunks cached across pages
Tree shaking : Dead code elimination
Content hashing : Immutable URLs for CDN caching
Lazy loading : Dynamic imports for large features
Browserslist targets modern browsers: "browserslist" : [
"Firefox >= 115" ,
"Chrome >= 112" ,
"Safari >= 13.1"
]
No polyfills for older browsers - encourages upgrades for security.
Testing
Unit tests use Node.js test runner:
ui/test # Run all tests
ui/test -w # Watch mode
ui/test winning # Run specific test file
Test files in ui/*/tests/**/*.test.ts
See Also