Frames, anchors & flow
This is what turns wiremark from "draws boxes" into "captures a user flow". By naming frames and linking them, you get an embedded navigation graph at no extra cost — and a way to compose shared app chrome once and reuse it.
Naming a frame: #id
A frame is named with a #id token on its Wireframe root. It is
HTML-anchor-like and unmistakable to parse:
Wireframe #login mobile
Wireframe #dashboard landscape
The name is optional, but you need one on any frame you want to link to.
Linking: to=#id
Any element can carry a to= property, which makes that element — or its
whole region — a clickable zone that navigates to a frame:
Button "Sign in" contained to=#dashboard
ListItem "Settings" to=#settings
Box * * to=#detail // an entire region is clickable
Card to=#product // the whole card navigates
The to= value is always a frame anchor.
Two deliberate constraints
These keep the flow model trivial:
to=targets frames only — never elements inside a frame. There is noto=#dashboard.settingsdeep-linking. The flow graph stays at the screen level.- The graph is inferred, never declared. There is no
from. The navigation graph is reconstructed entirely from whereto=links live. Read a document and you can build the full screen-to-screen flow with zero extra syntax.
Because links are inline and frame-level, a renderer can emit the inferred navigation as a Mermaid flowchart automatically — the same structure that powers the multi-frame flow view (see below).
Composing shared chrome: background=#id
Most apps repeat the same shell — app bar, nav rail, footer — on every screen.
Rather than copy it into each frame, author it once as its own frame and pull it
in underneath another with background=#id:
Wireframe #shell landscape visible=false
AppBar
Toolbar
Typography h6 "Acme"
Box 240px * // left nav rail, part of the shared chrome
Wireframe #dashboard landscape background=#shell
Grid cols=3 // only the screen-specific content; chrome comes from #shell
Card
Card
Card
#dashboard renders its grid on top of the #shell frame. Edit the shell once
and every screen that backgrounds it updates.
How it behaves:
- Resolution scope. The target may live in the same block or a different
block in the document. This is the one sanctioned cross-block dependency in
v0.1 — a frame that uses
background=is no longer independently renderable, because the renderer must resolve the id across the whole document. - Missing target. If the id cannot be resolved, the renderer draws the foreground frame alone and emits a warning. It never hard-fails.
- Chaining and cycles. Backgrounds may chain (
#abackgrounds#bwhich backgrounds#c), painted deepest-first. A frame must not reference itself directly or transitively; renderers detect cycles and break them with a warning. - Alignment and size (no anchor). Without
anchor=, the foreground frame drives the screen size. The background is underlaid at the top-left with no scaling; if it is larger it simply overflows/clips. There is no fitting behavior. To place the foreground inside a region of the background, use an anchor point instead.
Anchor points
background= underlays the shell, but both frames' content starts at the
top-left — a dashboard composed over an app shell would paint across the
shell's app bar and nav rail. An anchor point lets the background say where
foreground content belongs.
Declaring regions: Anchor #id
Anchor is an invisible, named region — a layout component that draws nothing.
Unsized, it fills the leftover space of its container on both axes, so a bare
Anchor #content means "the rest of this container". A background frame may
declare several:
Wireframe #shell landscape visible=false
AppBar
Toolbar
Typography h6 "Acme"
Stack row 100% *
Box 240px *
Anchor #side // a region inside the nav rail
Anchor #content // everything right of the rail, under the bar
Sizing tokens pin a region explicitly — a fixed bottom strip:
Wireframe #base portrait visible=false
Anchor #main // flexes: everything above the console
Anchor #console * 200px // a fixed 200px-tall strip at the bottom
Composing into a region: anchor=#id
A foreground frame picks a region with anchor=#id (alias at=), given
instead of a preset or w=/h=:
Wireframe #home background=#shell anchor=#content
Typography h1 "Dashboard"
Grid cols=3
Card to=#details
How it behaves:
- The anchor sizes and places the frame; the background sizes the canvas.
#home's content is laid out inside#content's box, and its canvas — the rectangle drawn, clipped, and used by the flow chart — becomes#shell's. - The region is a placement, not a viewport. Normal frame padding applies inside it; overflowing content is clipped at the canvas only.
- Shadowing. The id is looked up in the frame's background chain only,
nearest background first — a nearer frame's
#contentshadows a deeper one with the same id; within one frame, document order wins. The foreground's own tree is never searched. - Separate namespaces. Element ids (
anchor=) and frame ids (background=,to=) share the#sigil but never mix:anchor=never matches a frame, andto=stays frames-only. - Chaining. Anchored frames compose transitively: if
#pageanchors into#shell, a third frame may usebackground=#page anchor=#xwhere#xlives in#pageor#shell. - Any
#idworks.anchor=may target any element carrying a#id(e.g. aCard #hero), butAnchoris the purpose-built carrier and the documented pattern.
Like background=, anchors degrade gracefully — every failure is a soft
warning, never a hard fail:
| Condition | Warning | Fallback |
|---|---|---|
anchor= without background= | anchor "#a" requires background= | normal standalone layout |
| id not found in the background chain | anchor "#a" not found in background chain of "#f" | top-left overlay at own/preset size |
anchor= together with a preset or w=/h= | preset/size ignored: frame "#f" is sized by anchor "#a" | the anchor wins |
Anchor with no #id | Anchor without #id can never be targeted | laid out normally (an invisible spacer) |
duplicate #id within one frame | duplicate id "#a" in frame "#f" | the first declaration wins |
Hiding a reusable frame: visible=false
A frame that exists only to be a background should not also be drawn as its own
screen. visible=false suppresses standalone rendering:
Wireframe #shell visible=false
AppBar
Toolbar
Typography h6 "Acme"
visibledefaults totrue.visible=falseomits the frame from standalone drawing but does not stop it being used as abackground=target. "Hidden" means "not drawn on its own", not "never drawn".
Multiple frames and the flow view
A single ```wireframe block can declare several Wireframe frames. When
more than one frame is visible, wiremark arranges them as a flow chart: each
frame is a node, positioned automatically over the inferred to=#id graph, with
clean orthogonal connectors joining linked screens (a crisp diagram layer, drawn
over the hand-sketched frames — the same structure toMermaid emits).
Wireframe #home landscape
Grid cols=3
Card to=#details
Card to=#details
Wireframe #details landscape
Link "Back" to=#home
Typography h1 "Item details"
- Direction. The flow runs top-down by default. Add a top-level
Flow LRdirective — on its own line, like anIconsblock, not inside a frame — to lay the whole chart out left-to-right instead (Flow TDis the explicit default); a host may also pass{ direction }to the renderer, which wins over the directive. Orientation is a property of the whole diagram, so it lives in one place rather than on any single frame. - Background templates stay out of the flow. A
visible=falseframe (e.g. a#shellpulled in viabackground=#id) is never a flow node, but still composes underneath the screens that reference it. - Only resolvable links draw. A
to=#idwhose target is not a frame in the document — or a frame linking to itself — is left out of the graph and draws no connector; it never hard-fails. - Disconnected screens (frames nothing links to) are packed alongside the linked group rather than dropped.
- Gaps widen to fit the wiring. The gap between two ranks of frames is a routing channel: it grows from its minimum to seat every connector that crosses it on its own parallel track (so connectors never overlap each other or cut through a frame) and to hold their edge labels. Captions render inside that widened gap rather than on top of the screens, and an edge that skips a rank detours around the side instead of slicing through the frames between.
A lone frame is unaffected: a single-Wireframe document renders exactly as it
always has, with no flow chrome.
Recap
#idnames a frame; put it on theWireframeroot.to=#idon any element links to a frame; the nav graph is inferred from those links.- Targets are frames only; there is no deep-linking and no
from. background=#idunderlays a shared frame;visible=falsekeeps that shared frame from drawing on its own.Anchor #idnames a region inside a background frame;anchor=#id(aliasat=) composes a foreground frame's content into that region.- Several visible frames auto-arrange into a connected flow chart; a top-level
Flow TD(default) orFlow LRdirective sets its orientation.
Next: Patterns & recipes.