Browser Code
Pages work without JavaScript; a script and directives in the same .wisp file add behavior. No bundler. {…} is Rust (server); a quoted directive value and {:…} are JavaScript (browser).
<button on:click="count++">Clicked {:count} times</button>
<script>
let count = 0
</script>
The Script
A bare <script> (no attributes; one per file) works in pages, layouts and components and runs once per place the file is shown. <script type|src> stays plain HTML. Errors point at the .wisp line.
- Top-level
lets are state: assigning one, or changing an object, array,MaporSetin it (todos.push(t),todo.done = true), redraws. Aletset to a string, number or boolean and never assigned is a constant. importlines move to the module head (import c from 'https://esm.sh/canvas-confetti').- npm:
wisp add canvas-confetti[@1.2.3|tag]pins it inpackage.json(wisp remove x; no Node);import c from 'canvas-confetti'(also'pkg/sub','@scope/pkg') in scripts andsrc/lib. Dev loads esm.sh;wisp builddownloads into.wisp/npmand the binary serves/_app/c/npm/. A package not inpackage.json, or a range (^1.0), is a build error.
Runes
A write redraws only the bindings that read what changed (no virtual DOM); the script runs once; writes batch in a microtask.
<p>{:done} of {:todos.length} done</p>
{:#each todos as todo (todo.id)}
<li class:done="todo.done" on:click="todo.done = !todo.done">{:todo.text}</li>
{:/each}
<script>
let todos = $state([{ id: 1, text: 'Tea', done: false }])
let done = $derived(todos.filter((t) => t.done).length)
$effect(() => { document.title = `${done} done` })
</script>
| Rune | Meaning |
|---|---|
let x = $state(v) | Deep state (a plain let x = v is the same). |
$state.raw(v) | Changes only when assigned. |
$state.snapshot(x) | Plain copy. |
$derived(expr), $derived.by(fn) | Recomputed when read after an input changed; assigning is a build error. |
$effect(fn) | After the DOM is drawn and when what it read changes; may return a cleanup. |
$effect.pre(fn) | Same, before the DOM is drawn. |
$effect.root(fn) | Effects made in fn end with the function it returns, not with the component. |
$effect.tracking() | Whether the running code tracks what it reads (inside an effect or a binding). |
let { a, b = 1, c: d, ...rest } = $props() | Component props with browser defaults (absent or null); needs no {@props}. |
$bindable(default) | A prop a parent may bind:; with $props() only these bind. |
$inspect(a, b) | Logs on change; gone in release. |
$cart | Store cart's .value, tracked; $cart = x sets it. |
- A class's
$state/$derivedfields make its instances state.untrack(fn)reads untracked. A misplaced rune is a build error. - Deep state tracks plain objects, arrays, maps, sets and such classes; for a
Dateor other instance assign again (d = d). x === eredraws only where the answer changes (class:on="selected === row.id"redraws two rows).
TypeScript
<script lang="ts">, src/lib/*.ts ('$lib/x') and +page.ts. Types are stripped in place (lines and columns stay); no compiler. Code-producing TS is a build error saying what to write:
| Not allowed | Write |
|---|---|
enum Color { Red } | const Color = { Red: 'red' } as const |
namespace | a module |
constructor(private x: number) | x: number; constructor(x: number) { this.x = x } |
import fs = require('fs') | import fs from 'fs' |
wisp check --types also type-checks with the app's TypeScript (npm install -D typescript, or WISP_TSC naming a tsc; else skipped). Server values are typed by Rust (Vec<Item> is Item[], a #[derive(Json)] type an interface; hand-written Json is unknown).
Environment Variables
env.PUBLIC_NAME in browser code (script, directive, src/lib, +page.js) is written in at build (no env object exists).
- Values: the build's environment, then
.envfor names it lacks;wisp devrebuilds when.envchanges. - Only
PUBLIC_names reach the browser (env.DATABASE_URLis a build error). An unset one is a build error too: set it even empty (PUBLIC_FLAG=). envread whole orenv[name]is an error; a variable of your own namedenvis just that.- Server:
wisp::env("K").
Translations
t('cart.items', n) or t('hi', { name, count: n }) in a script or directive, no import; keys checked at build; the page sends only the messages its scripts use. src/lib code can't call t. Message files: /docs/design.
Directives
| Syntax | Meaning |
|---|---|
on:click="count++" | Handler; a bare name (on:click="press") is called with the event. |
bind:value="q" / bind:checked="done" | Two-way. bind:value alone binds value. An undeclared name is declared as state (let q): live search needs no <script>. |
bind:group="size" | Radios (value) and checkboxes (array) sharing a name. |
bind:files bind:open bind:innerHTML bind:currentTime bind:paused… | Any property; the element's own event keeps it current. |
bind:clientWidth="w" | Sizes (clientWidth/Height, offsetWidth/Height, contentRect). |
bind:this="el" | Element into el. |
:hidden="!open" | Live attribute; false, null, undefined remove it. |
:text="name" | Live text. |
class:open="isOpen" | Toggle a class; class:open alone reads open. |
style:--x="x" | Style property; style:color alone reads color. |
class={:['card', { on }]} | Names from strings, arrays, truthy object keys. |
style={:{ color, fontSize: '2em' }} | Properties from an object. |
{:...attrs} | Each key an attribute (an on… function a listener). |
transition:fade | fade slide scale fly blur; options transition:fly="{ y: 20 }". |
in:fly / out:fade | Only in / only out. |
transition:spin | Your spin(el, options, { direction }) returning { duration, delay, easing, css: (t, u) => '…' } or { tick(t, u) }. |
use:tip="'Hello'" | Calls tip(el, 'Hello') and its update on change; may return a cleanup or { update, destroy }. |
use:portal="'#modal'" | Move the element there (bare: <body>). |
animate:flip | Animate moves in a keyed {:#each}. |
<input bind:value="query" on:keydown.enter="search" on:keydown.escape="query = ''">
<div on:click.outside="open = false">…</div>
<input on:input.debounce.300ms="search()">
<div on:keydown.ctrl.s.prevent.window="save">…</div>
Event Modifiers
.prevent .stop .once .self .capture .passive.window,.document: listen there..outside..debounce[.300ms](default 250 ms).- Keys:
.enter .escape .space .tab .backspace .delete .up .down .left .right .home .end .pageup .pagedown, a letter or digit. .ctrl .shift .alt .meta.
Unknown ones are a build error. A bound input starts from what the server rendered or the visitor already typed.
Is This Page Useful?