| name | wireframe |
| description | Generate low-fidelity wireframes (ASCII, SVG, or HTML) from user story files. Use when creating wireframes for UI stories. |
SKILL: wireframe
Purpose
Generate low-fidelity wireframes from user story files. Output is deterministic — given the same story and layout hints, the same wireframe structure is produced. Three output formats: ASCII (default), SVG, HTML. Human approval is required before the wireframe feeds downstream mockup work.
Inputs
| Field | Source | Example |
|---|
story_file | path to story markdown | docs/stories/auth-reset-0001.md |
ui_components | derived from story Gherkin | form, button, error message |
stack | project.config.yaml | react-mantine |
Outputs
All three files are always required — not optional, not format-dependent:
| File | Location | Purpose |
|---|
<story-id>.wireframe.md | docs/design/wireframes/ | ASCII layout + approval metadata |
<story-id>.wireframe.html | docs/design/wireframes/ | Browser-previewable static wireframe |
<story-id>.wireframe.excalidraw | docs/design/wireframes/ | Editable Excalidraw diagram |
A wireframe stage that produces only .md is incomplete. Do not PAUSE or mark DONE without all three files.
ASCII Wireframe Primitives
Use these consistently across all wireframes:
┌─────────────────────────────────┐ ← container / card
│ [Label] [Input Field ] │ ← label + text input
│ [Button: Primary Action ] │ ← primary button
│ [Button: Secondary] │ ← secondary button
│ ○ Option A ○ Option B │ ← radio group
│ ☐ Checkbox label │ ← checkbox
│ ▼ Dropdown / Select │ ← select / combobox
│ ────────────────────── │ ← divider
│ ⚠ Error message text │ ← validation error
│ ✓ Success confirmation │ ← success state
└─────────────────────────────────┘
[Nav: Logo | Item 1 | Item 2 | CTA] ← navigation bar
[ Sidebar ][ Main Content ] ← two-column layout
[ Col 1 ][ Col 2 ][ Col 3 ] ← three-column grid
[ Full-width Banner ]← hero / header band
ASCII Wireframe — Example (Password Reset)
┌──────────────────────────────────────┐
│ Reset Password │
│ │
│ Email │
│ [ ] │
│ │
│ ⚠ No account found for this email │ ← error state
│ │
│ [Button: Send Reset Link ] │
│ │
│ ← Back to Login │
└──────────────────────────────────────┘
Generate Wireframe Files
For each story, produce all three output files. Run these steps:
STORY_FILE="docs/stories/auth-reset-password-20250416143000-0001.md"
STORY_ID=$(python3 -c "
import re
m = re.search(r'^id:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
mkdir -p docs/design/wireframes
Step 1 — Write ${STORY_ID}.wireframe.md (ASCII layout + approval metadata):
---
story_id: "{story_id}"
story_file: "{story_file}"
status: draft # draft | approved | rejected
approved_by: null
approved_at: null
---
## Wireframe: {story title}
### Default state
{ASCII wireframe}
### Error state
{ASCII wireframe — validation error}
### Success state
{ASCII wireframe — confirmation}
### Interaction Notes
- Tab order: {list of focusable elements in tab sequence}
- Primary action: {describe}
- Error handling: {describe visible error states}
### Approval
- [ ] Approved by product owner
- [ ] Approved by UX lead (if applicable)
Step 2 — Write ${STORY_ID}.wireframe.html (see HTML template below)
Step 3 — Write ${STORY_ID}.wireframe.excalidraw (see Excalidraw template below)
After writing all three, verify:
ls docs/design/wireframes/${STORY_ID}.wireframe.md
ls docs/design/wireframes/${STORY_ID}.wireframe.html
ls docs/design/wireframes/${STORY_ID}.wireframe.excalidraw
If any file is missing, produce it before continuing.
HTML Wireframe (always required)
Always generate the HTML wireframe — every story, every run. Use multiple <section> blocks for multi-state wireframes (default, loading, error, success, empty).
cat > "docs/design/wireframes/${STORY_ID}.wireframe.html" <<'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wireframe: {story title}</title>
<style>
* { box-sizing: border-box; font-family: monospace; }
body { background:
h1 { font-size: 1rem; color:
.states { display: flex; flex-wrap: wrap; gap: 1.5rem; }
.state { background:
.state-label { background:
.frame { padding: 1.5rem; }
.screen-title { font-weight: bold; font-size: 1.1rem; margin-bottom: 1rem; border-bottom: 1px solid
label { display: block; font-size: .8rem; color:
.input { border: 1px solid
.btn { border: none; padding: .5rem 1rem; cursor: default; margin-top: .75rem; width: 100%; font-weight: bold; }
.btn-primary { background:
.btn-secondary { background: transparent; border: 1px solid
.error { color:
.success { color:
.link { color:
.nav { display: flex; gap: 1rem; background:
.badge { background:
.divider { border: none; border-top: 1px solid
.tab-order { font-size: .7rem; color:
</style>
</head>
<body>
<h1>Wireframe: {story title} — {story_id}</h1>
<div class=>
<div class=>
<div class=>Default state</div>
<div class=>
<div class=>{Screen Title}</div>
<!-- Add form fields, buttons, content blocks here -->
<label>Field label</label>
<input class= = placeholder= disabled />
<div class=>Primary Action</div>
<a class= href=>Secondary </a>
<div class=>Tab order: Field → Primary Action → Secondary </div>
</div>
</div>
<div class=>
<div class=>Error state</div>
<div class=>
<div class=>{Screen Title}</div>
<label>Field label</label>
<input class= = placeholder= disabled style= />
<div class=>⚠ Error message describing the problem</div>
<div class=>Primary Action</div>
<div class=>Tab order: Field → Primary Action</div>
</div>
</div>
<div class=>
<div class=>Success state</div>
<div class=>
<div class=>{Screen Title}</div>
<div class=>✓ Success confirmation message</div>
<div class=>Back / Next step</div>
</div>
</div>
</div>
</body>
</html>
HTMLEOF
Extend with real field names, content, and states from the story Gherkin. One <div class="state"> block per Gherkin scenario.
Excalidraw Wireframe (always required)
Always generate the Excalidraw file — every story, every run. Excalidraw is the canonical editable wireframe format reviewers annotate.
Write the file as valid JSON to docs/design/wireframes/${STORY_ID}.wireframe.excalidraw. Each UI element is one entry in the elements array. Use the element templates below, copy and adapt:
python3 - <<'PYEOF'
import json, pathlib, os
story_id = os.environ.get("STORY_ID", "unknown")
out = pathlib.Path(f"docs/design/wireframes/{story_id}.wireframe.excalidraw")
out.parent.mkdir(parents=True, exist_ok=True)
def rect(id, x, y, w, h, label="", bg="transparent", stroke="#343a40", bold=False):
els = [{
"id": id, "type": "rectangle", "x": x, "y": y, "width": w, "height": h,
"angle": 0, "strokeColor": stroke, "backgroundColor": bg,
"fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid",
"roughness": 1, "opacity": 100, "groupIds": [], "roundness": {"type": 3},
"version": 1, "versionNonce": 1, "isDeleted": False,
"boundElements": None, "updated": 1, "link": None, "locked": False,
}]
if label:
els.append(text(id + "_lbl", x + w/2, y + h/2, label, bold=bold, anchor="center"))
return els
def text(id, x, y, content, bold=False, anchor="left", color=):
{
: , : , : x, : y,
: len(content) * 8, : 20,
: 0, : color, : ,
: , : 1, : ,
: 1, : 100, : [], : None,
: 1, : 1, : False,
: None, : 1, : None, : False,
: content, : 16,
: 3,
: anchor, : ,
: 14, : None, : content,
: 1.25,
: bold ,
}
def input_field(, x, y, w, label_text):
(
[text( + , x, y - 18, label_text)] +
rect(, x, y, w, 32, stroke=)
)
def button_primary(, x, y, w, label_text):
rect(, x, y, w, 36, label=label_text, =, stroke=, bold=True)
def button_secondary(, x, y, w, label_text):
rect(, x, y, w, 36, label=label_text, =, stroke=)
def section_label(, x, y, content):
[text(, x, y, f, color=)]
elements = []
elements += rect(, 50, 30, 700, 600, stroke=)
elements.append(text(, 70, 50, , bold=True))
elements += section_label(, 70, 90, )
elements += input_field(, 70, 130, 560, )
elements += button_primary(, 70, 190, 560, )
elements.append(text(, 70, 238, , color=))
elements += section_label(, 70, 280, )
elements += input_field(, 70, 320, 560, )
elements += rect(, 70, 320, 560, 32, stroke=)
elements.append(text(, 70, 360, , color=))
elements += button_primary(, 70, 390, 560, )
elements += section_label(, 70, 450, )
elements.append(text(, 70, 490, , color=))
elements += button_secondary(, 70, 520, 260, )
doc = {
: ,
: 2,
: ,
: elements,
: {: , : None},
: {},
}
out.write_text(json.dumps(doc, indent=2) + )
(f)
PYEOF
Adapt element positions and labels to match the actual story screens. Add more rect/text/input_field/button_primary calls per Gherkin scenario. Do not leave placeholder text ({Screen Title}) in the final file.
Approval Gate
All three files must exist and the .md must be status: approved before mockup or ui-mockup-builder proceeds:
python3 - <<'EOF'
import re, sys, pathlib
sid = open(".claude/state/maple.json").read()
for ext in ["md", "html", "excalidraw"]:
p = pathlib.Path(f"docs/design/wireframes/{sid}.wireframe.{ext}")
if not p.exists():
print(f"BLOCKED: missing {p}")
sys.exit(1)
md = pathlib.Path(f"docs/design/wireframes/{sid}.wireframe.md").read_text()
m = re.search(r'^status:\s*(\w+)', md, re.MULTILINE)
status = m.group(1) if m else 'draft'
if status != 'approved':
print(f"BLOCKED: wireframe {sid} not approved (status={status})")
sys.exit(1)
print("approved — all three artifacts present")
EOF
Failure Modes
| Condition | Action |
|---|
| Story has no Gherkin | Generate skeleton wireframe with placeholder states. Log NO_GHERKIN — skeleton only. |
docs/design/wireframes/ missing | Create it. |
Wireframe .md exists and is approved | Do not overwrite. Log SKIP — approved wireframe exists. |
Wireframe .md exists and is draft | Overwrite only if story Gherkin has changed. |
.html or .excalidraw missing despite .md existing | Generate the missing file(s) immediately. |
Logging
[wireframe] CREATE docs/design/wireframes/auth-reset-0001.wireframe.md
[wireframe] CREATE docs/design/wireframes/auth-reset-0001.wireframe.html
[wireframe] CREATE docs/design/wireframes/auth-reset-0001.wireframe.excalidraw
[wireframe] SKIP docs/design/wireframes/auth-reset-0001.wireframe.md (approved — locked)
[wireframe] BLOCKED auth-reset-0001 status=draft — needs approval before mockup
[wireframe] BLOCKED auth-reset-0001 missing .html — generating now