Skip to main content

Overview

The Lichess client is built with TypeScript, Snabbdom (virtual DOM), and Sass. The UI is organized as a pnpm monorepo workspace with 30+ packages in the ui/ directory.

Technology Stack

  • TypeScript: Type-safe JavaScript
  • Snabbdom: Lightweight virtual DOM library
  • Sass: CSS preprocessing
  • esbuild: Fast JavaScript bundler
  • pnpm: Fast, disk-efficient package manager

UI Directory Structure

ui/
├── .build/          # Build system source code
├── @types/          # TypeScript type definitions
├── analyse/         # Analysis board UI
├── bits/            # Small reusable UI components
├── build            # Build script
├── challenge/       # Challenge UI
├── chart/           # Chart components
├── cli/             # CLI tools
├── dasher/          # User menu
├── lib/             # Shared library code
├── lobby/           # Lobby UI
├── msg/             # Messaging UI
├── puzzle/          # Puzzle UI
├── round/           # Live game UI
├── site/            # Core site UI
├── test             # Test runner script
├── tournament/      # Tournament UI
├── tsconfig.base.json  # Base TypeScript config
└── ...

Workspace Structure

The UI is a pnpm monorepo defined in /pnpm-workspace.yaml:
packages:
  - 'ui/*'
Each package in ui/ has its own package.json describing:
  • Source files
  • Dependencies (internal and external)
  • Build configuration

Internal Dependencies

Packages depend on each other using workspace references:
{
  "dependencies": {
    "lib": "workspace:*",
    "common": "workspace:*"
  }
}

Building the UI

Build Script

The ui/build script is the main entry point for building UI assets:
ui/build --help

Watch Mode

Start the build system in watch mode to continuously rebuild on changes:
ui/build -w
When changes compile successfully, you’ll see:
✓ Created manifest.6f3a9d2e.json
The manifest lists public JavaScript and CSS assets. The server communicates updated URLs in subsequent responses.
Just reload your browser to see the changes - no server restart needed!

Building Specific Packages

Build only specific packages:
ui/build analyse puzzle

Production Build

Create an optimized production build:
ui/build --prod

Clean Build

Remove build artifacts and rebuild:
ui/build --clean

Package Configuration

Each package’s package.json includes a custom build property describing how assets are generated.

Bundle Property

Defines JavaScript modules to create as entry points:
{
  "build": {
    "bundle": "src/**/analyse.*ts"
  }
}
This matches files like:
  • src/analyse.ts
  • src/analyse.nvui.ts
  • src/analyse.study.ts
Naming rule: Source filenames must be prefixed by their package name.

Bundle with Inline Scripts

For critical rendering path code:
{
  "build": {
    "bundle": [
      "src/site.*Embed.ts",
      {
        "module": "src/site.ts",
        "inline": "src/site.inline.ts"
      }
    ]
  }
}
Inline scripts are injected into <script> tags to avoid FOUC (Flash of Unstyled Content).

Sync Property

Defines filesystem copies for assets:
{
  "build": {
    "sync": {
      "node_modules/*stockfish*/*.{js,wasm}": "/public/npm"
    }
  }
}
Useful for:
  • Copying npm packages to /public/npm
  • Linking local package versions during development

Hash Property

Defines assets to content-hash for CDN caching:
{
  "build": {
    "hash": [
      "/public/lifat/background/montage*.webp",
      "/public/npm/*",
      "/public/javascripts/**"
    ]
  }
}
Creates symlinks in /public/hashed/ with content-based filenames.

Import Resolution

TypeScript (tsc)

Uses exports in package.json for type checking:
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": {
        "source": "./src/index.ts",
        "default": "./dist/index.js"
      }
    },
    "./ceval": {
      "types": "./dist/ceval/index.d.ts",
      "import": {
        "source": "./src/ceval/index.ts",
        "default": "./dist/ceval/index.js"
      }
    }
  }
}
Import barrel exports:
import { type X, Y, Z } from 'lib/ceval';

esbuild

Uses only the source value for bundling:
{
  "exports": {
    "./boo/*": {
      "import": {
        "source": "./src/boo/*.ts"
      }
    }
  }
}
esbuild performs code splitting to create shared chunks across modules.

Development Workflow

1
Install Dependencies
2
cd ui
pnpm install
3
Start Watch Mode
4
In one terminal:
5
ui/build -w
6
Start the Server
7
In another terminal:
8
./lila.sh
run
9
Make Changes
10
Edit TypeScript or Sass files in ui/*/src/
11
The build system will automatically:
12
  • Detect changes
  • Rebuild affected modules
  • Update the manifest
  • Generate new hashed filenames
  • 13
    Reload Browser
    14
    Refresh your browser to see the changes.

    Testing

    Run UI tests with the test runner:
    ui/test
    
    See Testing for details.

    Key Packages

    lib

    ui/lib/ Shared library code:
    • Common utilities
    • Chess engine interface
    • WebSocket handling
    • xhr helpers

    common

    ui/common/ Common UI components and helpers.

    analyse

    ui/analyse/ Analysis board interface:
    • Move tree navigation
    • Computer evaluation display
    • Study interface

    round

    ui/round/ Live game interface:
    • Move input
    • Clock display
    • Game controls (resign, draw, takeback)

    puzzle

    ui/puzzle/ Puzzle training interface:
    • Puzzle presentation
    • Solution verification
    • Rating updates

    site

    ui/site/ Core site UI:
    • Navigation
    • User menu (dasher)
    • Notifications

    Working with Assets

    Images and Fonts

    Static assets go in /public/:
    public/
    ├── images/
    ├── fonts/
    ├── sounds/
    └── piece/  # Chess piece SVGs
    
    Reference in Sass:
    background-image: url('../images/logo.svg');
    

    Stylesheets

    Sass files in each package:
    ui/analyse/css/
    ├── _analysis.scss
    ├── _board.scss
    └── analyse.scss  # Entry point
    

    Linked Packages

    For development on external packages (e.g., chessground, pgn-viewer):
    1
    Clone the Package
    2
    cd ~/projects
    git clone https://github.com/lichess-org/chessground
    cd chessground
    pnpm install
    
    4
    cd ~/projects/lila
    pnpm link ~/projects/chessground
    
    5
    Use Sync in Watch Mode
    6
    The sync property in package.json will copy linked package changes:
    7
    ui/build -w  # Automatically syncs linked packages
    

    Build System Internals

    The build system source is in ui/.build/:
    ui/.build/
    ├── src/
    │   ├── main.ts        # Entry point
    │   ├── build.ts       # Build orchestration
    │   ├── bundle.ts      # esbuild bundling
    │   ├── sync.ts        # File syncing
    │   ├── hash.ts        # Content hashing
    │   └── ...
    └── package.json
    
    Runs with:
    node --experimental-strip-types --no-warnings src/main.ts
    

    Troubleshooting

    Changes Not Appearing

    1. Check build output for errors
    2. Clear browser cache (hard refresh: Ctrl+Shift+R)
    3. Verify manifest was updated in /public/compiled/

    Build Errors

    # Clean and rebuild
    ui/build --clean
    
    # Check TypeScript errors
    cd ui/analyse
    pnpm tsc --noEmit
    

    pnpm Issues

    # Clear pnpm cache
    pnpm store prune
    
    # Reinstall dependencies
    rm -rf node_modules
    pnpm install
    

    Node Version

    Ensure Node.js v24.11.1+:
    node -v
    

    Next Steps

    Additional Resources

    Build docs developers (and LLMs) love