Gallery
Every example in the repository, captured running in a browser, beside the markup that made it.
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:h2: class=bx-title child={"Todos"} :!h2:
:span: class="bx-badge bx-badge-quiet" child={to_string(len(model.todos)) + " items"} :!span:
:!div:
:hr: class=bx-divider :!hr:
:ul: class=list
:for: todo in model.todos key to_string(todo.id)
:li: class=item
:button: class={{ mark(todo.done) }} on:click=Msg.Toggle(string_to_int(key, 0))
{{ todo.label }}
:!button:
:!li:
:!for:
:!ul:
:div: class=bx-row
:button: class="bx-btn bx-btn-primary" child={"add one"} on:click=Msg.Add :!button:
:!div:
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:h2: class=bx-title child={"Save a draft"} :!h2:
:span: class="bx-badge bx-badge-quiet" child={to_string(model.hits) + " saves"} :!span:
:!div:
:hr: class=bx-divider :!hr:
:form: on:submit=Msg.Save
:div: class=field
:label: class=bx-label child={"What are you writing?"} :!label:
:input: class=bx-input value={{ model.draft }} on:input=Msg.Typed(value) :!input:
:!div:
:button: class="bx-btn bx-btn-primary" child=save :!button:
:!form:
:p: class=bx-muted
draft: {{ model.draft }} — last saved: {{ model.saved }}
:!p:
:!div:
:props: model: Model
:!props:
:section: class=card
# Today
:p: child={to_string(model.left) + " left"} :!p:
:for: task in model.tasks key to_string(task.id)
:button: class={{ mark(task.done) }} on:click=Msg.Toggle(string_to_int(key, 0))
{{ task.label }}
:!button:
:!for:
:!section:
:props: model: Model
:!props:
:section: class=card
:div: class=top
# Today
:p:
:span: class=count
{{ to_string(open_count(model.tasks)) }} open
:!span:
of {{ to_string(len(model.tasks)) }} — written as a document, compiled to WebAssembly
:!p:
:!div:
:div: class=row-add
:input: value={{ model.draft }} placeholder="What needs doing?" on:input=Msg.Draft(value)
:!input:
:button: class=add on:click=Msg.Add
Add
:!button:
:!div:
:div: class=tabs
:button: class={{ tab_class(model, "all") }} on:click=Msg.Show("all")
All
:!button:
:button: class={{ tab_class(model, "open") }} on:click=Msg.Show("open")
Open
:!button:
:button: class={{ tab_class(model, "done") }} on:click=Msg.Show("done")
Done
:!button:
:!div:
:if: len(shown(model)) == 0
:p: class=empty child={"Nothing here. Add something above."} :!p:
:!if:
:ul: class=list
:for: task in shown(model) key to_string(task.id)
:li: class=item
:button: class={{ pick_class(task.done) }} on:click=Msg.Toggle(string_to_int(key, 0))
:span: class=box
:!span:
:span: class=text
{{ task.label }}
:!span:
:!button:
:!li:
:!for:
:!ul:
:div: class=foot
:span:
{{ to_string(open_count(model.tasks)) }} left
:!span:
:button: class=clear on:click=Msg.Clear
Clear done
:!button:
:!div:
:!section:
:props: model: Model
:!props:
# Hello {{ model.user }}
:p:
{{ to_string(model.unread) }} unread
:!p:
:button: on:click=Msg.Clear
clear
:!button:
:props: model: Model
:!props:
# Feed — {{ to_string(model.items) }} items
:button: on:click=Msg.Refresh
refresh
:!button:
:button: on:click=Msg.TogglePolling
toggle polling
:!button:
:props: model: Model
:!props:
# Status — {{ model.status }}
:button: on:click=Msg.Save
save
:!button:
:props: model: Model
:!props:
# Last — {{ model.last }}
:button: class=tap on:click=Msg.Got(value)
tap
:!button:
:input: type=text class=typed on:input=Msg.Got(value)
:!input:
:div: class=wheel on:wheel=Msg.Got(value)
wheel here
:!div:
:div: class=keys on:keydown=Msg.Got(value)
type here
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:div:
:h2: class=bx-title child={"Reorder while you type"} :!h2:
:p: class=bx-muted
Type in a row, then move it. The caret follows the ROW, because the key says which row it is.
:!p:
:!div:
:span: class="bx-badge bx-badge-quiet" child={to_string(model.moves) + " moves"} :!span:
:!div:
:hr: class=bx-divider :!hr:
:for: row in model.rows key to_string(row.id)
:div: class=row
:input: class="bx-input field" value={{ row.text }} on:input=Msg.Typed(string_to_int(key, 0), value)
:!input:
:button: class="bx-btn lift" child={"move up"} on:click=Msg.Lift(string_to_int(key, 0)) :!button:
:!div:
:!for:
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:div:
:h2: class=bx-title child={"A loop inside a loop"} :!h2:
:p: class=bx-muted child={"64 squares from two nested :for: blocks"} :!p:
:!div:
:span: class=bx-badge child={to_string(model.lit) + " lit"} :!span:
:!div:
:hr: class=bx-divider :!hr:
:div: class=board
:for: row in model.rows key to_string(row.y)
:div: class=rank
:for: cell in row.cells key to_string(cell.id)
:button: class={{ shade(cell) }} on:click=Msg.Toggle(string_to_int(key, 0)) :!button:
:!for:
:!div:
:!for:
:!div:
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:h2: class=bx-title child=Preferences :!h2:
:span: class="bx-badge bx-badge-quiet" child={summary(model)} :!span:
:!div:
:hr: class=bx-divider :!hr:
:div: class=field
:label: class=bx-label child=Size :!label:
:select: class=bx-select on:change=Msg.Size(value)
:option: value=small selected={{ model.size == "small" }} child=Small :!option:
:option: value=medium selected={{ model.size == "medium" }} child=Medium :!option:
:option: value=large selected={{ model.size == "large" }} child=Large :!option:
:!select:
:!div:
:fieldset: class="bx-fieldset field"
:legend: child=Tone :!legend:
:div: class=tones
:label: class=bx-check
:input: type=radio name=tone value=warm checked={{ model.tone == "warm" }} on:change=Msg.Tone(value) :!input:
:span: child=Warm :!span:
:!label:
:label: class=bx-check
:input: type=radio name=tone value=cool checked={{ model.tone == "cool" }} on:change=Msg.Tone(value) :!input:
:span: child=Cool :!span:
:!label:
:!div:
:!fieldset:
:div: class=field
:label: class=bx-check
:input: type=checkbox checked={{ model.agree }} on:change=Msg.Agree(value) :!input:
:span: child={"I agree to the terms"} :!span:
:!label:
:!div:
:div: class=field
:label: class=bx-label child=Notes :!label:
:textarea: class=bx-area on:input=Msg.Note(value)
{{ model.note }}
:!textarea:
:!div:
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:h2: class=bx-title child={"Draft"} :!h2:
:span: class="bx-badge bx-badge-quiet" child={model.status} :!span:
:!div:
:hr: class=bx-divider :!hr:
:textarea: class="bx-area draft" on:input=Msg.Typed(value)
{{ model.text }}
:!textarea:
:div: class=bar
:button: class="bx-btn bx-btn-primary" child=save on:click=Msg.Save :!button:
:button: class=bx-btn child=restore on:click=Msg.Restore :!button:
:button: class="bx-btn bx-btn-quiet" child=clear on:click=Msg.Clear :!button:
:!div:
:p: class=bx-muted
It survives a reload — `Store` and `Load` are the browser's own storage, and `clear` puts the caret
back in the field.
:!p:
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:div:
:h2: class=bx-title child={"Server time"} :!h2:
:p: class=bx-muted child={to_string(model.ticks) + " ticks, each one asked of the server"} :!p:
:!div:
:button: class="bx-btn bx-btn-primary" child={button_text(model)} on:click=Msg.Toggle :!button:
:!div:
:hr: class=bx-divider :!hr:
:p: class=face child={label(model)} :!p:
:p: class=bx-muted
A `pure` view cannot read a clock, and a tick carries no payload — so the time comes from where time
lives.
:!p:
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:div:
:h2: class=bx-title child={"Snake"} :!h2:
:p: class=bx-muted child={"Arrow keys. A tick, four global keys, and a list that moves every frame."} :!p:
:!div:
:span: class=bx-badge child={verdict(model)} :!span:
:!div:
:hr: class=bx-divider :!hr:
:div: class=board
:for: seg in model.body key to_string(seg.x) + "-" + to_string(seg.y)
:div: class=seg style={{ at(seg) }} :!div:
:!for:
:div: class=food style={{ at(model.food) }} :!div:
:!div:
:div: class=bx-row
:button: class="bx-btn bx-btn-primary" child={"new game"} on:click=Msg.Again :!button:
:!div:
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:h2: class=bx-title child=Order :!h2:
:span: class=bx-badge child={to_string(len(model.lines)) + " lines"} :!span:
:!div:
:for: line in model.lines key to_string(line.id)
:div: class=line
:button: class="bx-btn bx-btn-icon" child={"−"} on:click=Msg.Fewer(string_to_int(key, 0)) :!button:
:button: class="bx-btn bx-btn-icon" child={"+"} on:click=Msg.More(string_to_int(key, 0)) :!button:
:div:
:div: class=name child={line.name} :!div:
:div: class=each child={money(line.each) + " each × " + to_string(line.count)} :!div:
:!div:
:div: class=amount child={money(line_total(line))} :!div:
:!div:
:!for:
:hr: class=bx-divider :!hr:
:div: class=sum
:span: class=bx-muted child={"subtotal " + money(subtotal(model.lines)) + " · service 12.5% " + money(service(model.lines))} :!span:
:span: class="bx-figure amount" child={money(cart_total(model.lines))} :!span:
:!div:
:!div:
:div: class=bx-card
:h2: class=bx-title child={"Split the bill"} :!h2:
:p: class=bx-muted
Dividing money cannot lose a penny, so the shares are computed rather than rounded.
:!p:
:div: class=bx-row
:button: class=bx-btn child={"one bill"} on:click=Msg.Split(1) :!button:
:button: class=bx-btn child={"two ways"} on:click=Msg.Split(2) :!button:
:button: class=bx-btn child={"three ways"} on:click=Msg.Split(3) :!button:
:!div:
:p: class="shares bx-mono" child={shares(model)} :!p:
:!div:
:props: model: Model
:!props:
:div: class=bx-card
:div: class=bx-spread
:h2: class=bx-title child={"Noughts and crosses"} :!h2:
:span: class=bx-badge child={verdict(model)} :!span:
:!div:
:hr: class=bx-divider :!hr:
:div: class=grid
:for: square in model.squares key to_string(square.id)
:button: class={{ face(square) }} child={square.mark} on:click=Msg.Play(string_to_int(key, 0)) :!button:
:!for:
:!div:
:div: class=bx-row
:button: class=bx-btn child={"new game"} on:click=Msg.Again :!button:
:!div:
:!div:
What these are
Each one is in the collection for a reason — an edge, a combination nothing else covers, or a case that was
silently wrong until somebody wrote it down. Most of them found a defect when they were written, and
examples/README.md says which.
Nothing here is a mock-up. tools/gallery.mjs builds each component, serves it, mounts the real
.wasm in a real browser and screenshots the result — and then reads the page’s own DOM back to check the
state it was given actually took. A screenshot is the one kind of claim that cannot go stale loudly: the
picture keeps looking correct after the component stops working, because a picture is a file.
The styling is two halves, and that is the same split Tailwind and Bootstrap make. A component’s
===style.local holds what makes it itself — a board is 3.6rem squares, a snake segment is 18px, and star
scopes those rules to that component. Everything shared — type, colour, spacing, the shape of a button —
belongs to the page, and here it is one 166-line stylesheet. star never reads a class name, so swapping that
file for Tailwind’s output changes nothing about how any of this works.