Examples
Each of these is running on this page, and the code beside it is the code that is running. New to weft? Start with the small ones under this shop. Set up a project when you want to try one yourself.
A whole small shop
A checkout form from warp, so the shop now has a labelled field, a switch and a submit button.
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.
An add-to-cart button
A product card. inCart is a piece of state: a value the page shows and can change.
Cart 0
import { state } from '@weft-js/core'import { Badge, Button, Card } from '@weft-js/warp'export default function Product() { const inCart = state(0) return ( <Card eyebrow="Coffee" title="House blend, 250 g"> <p>Cart <Badge count>{inCart.value}</Badge></p> <Button onClick={() => inCart.value++}>Add to cart</Button> </Card> )}- Try this
- Press Add to cart and watch the badge count up.
- Why it matters
- If you have written React, this already reads right. The difference is that Product ran once, so there are no hook rules or dependency lists to think about.
A basket that adds itself up
A quantity, plus shipping and a total that are worked out from it. Shipping turns free at three items.
1 × $12 · shipping $4
Total $16
import { derived, state } from '@weft-js/core'import { Button, Card } from '@weft-js/warp'export default function Basket() { const price = 12 const qty = state(1) const shipping = derived(() => (qty.value >= 3 ? 0 : 4)) const total = derived(() => price * qty.value + shipping.value) return ( <Card title="Your basket"> <p>{qty.value} × $12 · shipping ${shipping.value}</p> <p><strong>Total ${total.value}</strong></p> <Button onClick={() => qty.value++}>Add one</Button> <Button emphasis="ghost" tone="neutral" onClick={() => (qty.value = 1)} > Reset </Button> </Card> )}- Try this
- Press Add one twice. At three items shipping drops to $0 and the total changes with it.
- Why it matters
- You say what the total is once, and it can never go stale. Most bugs in forms and carts come from keeping such values in sync by hand.
Passing data to a child
Tip owns the bill. Price is a separate component that only displays the number it is given.
Each of you pays $20.00
import { state } from '@weft-js/core'import { Button, Card } from '@weft-js/warp'function Price(props: { dollars: number }) { return <strong>${props.dollars.toFixed(2)}</strong>}export default function Tip() { const bill = state(40) return ( <Card title="Split the bill"> <p>Each of you pays <Price dollars={bill.value / 2} /></p> <Button onClick={() => (bill.value += 10)}>Add a $10 dish</Button> </Card> )}- Try this
- Press Add a $10 dish. Price updates, though neither component ran again.
- Why it matters
- Passing a value to a child looks the same as in React, and the child stays up to date without re-rendering.
A list that keeps its rows
A to-do list where each row has a key, so weft knows which row is which.
import { state } from '@weft-js/core'import { Button, Card, Checkbox } from '@weft-js/warp'const start = ['Buy beans', 'Grind', 'Brew', 'Drink'].map(id => ({ id }))export default function Tasks() { const tasks = state(start) const later = () => { const [first, ...rest] = tasks.value tasks.value = [...rest, first!] } return ( <Card title="Today"> {tasks.value.map(t => <Checkbox key={t.id}>{t.id}</Checkbox>)} <Button emphasis="ghost" tone="neutral" onClick={later}> Do the first one later </Button> </Card> )}- Try this
- Tick Grind, then press Do the first one later a few times. The tick stays with its row.
- Why it matters
- Rows are moved, not rebuilt, so what is inside a row, such as a tick or typed text, survives a reorder.
Saving to a server
A save button that waits 800 ms, standing in for a real request, and disables itself meanwhile.
import { state } from '@weft-js/core'import { Badge, Button, Card, Spinner } from '@weft-js/warp'const save = () => new Promise(done => { setTimeout(done, 800) })export default function Profile() { const saving = state(false) const saves = state(0) const go = async () => { saving.value = true await save() saves.value++ saving.value = false } return ( <Card title="Profile"> <Badge tone="positive">Saves: {saves.value}</Badge> <Button disabled={saving.value} onClick={go}> {saving.value ? <Spinner /> : 'Save changes'} </Button> </Card> )}- Try this
- Press Save changes, and try pressing again while it spins.
- Why it matters
- Async code is plain async and await. State you change after an await updates the page like any other.
A search filter
A list of cities and a box that narrows it. The matches are worked out from what you type.
6 of 6
import { derived, state } from '@weft-js/core'import { Card, Input, Tag } from '@weft-js/warp'const CITIES = ['Lisbon', 'Oslo', 'Osaka', 'Lima', 'Lagos', 'Porto']export default function Trips() { const query = state('') const found = derived(() => { const q = query.value.toLowerCase() return CITIES.filter(c => c.toLowerCase().includes(q)) }) return ( <Card title="Where to?"> <Input label="Search cities" value={query.value} onInput={e => (query.value = e.currentTarget.value)} /> <p>{found.value.length} of {CITIES.length}</p> <div data-part="tags"> {found.value.map(c => <Tag key={c}>{c}</Tag>)} </div> </Card> )}- Try this
- Type os, then clear the box.
- Why it matters
- Filtering is one line of ordinary JavaScript. The list updates on every key without any wiring from you.
A live market table
A table of 1,000 prices. When live, one random price changes about 60 times a second.
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
- 100
import { onMount, state } from '@weft-js/core'import { Button, Card } from '@weft-js/warp'const rows = Array.from({ length: 1000 }, (_, i) => ({ id: i, price: state(100),}))export default function Market() { const live = state(false) onMount(() => { const beat = setInterval(() => { if (live.value) rows[(Math.random() * 1000) | 0]!.price.value++ }, 16) return () => clearInterval(beat) }) return ( <Card title="Market, 1,000 symbols"> <Button onClick={() => (live.value = !live.value)}> {live.value ? 'Pause' : 'Go live'} </Button> <ol data-part="prices" tabindex="0" aria-label="Prices"> {rows.map(r => <li key={r.id}>{r.price.value}</li>)} </ol> </Card> )}- Try this
- Press Go live, then scroll the table or use the rest of this page.
- Why it matters
- A live dashboard usually forces a framework to compare the whole table on each tick. Here each number has its own update, so one change costs one text write.
Hover and ticks shared across tabs
Two panes showing one list. Two kinds of state cross between them: what is ticked is data, a box synced to every tab, and hover, focus and press are UI state, one interaction per row, drawn by warp.
import { defineState, derived, state, type State } from '@weft-js/core'import { interaction } from '@weft-js/core/interaction'import { Card, Checkbox } from '@weft-js/warp'// What's ticked is data: one box each, read by both panes, and with sync: 'tabs' by every tab.const trip = defineState('demo-trip', () => ({ flights: state(false, { persist: 'local', sync: 'tabs' }), pack: state(false, { persist: 'local', sync: 'tabs' }),}))function Row(props: { id: string; text: string; done: State<boolean> }) { // Hover, focus and press are UI state: one key is one row, here, in other tabs and on other clients. const row = interaction({ share: () => `demo:${props.id}` }) const note = derived(() => { if (row.hover.value) return 'here' const { hover, pressed } = row.remote.value if (pressed) return 'pressed elsewhere' return hover ? `${hover} elsewhere` : '' }) return ( <Checkbox ui={row} hint={note.value} checked={props.done.value} onChange={() => { props.done.value = !props.done.value }}> {props.text} </Checkbox> )}export default function UiState() { return ( <div data-part="panes"> {['Ana', 'Ben'].map(who => ( <Card key={who} title={who}> <Row id="flights" text="Book flights" done={trip.flights} /> <Row id="pack" text="Pack" done={trip.pack} /> </Card> ))} </div> )}- Try this
- Tick a row: both panes tick. Hover a row: both panes light. Open this page in a second tab and do the same there: this tab follows, and says how many are elsewhere.
- Why it matters
- Hover stops being a pile of handlers and becomes a value you can read, draw and share. Your own hover never fires for someone else, so logic on it cannot echo.
Two views of one list
Ana and Ben see the same list. Here both panes are in your browser and share the same state, so nothing crosses a network.
import { state } from '@weft-js/core'import { Button, Card, Checkbox } from '@weft-js/warp'// One list, two views of it. In a real app it comes from a server `query`.const task = (id: number, text: string) => ({ id, text, done: state(false),})const todos = state([task(1, 'Book flights'), task(2, 'Pack')])const add = (who: string) => { const id = todos.value.length + 1 todos.value = [...todos.value, task(id, `From ${who}`)]}export default function Shared() { return ( <div data-part="panes"> {['Ana', 'Ben'].map(who => ( <Card key={who} title={who}> {todos.value.map(t => ( <Checkbox key={t.id} checked={t.done.value} onChange={(e: Event & { currentTarget: HTMLInputElement }) => (t.done.value = e.currentTarget.checked)} > {t.text} </Checkbox> ))} <Button emphasis="ghost" tone="neutral" onClick={() => add(who)} > Add a task </Button> </Card> ))} </div> )}- Try this
- Add a task in either pane, then tick a box. It ticks in the other pane too.
- Why it matters
- Tasks and ticks show in both panes with no sync code. With @weft-js/start the list comes from a server query, and .live() keeps other visitors in step the same way.
A React habit, caught by the compiler
A real error from the weft compiler for state(props.value), which copies a prop once. That is fine in some frameworks and a silent bug here.
import { state } from '@weft-js/core'import { Alert, Button, Card } from '@weft-js/warp'import show from '../generated/showcase.json'export default function Habit() { const open = state(false) return ( <Card title="weft build"> <Button onClick={() => (open.value = !open.value)}> {open.value ? 'Hide the error' : 'Build the React habit'} </Button> {open.value && ( <Alert tone="critical" title={show.habit.code}> {show.habit.message} </Alert> )} {open.value && <p>{show.habit.fix}</p>} </Card> )}- Try this
- Press the button to see the message the compiler gives.
- Why it matters
- Mistakes you bring from other frameworks stop the build with a reason and a fix, instead of shipping and going stale later. Your agent reads the same message.
A form from warp
A billing form: email field, switch and button, all from warp, the UI kit that ships with weft.
import { state } from '@weft-js/core'import { Button, Card, Input, Switch } from '@weft-js/warp'export default function Billing() { const saved = state(false) const submit = (e: Event) => { e.preventDefault() saved.value = true } return ( <Card title="Billing"> <form onSubmit={submit}> <Input label="Billing email" type="email" /> <Switch defaultChecked>Send a receipt</Switch> <Button type="submit">{saved.value ? 'Saved' : 'Save'}</Button> </form> </Card> )}- Try this
- Hover the field and the button, then press Tab to move through them.
- Why it matters
- Forms, dialogs, tables and menus are in the box, keyboard support included, so you do not choose and wire up a component library.
Chat that survives a cut wire
A real WebSocket to this site's server, in a room made just for you. The code is the page; the channel and its authorize check are in src/server/chat.ts.
import { skip } from '@weft-js/start'import { state } from '@weft-js/core'import { Badge, Button, Card, Input } from '@weft-js/warp'import { wire } from '../lib/wire'import { room } from '../server/chat'export default function Chat() { const joined = state(false) const up = state(true) const draft = state('') // getRandomValues, not randomUUID: a page on plain http (a LAN address) has no randomUUID. const id = crypto.getRandomValues(new Uint32Array(4)).join('-') const chat = room.connect(() => (joined.value ? { roomId: id } : skip)) const said = chat.collect('said', { max: 50, key: m => m.id }) const send = (e: Event) => { e.preventDefault() void chat.send('say', { text: draft.value || 'Hello' }) draft.value = '' } const join = () => { wire(true) joined.value = true } const toggle = () => { up.value = !up.value wire(up.value) } return ( <Card title="Chat"> <Badge>{chat.status.value}</Badge> {!joined.value && <Button onClick={join}>Join</Button>} {chat.status.value === 'unavailable' && <p>This site runs on functions, which can't hold the chat's socket. Run it with <code>weft dev</code> to try it.</p>} {joined.value && chat.status.value !== 'unavailable' && ( <form onSubmit={send}> <ol data-part="said"> {said.value.map(m => ( <li key={m.id}><b>{m.who}</b> {m.text}</li> ))} </ol> <Input label="Message" value={draft.value} onInput={e => (draft.value = e.currentTarget.value)} /> <Button type="submit" disabled={!up.value}>Send</Button> <Button type="button" emphasis="ghost" tone="neutral" onClick={toggle} > {up.value ? 'Cut the wire' : 'Restore the wire'} </Button> </form> )} </Card> )}- Try this
- Join and send a message. While the server is replying, cut the wire, wait a few seconds, then restore it. The replies you missed arrive in order.
- Why it matters
- Reconnecting and catching up take no code here: status, resume and the message list come with channel. A channel without authorize is an error, not a bug you find later.
A slow page with an instant shell
A real page from this server, in a frame. Its data takes as long as you choose. The code is that page.
import { Boundary, resource } from '@weft-js/core'import { Card, Skeleton } from '@weft-js/warp'import type { LoadArgs, Meta, RouteProps } from '@weft-js/start'import './slow.css'const later = <T,>(ms: number, value: T) => new Promise<T>(done => { setTimeout(done, ms, value) })export const meta: Meta = { title: 'Streaming a slow page', description: 'A product card whose stock and reviews stream in as they load: the page answers at once and each slow part fills itself in.' }export const load = ({ request }: LoadArgs) => { const asked = new URL(request.url).searchParams.get('ms') const ms = Math.min(Number(asked) || 1500, 3000) return { reviews: later(ms, ['Great beans', 'Arrived fast']), stock: later(ms / 2, 12), }}function Stock(props: { n: Promise<number> }) { const n = resource(() => props.n) return <p>{n.value} in stock</p>}function Reviews(props: { list: Promise<string[]> }) { const r = resource(() => props.list) return <ul>{(r.value ?? []).map(t => <li key={t}>{t}</li>)}</ul>}export default function Slow(props: RouteProps<typeof load>) { return ( <Card data-site="framed" title="House blend, 250 g"> <Boundary pending={<Skeleton />}> <Stock n={props.data.stock} /> </Boundary> <Boundary pending={<Skeleton />}> <Reviews list={props.data.reviews} /> </Boundary> </Card> )}- Try this
- Pick 3 s. The card appears at once, stock fills in at 1.5 s and reviews at 3 s.
- Why it matters
- load returns its promises without waiting, so the server sends the page first and streams each value in as it's ready. Each Boundary says what to show until then.
The shop, itemised
What the shop app in the repo evals sends to the browser, in compressed JavaScript. Measured by evals/warp-exit.mjs with brotli; the app and the results are in the repo.
weft and the shop6.02 KB
warp components it uses6.6 KB
JavaScript sent12.61 KB
import show from '../generated/showcase.json'export default function Receipt() { const { weft, kit, total } = show.receipt return ( <figure data-part="receipt"> <p style={`--share:${(weft / total) * 100}%`}><span>weft and the shop</span><b>{weft} KB</b></p> <p style={`--share:${(kit / total) * 100}%`}><span>warp components it uses</span><b>{kit} KB</b></p> <p data-part="total"><span>JavaScript sent</span><b>{total} KB</b></p> </figure> )}- Try this
- Compare it with the download sizes on the home page, which measure a larger dashboard.
- Why it matters
- You pay for the components you use. A page that imports a button doesn't bring the rest of warp with it.
Ask the app
A small todo app above its reactive graph. The JSON is what graph() from @weft-js/core/devtools returns. Development builds only: this frame is one.
import { derived, onCleanup, state } from '@weft-js/core'import { render } from '@weft-js/core/dom'import { graph } from '@weft-js/core/devtools'function Todos() { const todos = state(['Buy beans', 'Brew']) const done = state(0) const left = derived(() => todos.value.length - done.value) return ( <main> <button onClick={() => (todos.value = [...todos.value, `Task ${todos.value.length + 1}`])}>Add a task</button> <button disabled={left.value === 0} onClick={() => done.value++}>Finish one</button> <ul>{todos.value.map(t => <li key={t}>{t}</li>)}</ul> <p>{left.value} left</p> </main> )}// What an agent reads, here or through `weft mcp`: every named value, what it reads and what reads it.function Graph() { const lines = state<string[]>([]) const was = new Map<string, string>() const timer = setInterval(() => { lines.value = graph().map(n => { const line = JSON.stringify({ name: n.name, value: n.value, reads: n.reads, feeds: n.feeds }) const lit = was.has(n.name) && was.get(n.name) !== line was.set(n.name, line) return (lit ? '*' : ' ') + line }).filter(l => !l.includes('"Graph')) }, 300) onCleanup(() => clearInterval(timer)) return <pre>{lines.value.map(l => <div key={l} class={l[0] === '*' ? 'lit' : ''}>{l.slice(1)}</div>)}</pre>}render(() => <><Todos /><Graph /></>, document.getElementById('app')!)- Try this
- Press Add a task, then Finish one. Only the lines that changed light up.
- Why it matters
- An agent sees the same JSON through weft mcp, with your real names, so it can check what depends on what instead of guessing.