Core Concepts

Components & Templating

A .wizz file is one component: a template with exactly one root element, plus up to one each of <script>, <wizz:head>, and <wizz:style>. The compiler turns it into DOM-creation and update instructions — there is no template interpreter in the browser.

Anatomy of a component

src/components/Profile.wizz
<script>
  // 1. Optional script: imports, props, state, handlers, hooks.
  import Avatar from "./Avatar.wizz";

  export let user;               // prop (read-only here)
  let expanded = false;          // reactive state

  function toggle() {
    expanded = !expanded;
  }
</script>

<wizz:head>
  <title>{user.name} - Profile</title>
</wizz:head>

<article class="profile">        <!-- 2. Exactly one root element -->
  <Avatar src={user.avatar} />
  <h2>{user.name}</h2>
  <button on:click={toggle} aria-expanded={expanded}>Details</button>
</article>

<wizz:style>
  .profile { display: grid; gap: 1rem; }
  h2 { margin: 0; }              /* scoped: other h2s are untouched */
</wizz:style>
  • Template — exactly one native root element, per the language contract. Wrap siblings in a single element.
  • <script> — top-level let is reactive state, export let declares a prop, named functions are handlers, and onMount/onDestroy register hooks with no import.
  • <wizz:head> — only <title>, <meta>, and <link>.
  • <wizz:style> — raw CSS, scoped to this component. A plain <style> element is rejected with a pointer to <wizz:style>.

Template expressions

Curly braces accept a deliberately small, statically analyzable grammar: identifiers, member access, string and number literals, and strict equality. Function calls and ternaries fail with WIZZ-P033. Treat anything outside that subset as unsupported even when it happens to parse — compute it in the script and reference the result by name.

Expressions.wizz
<script>
  let user = { name: "Ada", role: "admin" };
  let items = ["a", "b", "c"];

  // Derive anything non-trivial in the script, then reference it by name.
  let itemCount = items.length;
  let isAdmin = user.role === "admin";
  let greeting = "Welcome back, " + user.name;
</script>

<section>
  <h2>{greeting}</h2>                 <!-- identifier -->
  <p>{user.name}</p>                  <!-- member access -->
  <p>{"v1.0"} · {42}</p>               <!-- string / number literals -->
  {#if user.role === "admin"}         <!-- strict equality -->
    <p>{itemCount} items</p>
  {/if}
</section>

Text output escapes &, <, and >; attribute values additionally escape ". To show a literal angle bracket in prose, interpolate a string literal such as {"<script>"} — HTML entities are not decoded in templates.

Dynamic attributes

Bind an attribute by making its entire, unquoted value an expression: class={cardClass}. value, checked, and disabled update as DOM properties; every other name updates as an HTML attribute.

Attributes.wizz
<script>
  let name = "Ada";
  let selected = true;
  let busy = false;
  let active = true;
  // Compose multi-part values in the script:
  let cardClass = active ? "card is-active" : "card";
  let profileHref = "/users/" + name.toLowerCase();
</script>

<div class={cardClass}>
  <a href={profileHref}>Profile</a>
  <input value={name} checked={selected} disabled={busy} aria-label={name} />
</div>

Props

A child declares inputs with export let. The parent passes them as attributes on the component tag: static attributes pass strings, bare attributes pass true, and {expressions} pass values that re-evaluate whenever their dependencies change.

Greeting.wizz
<script>
  export let name = "Guest";   // default when the parent omits it
  export let count;            // bare declaration defaults to undefined
</script>

<p>Hello {name}, clicked {count} times</p>
App.wizz
<script>
  import Greeting from "./components/Greeting.wizz";
  let clicks = 0;

  function click() {
    clicks += 1;
  }
</script>

<main>
  <Greeting name="Ada" count={clicks} emphasized />
  <button on:click={click}>Click</button>
</main>
  • New values arrive through the child's setProps() and it rerenders in place — DOM identity and internal state survive.
  • Props are read-only in the child: assigning one is a compile error (WIZZ-G015).
  • An omitted prop keeps its default; passing undefined explicitly restores it. Undeclared attributes are ignored.

Live: props in action

This sandbox is two real Wizz components. The parent owns the state; the child Meter only receives props. Every value the child shows is computed by the parent, so the child stays a pure view.

Live · PropsDemo.wizz → Meter.wizz

Parent → child prop updates: 0 · the child is never remounted.

CPU budget40%
PropsDemo.wizz (excerpt)
<script>
  import Meter from "./Meter.wizz";

  let level = 40;
  let barStyle = "transform: scaleX(0.4)";

  function handleLevel(event) {
    level = Number(event.target.value);
    barStyle = "transform: scaleX(" + level / 100 + ")";
  }
</script>

<div class="props-demo">
  <input type="range" value={level} on:input={handleLevel} />
  <Meter label="CPU budget" value={level} barStyle={barStyle} />
</div>

Composition & imports

Import components with a default import of a relative .wizz path and render them as self-closing tags inside a native element. The build rewrites the import to the compiled .js path.

  • No children or slots — pass content through props, or compose with native elements around the tag.
  • No on: directives on component tags — pass a callback prop instead.
  • No components inside {#each} — render native markup in loops.
  • Destroying a parent destroys every imported child.
Toolbar.wizz
<script>
  import SearchBar from "./SearchBar.wizz";
  let query = "";

  // Children talk to parents through callback props:
  function handleSearch(next) {
    query = next;
  }
</script>

<div class="toolbar">
  <SearchBar onSearch={handleSearch} placeholder="Filter…" />
</div>

Scoped styles

Every element a component renders carries a generated data-wizz-s attribute, and each selector's terminal element gains it — so two components can both style h2 without colliding. @keyframes names are suffixed the same way. Global resets and tokens stay in App.css.

Page.wizz
<main>
  <Card />
  <h2>Page heading</h2>
</main>

<wizz:style>
  h2 { font-size: 24px; }
  /* compiles to: h2[data-wizz-s="s1ab2c3"] { font-size: 24px; } */

  @keyframes fade { from { opacity: 0; } }
  /* the name gains the scope suffix: @keyframes fade-s1ab2c3 */
</wizz:style>

<wizz:head> declares per-page metadata. Expressions evaluate at render time on the server and at mount on the client; the server delivers the head in the document and hydration adopts it. Head content follows navigation, not state — reactive head updates are out of scope.

Post.wizz
<script>
  export let post;
</script>

<wizz:head>
  <title>{post.title} — My Blog</title>
  <meta name="description" content={post.summary}>
  <link rel="canonical" href={post.url}>
</wizz:head>

<article>
  <h1>{post.title}</h1>
</article>

When several mounted components declare a <title>, the last in tree order wins with a development warning; <meta> and <link> concatenate. Keep the document shell free of its own <title> so a page's title is the only one.

Lifecycle & DOM rendering

Panel.wizz
<script>
  let width = 0;
  let observer = null;

  onMount(() => {
    // Runs after the component is attached, its children are mounted,
    // and the initial update has completed. Browser APIs are safe here.
    const node = document.querySelector(".panel");
    observer = new ResizeObserver(([entry]) => {
      width = Math.round(entry.contentRect.width);
    });
    observer.observe(node);
  });

  onDestroy(() => {
    // Runs before children are destroyed and the root is removed.
    observer.disconnect();
  });
</script>

<section class="panel">
  <p>{width}px wide</p>
</section>
  • onMount runs once after attach, child mounts, and the initial update. State assigned there flows through the batched scheduler.
  • onDestroy runs when destroy() is called — before children are destroyed and the root is removed.
  • Wizz removes every listener it attached through on:. Listeners you add to window or document are yours to remove.
  • Wizz owns the DOM it creates: do not reorder, remove, or replace its nodes while mounted.

Outside the router, every compiled module can be mounted by hand:

main.js
import mountComponent from "./dist/components/Counter.js";

const handle = mountComponent(document.querySelector("#slot"), { label: "Clicks" });
handle.setProps({ label: "Taps" });   // present when the component declares props
handle.destroy();                     // call exactly once

Rules the compiler enforces

RuleInsteadCode
A template needs a root elementWrap content in one native elementWIZZ-G001
One <wizz:head> / <wizz:style>Merge the blocksWIZZ-P013 / P014
No plain <style>Use <wizz:style>WIZZ-P020
No children on component tagsPass propsWIZZ-G008
No on: on component tagsPass a callback propWIZZ-G011
Component tags inside a native elementWrap the tagWIZZ-G010
Props are read-onlyCopy into a local letWIZZ-G015
No calls or ternaries in {...}Compute in the scriptWIZZ-P033
{#each} inside a native elementWrap the blockWIZZ-G002
One root element per {#each} bodyWrap the rowWIZZ-G003
No components / on: inside {#each}Native markup + delegationWIZZ-G004 / G006

A few behaviors compile cleanly but are worth knowing:

  • {#if} is chosen at mount. Toggle visibility of reactive UI with a bound class instead, and keep dynamic bindings out of branches that may not be taken.
  • Avoid empty-string text interpolations in server-rendered components: an empty text node does not survive HTML parsing, so hydration falls back to a client render. Initialize display strings to something visible.
  • Keep scripts LF-only. The hydration walk compares template whitespace exactly, and the browser normalizes CRLF.
  • Use const for locals inside helper functions. The state scanner treats let declarations as component state.