Write React components in Lean, a typed functional language. Your components compile to JavaScript and run in the browser using React.
The appeal is familiar if you use TypeScript across the stack: your frontend and backend can share types and validation rules. Props, state, hooks, and components you can pass into other components remain the core UI building blocks.
v0.1 release · Get started · How-to guide · Agent skill
You’ll need Git, Node 22.13 or newer, and elan, the Lean version manager. Think of elan like nvm: it installs the Lean version selected by this project. Follow its installation instructions for your OS, then open a new terminal.
git clone --branch v0.1 https://github.com/theoriclabs/lean-react.git
cd lean-react
npm ci
npm run devOpen http://localhost:4173. The first build may download the Lean compiler. The examples run in browser memory, so you can try them immediately.
Start by editing the counter component. The dev server rebuilds when you save; refresh the browser to see the change.
import LeanReact
open LeanReact
structure CounterProps where
label : String
def Counter : Component CounterProps := component fun props => do
let count ← useState 0 "count"
pure <| DOM.button {
onPress := some (count.modify (fun value => value + 1))
} #[
text (props.label ++ ": " ++ toString count.value)
]Read this as a React function component:
structure CounterPropsdefines the props, like a TypeScript interface.useStategives youcount.value,count.set, andcount.modify. The last one works likesetCount(previous => previous + 1)."count"is a stable label for the hook.DOM.buttoncreates a button;#[...]contains its children.onPressruns the update when clicked.
A few syntax hints: fun value => ... is an arrow function, some supplies an optional prop, and ++ joins strings. ← reads the result of a hook; pure <| returns the rendered element.
The first-form tutorial walks through complete files, including compiling and mounting a component.
Pass an editor into a form. Pass a row component into a list. Share a custom hook between two layouts. These are ordinary values and functions, so you can change one piece without rewriting the whole screen.
The playground includes:
- A ticket workspace with interchangeable layouts and field editors.
- Collection forms that keep each row’s state when you reorder them and preserve invalid input while you edit.
- A context provider and consumer built in separate libraries that share live updates.
Shared domain definitions, called ontologies in this project, describe application data and its rules. For example, the same title validator can run in your form and on a Lean backend. Start with a type and a parsing function; add richer descriptions when your application needs them.
Use regular CSS files and className props. JavaScript and TypeScript can consume the generated components too.
Existing React libraries need an explicit binding between their props and Lean. There’s a working foreign-component example in the interop guide. shadcn and Tailwind aren’t configured out of the box.
This is an experimental release for trying Lean-authored frontends. The examples demonstrate working components, forms, asynchronous data loading, and shared domain rules. APIs can change. Only a subset of Lean compiles to JavaScript today; routing, React Server Components, and automatic JS/TS bindings are still ahead. See current support and limits.
To build your own screen, follow the how-to guide. If you’re using a coding agent, ask it to read SKILL.md first.
Application code lives in examples/; reusable framework code lives in engine/. Contributor checks, optional backend setup, and architecture details are in the development guide, backend guide, and vision.

