---
html: true
title: Reactive Markdown
transitions: true
---

@include /nav.wd

<main>

# Reactive Markdown

This page opts into the runtime because it declares state. Static pages ship zero framework JavaScript.

:state count = 0

The count is { count }.

:button "Increment" -> count++

`:if` reads JavaScript-style comparisons — `==` `!=` `<` `<=` `>` `>=` `contains` — joined with `and`, `or`, and `not`. This chain reacts to the counter above, no handler wired by hand:

:if count >= 3 and count <= 5
Counter is in the sweet spot (3–5).
:else if count > 5
Counter is over the top.
:else
Counter is warming up.
:endif

:state open = false

:button "Show details" -> open = true
:button "Hide details" -> open = false

:if open
The details are visible now.
:else
The details are hidden.
:endif

## Keyed loops over real objects

The same `@loop` that unrolls static data also drives reactive lists. Items are objects, bindings use dotted paths, and the runtime patches the list by key instead of re-rendering it:

:state todos = [{"id": 1, "title": "Route pages", "owner": "PM"}, {"id": 2, "title": "Ship tiny runtime", "owner": "Dev"}]

@loop todos into todo
- **{ todo.title }** — { todo.owner }
@endloop

:button "Add todo" -> todos += {"id": 3, "title": "Live compile", "owner": "Testing"}

## Multi-branch conditionals + reactive classes

`:if … :else if … :else … :endif` picks one of several branches per row, and a container class can react to the row too: `.class when <predicate>` toggles a class from the same whitelist `@loop … where` uses. Each card below picks one of three badges *and* tags itself `.is-done` / `.is-blocked`, all reactive:

<style>
.task-row { border-left: 4px solid #9991; padding-left: .6rem; }
.task-row.is-done { border-left-color: #2e9e5b; }
.task-row.is-blocked { border-left-color: #d2453f; }
</style>

:state tasks = [{"id": 1, "title": "Draft the spec", "done": true, "blocked": false}, {"id": 2, "title": "Wire the runtime", "done": false, "blocked": true}, {"id": 3, "title": "Write the demo", "done": false, "blocked": false}]

@loop tasks into task
::: card .task-row .is-done when task.done .is-blocked when task.blocked
**{ task.title }** —
:if task.done
done ✓
:else if task.blocked
blocked ✗
:else
in progress …
:endif
:::
@endloop

:button "Add a finished task" -> tasks += {"id": 4, "title": "Ship it", "done": true, "blocked": false}

Each row picks one of three states from a single `:else if` chain, and its left border color comes from a reactive `.class when …` — plain Markdown, no component, no ternary, no JavaScript.

## Live filtering — search in pure Markdown

`@loop … where` filters a list, and because the predicate reads a `:state` value, the loop re-filters live as you type. `:bind` is the input bound two-way to that state. Type below and watch the list narrow — no event handlers, no JavaScript:

:state products = [{"id": 1, "name": "Aurora Lamp", "price": 49}, {"id": 2, "name": "Briza Fan", "price": 39}, {"id": 3, "name": "Cove Speaker", "price": 89}, {"id": 4, "name": "Dune Mug", "price": 12}, {"id": 5, "name": "Ember Kettle", "price": 64}]
:state cart = []
:state q = ""

:bind q placeholder="Search products…"

@loop products into p where p.name contains q
::: card .product
**{ p.name }** — ${ p.price }
:button "Add to cart" -> cart += p
:::
@endloop

The predicate whitelist is compile-time validated — item fields, declared state, numbers, and strings only. An item-only predicate (say `where p.price < 50`) would instead filter at build time and ship zero JavaScript.

### …and an effect that watches the search

`:effect <state> -> <actions>` runs actions whenever the watched state changes — for side effects past deriving state. This one counts how many times the search box above has changed:

:state searches = 0
:effect q -> searches++

The search has changed **{ searches }** time(s). Type in the box above and this updates on its own — no handler wired by hand.

### …and an editable cart

A `:button` inside a loop can act on its own row. **Add to cart** carries the row into another list; **Remove** drops a line — a whole add/remove flow with no JavaScript:

@loop cart into line
::: card .line
{ line.name } — ${ line.price }
:button "Remove" -> cart remove line
:::
@endloop

## Section-scoped state

Two sections can each own a state value named `tally` without colliding. Buttons resolve to the nearest scope:

::: section #left .tally-card
:state tally = 0

### Left section

The left tally is { tally }.

:button "Bump left" -> tally++
:::

::: section #right .tally-card
:state tally = 10

### Right section

The right tally is { tally }.

:button "Bump right" -> tally++
:::

Static pages like the docs stay runtime-free.

</main>
