v0.2.4 · specification

The web language
written for agents.

Lith is TypeScript, JSX and Prisma-style queries plus five declarations. An AI agent builds a correct, full-stack web app in about a quarter of the tokens it spends on Next.js, and the compiler keeps every colour and size on your design system.

1 fileper app
2.4ktokens to learn it
0 kBJS on static pages
todos.lithfull-stack
table Todo own {
  title: string(1..200)
  done: boolean = false
  created: Date = now()
}

page "/" {
  const todos = db.todo.findMany({ orderBy: [{ done: "asc" }] })
  return <main>
    <form action={addTodo}>
      <input name="title" /><button>Add</button>
    </form>
    <ul>{todos.map(t => <li>
      <input type="checkbox" checked={t.done}
        onChange={() => setDone(t.id, !t.done)} />
      {t.title}
    </li>)}</ul>
  </main>
}

function addTodo(title: string) {
  db.todo.create({ data: { title } })
}

function setDone(id: int, done: boolean) {
  db.todo.update({ where: { id }, data: { done } })
} 
01 · Benchmarks

Less to write. Less to read. Fewer retries.

Six reference tasks, each written by an expert in Lith and in five popular web stacks: Next.js, SvelteKit, Laravel, Django and Rails. Tokens are counted with the o200k_base tokenizer: what a model actually pays.

2.8–4.2×
fewer tokens to write
195 vs 537–825 per app −64 to −76%
6files
for all six apps
other stacks: 31–37 −81 to −84%
166lines
of source in total
Rails 363 · Next.js 611 −54% to −73%
20/ 20
unseen apps passed
blind models, card only 100%

Mean tokens per task, by stack

Baseline stackLith
Next.jsTypeScript · 32 files
825
4.2×
SvelteKitTypeScript · 34 files
775
4.0×
LaravelPHP · 31 files
615
3.2×
DjangoPython · 32 files
552
2.8×
RailsRuby · 37 files
537
2.8×
Lithv0.2.4 · 6 files
195
—

Tokens per task

Next.jsLith
Counter islandT02
229
114
2.0×
Todo CRUDT03
1,193
340
3.5×
Login & logoutT04
1,076
177
6.1×
JSON CRUD APIT07
744
38
19.6×
Per-user notesT09
1,078
233
4.6×
Optimistic starT13
630
266
2.4×

Write tokens are the mean of 6 expert solutions per stack (Lith: the v0.2.4 reference solutions). Files and lines count everything written for a task, starting from each framework's standard scaffold. The blind result: fresh models that had only the 2,402-token Lith card built 4 hidden apps in each of 5 runs, 20 of 20 passing after compiler-guided fixes (v0.2 release check).

02 · Same app, side by side

One file instead of a framework's worth.

A todo app with per-user data, validation, a detail page and a 404. In Lith, the schema, pages, server logic and the server/client split all live together, and the compiler infers which code runs where.

TypeScript / Next.js

  • app/
  • page.tsxserver
  • actions.ts"use server"
  • todo-controls.tsx"use client"
  • todos/[id]/
  • page.tsxserver
  • lib/
  • db.tsprisma
  • prisma/
  • schema.prismaschema
6 files · 146 lines1,193 tok
T03_app.lithLith
table Todo own {
  title: string(1..200)
  done: boolean = false
  created: Date = now()
}

page "/" {
  const todos = db.todo.findMany({ orderBy: [{ done: "asc" }, { created: "asc" }] })
  return <main>
    <h1>Todos</h1>
    <form action={addTodo} class="flex-row">
      <input name="title" />
      <button>Add</button>
    </form>
    <ul>
      {todos.map(t => <li>
        <input type="checkbox" checked={t.done} onChange={() => setDone(t.id, !t.done)} />
        <a href={`/todos/${t.id}`} class={t.done ? "line-through" : ""}>{t.title}</a>
        <button onClick={() => deleteTodo(t.id)} class="text-danger">Delete</button>
      </li>)}
    </ul>
  </main>
}

page "/todos/:id" (id: int) {
  const todo = db.todo.findUniqueOrThrow({ where: { id } })
  return <main>
    <h1>{todo.title}</h1>
    <p class="text-muted">Created {todo.created}</p>
  </main>
}

function addTodo(title: string) {
  db.todo.create({ data: { title } })
}

function setDone(id: int, done: boolean) {
  db.todo.update({ where: { id }, data: { done } })
}

function deleteTodo(id: int) {
  db.todo.delete({ where: { id } })
}
1 file · 43 lines340 tok · 3.5× fewer
03 · Highlights

Every feature had to earn its tokens.

Lith came out of a measured design lab: four competing designs, blind tests with models that had never seen the language, and a rule that every feature must lower total cost.

/01

Looks like TypeScript because it is

The TS 5 TSX grammar plus contextual declarations. Models already know it: 64 blind programs had zero syntax-caused errors.

table page api crud layout state effect auth
/02

Tables with refinement types

Ranges and lengths live in the type and are checked on every write and every form. Migrations for new columns are automatic.

title: string(1..200) views: int(0..) = 0
/03

Ownership as a keyword

own scopes every query, update and delete to the logged-in user. Other users' rows act as missing.

table Note own { body: string }
/04

Inferred server / client split

A function that touches db or me runs on the server; the browser calls it automatically. No "use server", no API glue.

<form action={addComment}>
/05

Forms that handle themselves

Inputs fill typed parameters by name. Every check runs at once, and failures re-show the form with messages next to each field. It works without JS.

fail({ body: "No links" })
/06

A REST API in one line

List, get, create, patch and delete with validation, JSON errors and correct status codes. Plain api routes return any Response.

crud "/api/products" Product
/07

Compiled signals, tiny islands

state and effect replace hooks. Pages without islands ship 0 kB of JavaScript; the client runtime targets 10 kB gzipped.

state starred = task.starred
/08

Errors an agent can apply

lith check returns the first error as JSON with a ready patch, so a fix costs one short round instead of a debugging session.

{ "code": "E0110", "patch": … }
/09

Native to the web

Emits standard JS, HTML and CSS on Node 22. Import any npm package; export .d.ts types so TypeScript can call into Lith.

import { marked } from "marked"
04 · Design systems, enforced

A model can't invent a colour in Lith.

Every Lith app takes its look from a design system: a standard token file with primitives and semantic tokens. The rules are in the compiler, not in a prompt, so a model follows them in markup, CSS and server code alike, and every violation comes back with the fix.

frontend

Raw values don't compile

Hex colours, px sizes, arbitrary [values] and Tailwind's palette are errors in classes and in <style> alike. Each error carries a safe patch to the nearest semantic token, measured as colour distance (ΔE00) or px.

<p class="text-gray-500 p-4"> E0620 raw value → text-muted · p-inset
frontend

Primitives stay in the token file

The palette and scales exist only as targets of semantic tokens. App code names roles (surface, text-muted, danger), never blue-700, so a rebrand is a token-file edit.

<button class="bg-blue-700"> E0621 primitive → bg-accent (aliases blue-700)
frontend + backend

New values only by design

Code cannot declare tokens or set CSS variables. A token added to the file that nearly equals an existing one (ΔE00 < 2, within 1px) is an error, and lith check lists every design-system change on its own.

:root { --brand: #e11d48 } E0624 design values come only from the token file
backend

The same rules on the server

Emails, OG images, PDFs and API responses style themselves from tokens.color.text, never a literal. Raw colours and lengths in CSS strings and style objects are E0620 there too.

html: `<h1 style="color:#333">` E0620 → ${tokens.color.text}
backend

White-label without breaking contrast

A data slot lets the database set a brand colour per tenant. The compiler derives its on-colour, always at least 4.5:1, so text on the brand colour meets WCAG AA.

table Org { brand: color | null } <html tokens={{ brand: org.brand }}> → bg-brand text-on-brand
any system

Your tokens, any modes

W3C Design Tokens and Figma variable exports load as they are: any names, light, dark, high-contrast or brand modes. Output is plain CSS variables; switching modes needs no JavaScript.

<html data-theme="system"> follows the OS: dark, high-contrast, light
0raw values, primitives, extra tokens or broken modes in 9 blind design-system tasks
+122card tokens for the whole design-system section; the compiler holds the rest
87semantic tokens style this site, checked on every build
05 · Principles

Built on what the measurements said, not on taste.

Token counts: o200k_base · 6 measured tasks · 5 baseline stacks · blind validation n = 8