Skip to content
weftGitHub

The TypeScript framework for fast web apps.

Write components in TSX, as you would in React. weft compiles them into small, direct page updates, and comes with routing, data, a UI kit and errors your coding agent can act on.

Get startedSee examples

node packages/create-weft/scripts/local.mjs new my-appweft is not on npm yet, so run this from a checkout of the repo.

Here is a small shop, built one idea at a time. Each step adds a few lines, marked in the code, and each one is running.

You already know how to write this

A weft component is a function that returns TSX. Keep a value in state, show it, change it. There is no new template language to learn.

One product and an add-to-cart button. count is state: a value the page shows and can change.

Coffee, $120
import { state } from '@weft-js/core'import { Badge, Button, Card } from '@weft-js/warp'export default function Shop() {  const count = state(0)  return (    <Card title="Coffee, $12">      <Button onClick={() => count.value++}>Add to cart</Button>      <Badge count>{count.value}</Badge>    </Card>  )}
Try this
Press Add to cart. The badge counts up.
Why it matters
This is a plain function returning TSX, so what you know from React carries over. Shop runs once, so there are no hook rules or dependency lists.

warp draws, too

Canvas art in your theme's own inks. These run on this page, three at most moving at a time.

See the gallery

Values that keep themselves right

Say how a value is worked out, and weft keeps it current. The shop gets a total, and shipping that turns free at $36.

The total and shipping are new, marked in the code. Neither is set by hand: derived works each one out from count.

Coffee, $120

Shipping $0

Total $0

import { derived, state } from '@weft-js/core'import { Badge, Button, Card } from '@weft-js/warp'export default function Shop() {  const count = state(0)  const total = derived(() => count.value * 12)  const shipping = derived(() =>    total.value === 0 || total.value >= 36 ? 0 : 4,  )  return (    <Card title="Coffee, $12">      <Button onClick={() => count.value++}>Add to cart</Button>      <Badge count>{count.value}</Badge>      <p>Shipping ${shipping.value}</p>      <p><strong>Total ${total.value + shipping.value}</strong></p>    </Card>  )}
Try this
Add three coffees. Shipping appears at $4, then drops to $0 when the total reaches $36.
Why it matters
You state a rule once and it can never go stale. Carts and forms get most of their bugs from keeping values like these in sync by hand.

Only what changed gets updated

Most frameworks re-run your component and compare the result to find what changed. weft works that out when it compiles, so a change goes straight to the piece of the page it affects. The shop gets more products and a search box: one new value and one line of filtering, and everything that depends on it follows.

A search box. shown is derived from what you type, and the filter is one line of ordinary JavaScript.

Shop

0

0

0

Shipping $0

Total $0

import { derived, state } from '@weft-js/core'import { Badge, Button, Card, Input } from '@weft-js/warp'const products = [  { id: 1, name: 'Coffee', price: 12, count: state(0) },  { id: 2, name: 'Filters', price: 5, count: state(0) },  { id: 3, name: 'Grinder', price: 40, count: state(0) },]export default function Shop() {  const search = state('')  const shown = derived(() => {    const word = search.value.toLowerCase()    return products.filter(p => p.name.toLowerCase().includes(word))  })  const total = derived(() =>    products.reduce((sum, p) => sum + p.count.value * p.price, 0),  )  const shipping = derived(() =>    total.value === 0 || total.value >= 36 ? 0 : 4,  )  return (    <Card title="Shop">      <Input        label="Search"        value={search.value}        onInput={e => (search.value = e.currentTarget.value)}      />      {shown.value.map(p => (        <p key={p.id}>          <Button size="sm" onClick={() => p.count.value++}>            {p.name}, ${p.price}          </Button>          <Badge count>{p.count.value}</Badge>        </p>      ))}      <p>Shipping ${shipping.value}</p>      <p><strong>Total ${total.value + shipping.value}</strong></p>    </Card>  )}
Try this
Add a coffee, type gr, then clear the box. Your count is still there.
Why it matters
There is nothing to wire up: no effect, no event bus. Each count lives with its product, so it is still there when the row comes back.

A UI kit comes with it

The buttons and fields so far are warp, weft's own component library: 244 components, with keyboard support, themes and 651 icons. The shop gets a checkout.

A checkout form from warp, so the shop now has a labelled field, a switch and a submit button.

Shop

0

0

0

Shipping $0

Total $0

import { derived, state } from '@weft-js/core'import { Badge, Button, Card, Input, Switch } from '@weft-js/warp'const products = [  { id: 1, name: 'Coffee', price: 12, count: state(0) },  { id: 2, name: 'Filters', price: 5, count: state(0) },  { id: 3, name: 'Grinder', price: 40, count: state(0) },]export default function Shop() {  const search = state('')  const ordered = state(false)  const shown = derived(() => {    const word = search.value.toLowerCase()    return products.filter(p => p.name.toLowerCase().includes(word))  })  const total = derived(() =>    products.reduce((sum, p) => sum + p.count.value * p.price, 0),  )  const shipping = derived(() =>    total.value === 0 || total.value >= 36 ? 0 : 4,  )  const order = (e: Event) => {    e.preventDefault()    ordered.value = true  }  return (    <Card title="Shop">      <Input        label="Search"        value={search.value}        onInput={e => (search.value = e.currentTarget.value)}      />      {shown.value.map(p => (        <p key={p.id}>          <Button size="sm" onClick={() => p.count.value++}>            {p.name}, ${p.price}          </Button>          <Badge count>{p.count.value}</Badge>        </p>      ))}      <p>Shipping ${shipping.value}</p>      <p><strong>Total ${total.value + shipping.value}</strong></p>      <form onSubmit={order}>        <Input label="Email for the receipt" type="email" />        <Switch defaultChecked>Gift wrap</Switch>        <Button type="submit" disabled={total.value === 0}>          {ordered.value ? 'Order placed' : 'Place order'}        </Button>      </form>    </Card>  )}
Try this
Add something, press Tab to move through the controls, then place the order. The button stays off while the cart is empty.
Why it matters
Forms, dialogs, tables and menus are in the box with keyboard support, so you do not pick and wire up a component library.

Browse the components

One project, front to back

A real shop keeps its products in a database. With @weft-js/start, a server query lives beside your components, and each file in src/routes is a page. Database and sign-in are built in, so the whole app is one codebase.

// src/server/shop.tsexport const products = query({  tags: ['products'],  run: ({ ctx }) => ctx.db.select().from(schema.products),})// in Shop: the list now comes from the databaseconst list = products(){(list.value ?? []).map(p => <Row key={p.id} product={p} />)}
src/routes/index.tsx
the / page
src/routes/products/[id].tsx
the /products/:id page
src/server/shop.ts
queries and mutations
src/db/schema.ts
your tables

Read about full-stack weft

The same app in weft, Solid and React

These are three real production builds of one small app, running in your browser now: a counter and a grid of 1,000 cells. Press the buttons and compare what each one had to do.

weftsource

download size
3.2 KB
components re-run
0
small updates run
0
page changes
0
time per update
–

Solidsource

download size
4.2 KB
components re-run
0
small updates run
0
page changes
0
time per update
–

Reactsource

download size
50.2 KB
components re-run
0
small updates run
–
page changes
0
time per update
–

All three make the same page changes, because they show the same thing. The difference is the work behind it: React re-runs components to find those changes, and ships more code to do so. How this is measured: each update at 60 a second is timed on its own, with nothing watching the page, and the times are added up. One update in 30 runs untimed so its page changes can be counted. Time per update stays empty until a run has about five seconds in it, because a few clicks are too few to compare. On an Apple M3 Pro, 60 updates a second take 107 µs each in weft, 182 µs in Solid, 341 µs in React and 72 µs in hand-written DOM.

Speed, measured in Chromium

weft is faster than Solid in 4 of these 12 rows and slower in 1, marked in red. In the rest the rounds overlap. Times are milliseconds, the median of three rounds on an Apple M3 Pro on 2026-11-01, with Solid 1.9.7.

OperationweftSolidReactFastest
Create 1,000 rows22.920.624.2Too close
Replace 1,000 rows22.122.127.2Too close
Create 10,000 rows218.9212.8385.3Too close
Append 1,000 rows22.723.227.3Too close
Update every 10th row0.610.661.07Too close
Select a row<0.010.010.17weft
Swap two rows0.180.252.84Too close
Remove a row0.200.170.32Solid
Clear all rows2.672.674.11Too close
Update one cell of 10,000<0.01<0.011.07weft
Update every 10th of 10,0000.621.072.31weft
Update all 10,000 cells1.713.053.82weft

Pages load fast because there is less to download

The same dashboard app, built with a UI kit in each framework. Less JavaScript means pages are ready sooner, most of all on phones and slow connections.

  • weft + warp23.1 KB
  • Solid + Kobalte47.4 KB
  • React + Radix98.3 KB
  • React + MUI239.6 KB

JavaScript sent to the browser, compressed with brotli, from this repo's comparison app.

When your agent makes a mistake, weft catches it

The compiler stops the mistake before it ships and says how to fix it, in words your agent can act on.

The component works out its total once, so the number on the page can never change.

function Total(props: { count: number }) {  const total = props.count * 5  return <b>{total}</b>}
W001 props is read once when the component is set up, so this never updates.Total.tsx, line 2. Fix: Wrap in derived, move into watch, or untrack() if a snapshot is intended
function Total(props: { count: number }) {  const total = props.count * 5  const total = derived(() => props.count * 5)  return <b>{total}</b>  return <b>{total.value}</b>}
function Total(props: { count: number }) {  const total = derived(() => props.count * 5)  return <b>{total.value}</b>}
10

Count 2

Press +1. The count goes up, the total stays stuck.

Try this
Press +1 on step 1, then again on step 4.
Why it matters
The error carries the reason and the fix, so an agent can correct its own mistake before you read the code.

Build something

Make an app from a checkout, then work through Learn a chapter at a time.

Get startedRead the reference