Documentation
Boceto is a tiny DSL for hand-drawn wireframes. Write a few lines inside a fenced markdown code block, get a styled diagram. This page documents every construct in the language.
The DSL has seven keywords. That's all of it:
| Keyword | What it does |
|---|---|
element | Draw a UI element (one of 83 built-in types) or call a component. |
text | Shorthand for a text label. |
arrow | Connect two elements by id. |
row / col | Open a layout container that arranges its children. |
component | Define a reusable composite. |
end | Close the most recent row, col, or component block. |
File embedding
Boceto content lives inside markdown fenced code blocks. The info string
starts with boceto and may include an optional page name after
a colon:
```boceto:Login Screen
element navbar 0 0 600 44 "MyApp"
element heading 100 80 400 28 "Welcome back"
```
Coordinates are pixels in page-space. Top-left is (0, 0).
Multiple boceto blocks in the same document become multiple
pages of one diagram.
Your first diagram
Show source
element navbar 0 0 900 44 "MyApp" element heading 280 90 340 32 "Welcome back" fontSize=28 element label 280 130 340 22 "Sign in to continue" element input 280 170 340 36 "Email" element primary-button 280 210 340 36 "Sign In"
element
The core statement. Draws one UI element.
element TYPE[#id] X Y W H "label" ["note"] [key=value ...]
- TYPE — one of 83 element types (see Elements).
- #id — optional named identifier (used by
arrowto reference this element). - X Y W H — pixel coordinates and size. Required positionally, even inside a
row/col(the container will overrideXandY; write0 0). - "label" — quoted text displayed on the element. Use
""if blank. - "note" — optional sticky-note annotation.
- key=value — type-specific attributes (e.g.
fontSize=18,rows=5) and layout attrs when inside a flex container.
text
Shorthand for a label element with implicit sizing.
text X Y "content" [w=N h=N fontSize=N ...]
Equivalent to element label X Y 200 24 "content" with the same attributes.
arrow
Connect two elements by id. The arrow draws from the midpoint of the source's bottom edge to the midpoint of the target's top edge.
arrow FROM_ID TO_ID ["optional label"]
Show source
element button#a 100 40 120 40 "Start" element box#b 380 100 200 80 "Process" element button#c 660 40 120 40 "Done" arrow a b arrow b c "completes"
Attributes (key=value)
Any element accepts trailing key=value tokens. Unknown attributes
are preserved verbatim — implementations may consume them, others pass them
through. Common attributes:
| Attribute | Meaning |
|---|---|
id | Named id (alternative to TYPE#id shorthand). |
fontSize | Override text size in pixels. |
| type-specific | e.g. rows, cols on table; progress on progress. |
| flex attrs | See child flex attributes when inside a row/col. |
Named IDs
Two equivalent forms; the shorthand is preferred for round-trip diffs.
element button#save 0 0 100 30 "Save"
element button 0 0 100 30 "Save" id=save
IDs must match [A-Za-z][A-Za-z0-9_-]*. Without an id, the parser
auto-generates an opaque one.
Text rendering
Every text-bearing element honors four generic attributes that control how
its label fits inside the bounding box: overflow,
align, fontSize, and minFontSize.
Quoted labels also accept \n for hard line breaks. Defaults
are sensible per element (a button ellipsizes; a
heading wraps) — these attributes only matter when you want
to override the default.
Multi-line labels (\n)
Inside a quoted token, \n is interpreted as a newline.
Renderers like textarea, alert, and
chat-bubble respect those line breaks before word-wrapping
each segment. \t is also recognised for tabs; \"
and \\ remain literal quote and backslash.
overflow
Four modes. ellipsis trims to a single line with a trailing
…. wrap word-wraps onto multiple lines, capped
by the box height. clip hard-clips at the box edge.
shrink binary-searches a smaller font size between
minFontSize (default 9) and fontSize until the
label fits.
textAlign
textAlign=left | center | right overrides the per-element default
horizontal alignment. In wrap mode, every line uses the same
anchor. (The attribute is named textAlign rather than
align to avoid collision with the flex container's cross-axis
align attribute, which uses a different value set.)
fontSize + minFontSize
fontSize sets the requested point size (it is the upper bound
when overflow=shrink). minFontSize caps how small
shrink is allowed to go; the default is 9. Both attributes are ignored on
modes that aren't shrink (except fontSize, which
always sets the requested size).
Layout: row and col
A row arranges children left → right. A col
arranges them top → bottom. The grammar mirrors element:
row[#id] X Y W|auto H|auto [attrs...]
<child statements>
end
col[#id] X Y W|auto H|auto [attrs...]
<child statements>
end
Show source
row 20 20 860 80 gap=12 align=middle element button 0 0 100 36 "Save" element button 0 0 100 36 "Cancel" element button 0 0 100 36 "Reset" end
0 0 for X/Y. The grammar requires
positional tokens, but the container overrides X/Y. Width and height become
the child's preferred size.
Distribution: justify and align
justify spreads children along the main axis;
align positions them on the cross axis.
Show source
row 20 20 860 60 justify=between align=middle element button 0 0 100 36 "Back" element heading 0 0 200 28 "Settings" element primary-button 0 0 100 36 "Save" end
Valid justify: start, middle,
end, between, around,
evenly. Valid align: start,
middle, end, stretch.
Sizing: auto, grow, min/max
Container auto
Use auto for W or H to shrink-wrap the container around its content.
Show source
row 20 20 auto auto padding=12 gap=8 align=middle element heading 0 0 200 28 "Toolbar" element button 0 0 80 32 "Save" element button 0 0 80 32 "Cancel" end
Child grow
A child with grow=1 takes any leftover main-axis space. Two
siblings with grow=1 each split the remainder evenly.
Show source
row 20 20 860 44 gap=8 align=middle element button 0 0 80 32 "Back" element input 0 0 0 32 "Search…" grow=1 element button 0 0 80 32 "Filter" end
0 means "no preferred size". A child with
w=0 grow=1 reads as "I have no width preference; give me the
remaining space." An actual zero-sized element wouldn't render, so the
overload is unambiguous.
Constraints: min-w / max-w / min-h / max-h
Guardrails on a child's size. min-w is essential for wrap-as-grid
layouts — it sets the threshold at which children break to the next line.
Wrap as grid
wrap=wrap on a row pushes overflowing children to a
new line. Combined with min-w + grow=1 on the
children, you get a responsive grid without leaving the row syntax.
Show source
row 20 20 860 180 gap=12 wrap=wrap align=start element card 0 0 0 80 "" grow=1 min-w=180 element card 0 0 0 80 "" grow=1 min-w=180 element card 0 0 0 80 "" grow=1 min-w=180 element card 0 0 0 80 "" grow=1 min-w=180 element card 0 0 0 80 "" grow=1 min-w=180 end
Elements as containers
Any element type can host children via the same block-form syntax as composites.
A trailing : on the element line opens a children block, closed by
end. The element's chrome (border, header, etc.) renders normally,
and the children render inside the content rect.
Two modes, same trigger rule as composite shells:
- Flex container — set
direction=row|col; children flow with fullgap/padding/justify/align/wrapsupport. - Absolute body — omit
direction; children render at their declared local(x, y)inside the content rect.
Show source
element box 0 0 400 auto "" direction=col padding=12 gap=8 align=stretch : element heading 0 0 0 24 "In a box" element label 0 0 0 18 "With a flex layout" end
Element types that draw header chrome (card, modal,
window-frame, browser-frame, phone-frame)
get an intrinsic content inset added to your padding so children
land below the chrome automatically — no manual padding-top=40 trick
needed:
Show source
element modal 80 20 400 auto "Confirm action" direction=col padding=12 gap=12 align=stretch :
element label 0 0 0 18 "Are you sure you want to continue?"
row 0 0 0 36 gap=8 justify=end
element button 0 0 100 32 "Cancel"
element primary-button 0 0 100 32 "Confirm"
end
end
Generic border + shadow
Any element accepts optional border and shadow
attributes — independent of block-form children.
border=true— 1.5px default stroke.border=3— 3px stroke.border=#3b82c4— colored stroke.border=false— none (default).shadow=true— default drop shadow.shadow=12— 12px blur.shadow=false— none (default).
Show source
element box 40 40 220 100 "Plain" element box 280 40 220 100 "Bordered" border=#3b82c4 shadow=8 element button 540 40 100 36 "Floating" shadow=14
Alignment tips
A few rules that keep diagrams looking sharp:
-
Stacked sections should share their padding. If a header
uses
padding=12and a body usespadding=16, their titles sit at different left edges. Pick one padding value per layout. -
Use
align=stretchwhen children should fill the cross axis. The default cross-axis behavior doesn't stretch. -
Use
0for "flex decides". Pairs withgrow,basis, oralign=stretch. -
Use
autofor "size to content". A container withh=autogrows to fit the tallest child. A call siteauto autouses the component's declared default if set.
component
Define a reusable composite once and reference it many times — like a function whose parameters fill in labels and attribute values.
component NAME[(p1, p2, ...)] [layout-attrs] [size-defaults]
<body statements>
end
The body contains the same statements as a page: element,
text, arrow, row, col,
and other component references. Inside the body, $name and
${name} are placeholders for parameters; they're substituted
at every call site.
To reference a component, use element NAME ...:
Show source
component UserCard(name, role) element card 0 0 240 80 "" element avatar 8 8 40 40 "" element heading 56 12 180 24 "$name" element label 56 38 180 18 "$role" end element UserCard 20 20 240 80 "" name="Jane Doe" role="Admin" element UserCard 280 20 240 80 "" name="John" role="User"
Responsive shells
Add direction=row or direction=col to the component
header and the body becomes flex-laid-out inside an implicit shell sized to
the call site. The same component can then be dropped at a fixed size, into
a row with grow=1, or into a wrap-grid — and the body re-flows
every time.
Show source
component Panel(title) direction=col align=stretch padding=16 gap=8 min-w=200 max-w=400 element heading 0 0 0 24 "$title" element box 0 0 0 0 "" grow=1 end # fixed element Panel 20 20 280 120 "" title="Fixed" # grow inside a row row 20 160 860 120 gap=12 align=stretch element box 0 0 200 0 "" id=sidebar element Panel 0 0 0 0 "" grow=1 title="Grows" end # wrap grid row 20 300 860 40 gap=12 wrap=wrap element Panel 0 0 0 0 "" grow=1 title="A" element Panel 0 0 0 0 "" grow=1 title="B" element Panel 0 0 0 0 "" grow=1 title="C" end
The component header accepts every row/col attribute
(gap, padding, align, justify,
wrap) plus size defaults (w, h,
min-w, min-h, max-w, max-h)
and per-instance flex defaults (grow, shrink,
basis, align-self).
Call-site values override component defaults. Use auto at the
call site to fall back to the default; use 0 at the call site
to defer to the parent container's layout.
Absolute-body components
Omit direction on the header and the component keeps an
absolute body: body coordinates are relative to the
instance origin, and the body does not re-flow when the instance is
resized. Useful for pixel-precise widgets you want identical wherever they
land.
Slots — composite children
A component body may include slot markers that designate where
call-site children render. A trailing : on the
element <Composite> ... line opens a children block,
closed by end.
Two flavors:
- Anonymous default slot — declared with bare
slot; filled by the call site's bare children. - Named slots — declared with
slot NAME; filled byslot NAME … endsub-blocks at the call site.
Show source
component Card(title) direction=col align=stretch padding=12 gap=8 w=300
slot header
element heading 0 0 0 24 "$title"
slot
end
element Card 20 20 320 auto "" title="Members" :
slot header
element label 0 0 0 16 "Showing 3 of 24"
end
element label 0 0 0 18 "Alice"
element label 0 0 0 18 "Bob"
element label 0 0 0 18 "Mei"
end
Rules:
- The single-line call form (no
:) is still valid — slots default to empty. - Passing bare children when the component has no anonymous
slotis a parse error. - Passing
slot NAMEthe component doesn't declare is a parse error. - A slot name may appear at most once per call site.
- Built-in element types (
element box,element button, …) do not accept children — only composite references do.
Common patterns
App shell — fixed chrome, flexible main
Header and Footer declare h=N;
Main uses grow=1 to fill the rest. The shared
padding across all three is what makes their content edges
line up.
Show source
component Header(title) direction=row align=middle padding=16 gap=8 h=56 element heading 0 0 0 28 "$title" grow=1 element button 0 0 80 32 "Action" end component Footer() direction=row align=middle padding=16 h=44 element label 0 0 0 16 "© 2026" grow=1 element label 0 0 120 16 "v1.0.0" end component Main() direction=col align=stretch padding=16 gap=12 grow=1 element heading 0 0 0 24 "Welcome" element box 0 0 0 0 "" grow=1 end component AppShell(title) direction=col align=stretch element Header 0 0 0 0 "" title="$title" element Main 0 0 0 0 "" element Footer 0 0 0 0 "" end element AppShell 20 20 860 340 "" title="My App"
Chrome sized to content (h=auto)
Drop h=auto on the header and it grows with its tallest child.
Show source
component DynHeader(title) direction=row align=middle padding=16 gap=8 h=auto element heading 0 0 0 32 "$title" grow=1 element button 0 0 100 40 "Save" element button 0 0 100 40 "Cancel" end component Main() direction=col align=stretch padding=16 gap=12 grow=1 element heading 0 0 0 24 "Content area" element box 0 0 0 0 "" grow=1 end component Shell(title) direction=col align=stretch element DynHeader 0 0 0 0 "" title="$title" element Main 0 0 0 0 "" end element Shell 20 20 860 240 "" title="Sized to content"
Sidebar + main, two columns
Show source
component Sidebar() direction=col align=stretch padding=16 gap=8 w=200 element heading 0 0 0 20 "Menu" element button 0 0 0 32 "Home" element button 0 0 0 32 "Settings" element button 0 0 0 32 "Profile" end component Main() direction=col align=stretch padding=16 gap=8 grow=1 element heading 0 0 0 24 "Dashboard" element box 0 0 0 0 "" grow=1 end component TwoCol() direction=row align=stretch element Sidebar 0 0 0 0 "" element Main 0 0 0 0 "" end element TwoCol 20 20 860 220 ""
Modal — fixed width, content-driven height
Show source
component Modal(title) direction=col align=stretch padding=16 gap=12 w=400 h=auto
element heading 0 0 0 24 "$title"
element label 0 0 0 18 "Are you sure you want to continue?"
element input 0 0 0 36 "Reason (optional)"
row 0 0 0 36 gap=8 justify=end
element button 0 0 100 32 "Cancel"
element primary-button 0 0 100 32 "Confirm"
end
end
element box 0 0 900 280 "" id=backdrop
element Modal 250 40 400 0 "" title="Confirm action"
Web components
Boceto ships two framework-agnostic custom elements you can drop into any HTML
page. <boceto-view> is a read-only renderer; <boceto-edit>
is an interactive editor. Both share the same code attribute as the
source of truth — set it once, hand-edit it later, or wire it up to the editor
and watch the markup update on every commit.
Install
Two equivalent forms — pick whichever fits your build:
// ESM — bundler or modern browser with an importmap. import '@boceto/view/auto' import '@boceto/edit/auto' // Or via plain <script> tags pointing at the pre-built bundles // (each bundle inlines its own copy of @boceto/core + yoga-layout): <script type="module" src="./assets/boceto-view.js"></script> <script type="module" src="./assets/boceto-edit.js"></script>
The /auto entry point is tree-shake-safe; it only registers the
custom element with customElements.define. If you want manual
control over the tag name, import the constructor and call defineBocetoView()
/ defineBocetoEdit() yourself (each accepts an optional tag override).
<boceto-view> — read-only renderer
Paints a Boceto document onto a <canvas> inside a shadow root.
Re-renders whenever any observed attribute changes; emits
boceto-render when a paint completes.
<boceto-view width="600" height="200" code='```boceto element navbar 0 0 600 44 "MyApp" element heading 100 80 400 28 "Hello, Boceto" ```'></boceto-view>
Attributes
| Attribute | Type | Meaning |
|---|---|---|
code | string | Inline DSL source. Overrides slot text content. |
src | URL | Fetched on connect/attribute change if code is absent. |
width | integer | Canvas pixel width. Default 860. |
height | integer | Canvas pixel height. Default 600. |
page | id / name / index | Which page to render in a multi-page doc. |
fit | fixed | content | content grows the canvas to encompass every element; width / height become minimums. Default fixed. |
padding | integer | Breathing room around content when fit="content". Default 16. |
Events
| Event | Detail | Fires on |
|---|---|---|
boceto-render | { doc, page } | Every successful paint. |
Auto-fit example
Set fit="content" when the doc's size is data-driven or you don't
know the bounding box in advance. The canvas grows automatically:
<boceto-view width="200" height="100" fit="content" padding="20" code='...'></boceto-view>
Loading from a URL
<boceto-view src="./scenes/login.boceto" width="600" height="320"></boceto-view>
<boceto-edit> — interactive editor
A thin DOM adapter around a framework-agnostic BocetoEditor controller.
Click / drag / resize on the canvas, multi-select, undo / redo, right-click for a
context menu, double-click to rename. Selection state, geometry mutations, and
history are all routed through the controller, which is exposed as
el.editor for programmatic use.
<boceto-edit width="600" height="280" code='...'></boceto-edit>
Attributes
| Attribute | Type | Meaning |
|---|---|---|
code | string | Source of truth. External writes call editor.setCode(); commits emit change. |
src | URL | Fetched on connect if code is absent. |
width / height | integer | Initial canvas size. Once the host gets a non-zero CSS size, it tracks that instead (drag a parent with resize: both to grow the canvas live). |
page | id / name / index | Active page. |
readonly | boolean | Suppresses every mutation (pointer / keyboard / programmatic). |
mode | select | Reserved for forward compatibility. Only select in v0.2. |
Events
| Event | Detail | Fires on |
|---|---|---|
change | { code } | User commit (drag release, resize, delete, undo, redo, programmatic mutation). |
select | { ids: string[] } | Selection set changes. |
page | { pageId } | Active page changes. |
External setAttribute('code', ...) does not emit
change — it's a source-of-truth reset, not a user action.
Pointer + keyboard reference
| Gesture | Action |
|---|---|
| Click an item | Select (replace). |
| Shift / ⌘-click | Add to or toggle selection. |
| Drag selected | Move (one history entry per drag). |
| Drag a handle | Resize. |
| Drag empty canvas | Rubber-band select. |
| Double-click | Inline label edit. |
| Right-click | Context menu: Bring to Front / Forward / Send Backward / to Back / Duplicate / Delete. |
⌘K | Open the element palette (<boceto-palette>) when bound to the editor. |
⌘] / ⌘⇧] | Bring forward / to front. |
⌘[ / ⌘⇧[ | Send backward / to back. |
Backspace / Delete | Remove selection. |
⌘D | Duplicate. |
Arrow keys (Shift = ×10) | Nudge selection. |
⌘Z / ⌘⇧Z | Undo / redo. |
Programmatic API: el.editor
The custom element exposes its BocetoEditor controller as
el.editor. Drive the doc from JavaScript — useful for toolbars,
AI-generated edits, or any UI sitting alongside the canvas:
const el = document.querySelector('boceto-edit')
el.addEventListener('change', e => save(e.detail.code))
// Selection + mutation
el.editor.addElement('button', 200, 100, { label: 'Save' })
el.editor.move(['p0e0'], 10, 0)
el.editor.bringToFront(['p0e0'])
// History
el.editor.undo()
el.editor.redo()
// Pages
el.editor.addPage('Settings')
el.editor.setPage('Settings')
Headless: BocetoEditor without DOM
The controller is exported separately for use in React hooks, Svelte actions,
tests, or anywhere a canvas isn't around. Same API as el.editor:
import { BocetoEditor } from '@boceto/edit'
const ed = new BocetoEditor({
code: '```boceto\\nelement box 0 0 100 50 "Hi"\\n```',
})
ed.on('change', ({ code }) => persist(code))
ed.move(['p0e0'], 10, 0)
const updated = ed.code // serialised on demand
@boceto/react — React wrappers
React wrappers around the custom elements. Props pass through as attributes;
events fan back out via onRender / onChange.
BocetoEditFull bundles the editor with its floating
palette and inspector — use it when you want a working authoring
surface (the bare BocetoEdit renders only the canvas).
import { BocetoView, BocetoEditFull } from '@boceto/react'
<BocetoView code={code} width={600} height={320} />
<BocetoEditFull code={code} onChange={setCode} readOnly={isLocked} />
// Custom layout — wire palette + inspector by a shared id:
import { BocetoEdit, BocetoPalette, BocetoInspector } from '@boceto/react'
<BocetoEdit id="ed-1" code={code} onChange={setCode} />
<BocetoPalette for="ed-1" />
<BocetoInspector for="ed-1" />
The editor also exposes the new imports prop introduced
by @boceto/tiptap, so a
React-hosted editor can resolve components defined in a sibling
fence when embedded inside a doc-aware host.
@boceto/vue — Vue 3 wrappers
defineComponent-based wrappers that ship as plain
runtime components (no SFC build step). Use
v-model:code on <BocetoEdit> for
two-way binding. The boceto-render CustomEvent surfaces
as Vue's render event.
import { BocetoView, BocetoEdit } from '@boceto/vue'
<BocetoView :code="code" :width="600" :height="320" @render="onRender" />
<BocetoEdit v-model:code="code" :readOnly="isLocked" />
@boceto/svelte — Svelte wrappers
Source-only .svelte components — your Svelte tooling
compiles them. Compatible with Svelte 4 and Svelte 5. Use
bind:code for two-way binding; on:render
and on:change surface the custom-element events.
<script>
import { BocetoView, BocetoEdit } from '@boceto/svelte'
let code = 'element navbar 0 0 600 44 "MyApp"'
</script>
<BocetoView {code} width={600} height={320} on:render={(e) => console.log(e.detail.doc)} />
<BocetoEdit bind:code readOnly={isLocked} />
@boceto/tiptap — TipTap node + extension
A TipTap node (bocetoBlock) and an extension
(BocetoContext) for embedding Boceto blocks inside any
TipTap editor. The extension broadcasts the doc-level Boceto source
to every node view, so a component defined in one
fenced block is resolvable from any other block in the same
document — without merging them into a single fence.
This is the cross-block context the markdown plugins
(@boceto/remark, @boceto/markdown-it) get
for free, ported into a TipTap editor that stores each block as an
atomic node.
import { useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { BocetoBlock, BocetoContext, BOCETO_ICON_SVG } from '@boceto/tiptap'
import { withReactNodeView } from '@boceto/tiptap/react'
const editor = useEditor({
extensions: [StarterKit, withReactNodeView(BocetoBlock), BocetoContext],
content: /* markdown / HTML with one or more ```boceto fences */,
})
// Toolbar button:
<button onClick={() => editor?.chain().focus().insertBocetoBlock().run()}>
<span dangerouslySetInnerHTML={{ __html: BOCETO_ICON_SVG }} />
</button>
Heads-up: the built-in React node view mounts the
full editing trio — <boceto-edit>, <boceto-palette>,
<boceto-inspector>. If you swap in your own node
view, mount all three; @boceto/react's wrapper only
renders <boceto-edit>, so authoring without the
palette + inspector is unusable.
Under the hood, BocetoContext populates
editor.storage.bocetoContext.source with every block's
fenced DSL joined together. The node view subtracts its own code and
passes the rest to <boceto-view> /
<boceto-edit> via the new imports
attribute. Runnable demo: examples/tiptap-demo.
AI assistance
Boceto ships two complementary pieces for AI coding assistants: the skill (markdown rules that trigger the agent and teach the non-negotiables) and the MCP server (runtime tools the agent calls — lint, fix, render-svg, recipe + element catalog). Install both together with one command.
Install — npx boceto add mcp
npx boceto add mcp # auto-detect AI client; install MCP + matching skill npx boceto add mcp claude-code # explicit (claude-code | cursor | claude-desktop) npx boceto add skill <target> # skill only — for assistants without MCP support npx boceto check # what's installed where
add mcp writes the mcpServers.boceto
entry into your AI client's config and co-installs the
matching skill (the skill is what triggers the agent to call the
MCP tools — MCP alone leaves it with tools it doesn't know about).
Auto-detects when exactly one MCP-client config exists; otherwise
prompts. Atomic JSON merge — never clobbers other entries.
--skip-skill to install MCP only.
MCP client (add mcp) | Skill goes to |
|---|---|
claude-code | .claude/skills/boceto/ (project) |
cursor | .cursor/rules/boceto.mdc (project) |
claude-desktop | ~/.claude/skills/boceto/ (user) |
For assistants without a standardised MCP config
(Aider, GitHub Copilot, OpenAI Codex / AGENTS.md,
Gemini CLI, Cline, Windsurf, …): use boceto add skill <target>
for the same set of targets the old @boceto/skill install
command supported. Run boceto list skill for the
full set.
Manual MCP install (edit JSON yourself)
{
"mcpServers": {
"boceto": {
"command": "npx",
"args": ["-y", "@boceto/mcp"]
}
}
}
Drop into ~/.claude.json, ~/.cursor/mcp.json, or the OS-specific Claude Desktop config and restart your client.
MCP server — @boceto/mcp
Closes the runtime loop:
generate DSL → boceto_lint → fix → boceto_render_svg → iterate.
Element catalog and recipes surface as on-demand tools so the
agent doesn't carry the whole canon in its context.
Tools
| Tool | What it does |
|---|---|
boceto_parse | Parse DSL → { ok, doc } or { ok: false, error: { line, message } }. |
boceto_lint | Issues with line / column / endColumn / severity / fix, plus a parse-clean fixed source. |
boceto_fix | Returns the autofixed source directly. |
boceto_render_svg | Render a page to a self-contained SVG (Yoga flex applied; deterministic). |
boceto_list_elements | Every element type, grouped by category, with default sizes. |
boceto_describe_element | One element's attribute schema and defaults. |
boceto_list_recipes | All recipes (mockups + shells) with slug + summary. |
boceto_read_recipe | The full markdown body of one recipe by slug. |
The MCP server also exposes the skill canon + formal spec as
resources under boceto://skill/*,
boceto://references/*,
boceto://recipes/index.md, and
boceto://spec/boceto-spec.md — readable by any
resource-aware client.
AI Skill — @boceto/skill
Trimmed teaching layer: SKILL.md (always loaded) +
three reference files (grammar.md, layout.md,
component-doc-pattern.md). The element catalog and
recipes live behind the MCP tools above — the skill is small on
purpose, so the agent keeps it in context. When asked for a
composite component, the agent returns literate
markdown: ## heading + description + fenced
definition + example usage + Used in cross-reference.
Non-negotiables the skill teaches: six positional slots on every
element line, four positional slots on every
row / col line, canonical types only
(hallucination map for the common substitutes), components-first
authoring, definition-once per page, the literate output pattern,
and the three documented parser limitations.
Try it
"Sketch a wireframe for a settings page with three sections." "Add a search bar at the top of this ````boceto` block." "Mock up a Spotify-like mobile player wrapped in a phone-frame." "Define a reusable pricing-card composite and place three tiers in a row."
Updating — re-run npx boceto add mcp --force
(or npx boceto add skill <target> --force) to refresh.
npx always fetches the latest published CLI on each
invocation. --from-git pulls unreleased changes from
the main branch. boceto check compares
installed vs npm-latest.
Linter — @boceto/lint
A standalone linter + auto-fixer for Boceto source. Designed to
catch the failure modes humans and AI assistants hit most often:
missing label slots, invented element types, fractional coords,
mismatched align vs textAlign, unclosed
blocks, unterminated quoted strings, and structural parse errors —
with character-precise line + column ranges so editors can
underline the exact offending token.
The linter is line-oriented and stateless: it accepts a source
string and returns a typed LintReport with
{ issues, fixed }. The fixed output is
the source rewritten with every available autofix applied, in one
pass. Cross-checks against @boceto/core's real
parse() as a final step, so anything the rules miss
still gets surfaced as a parse-error issue with
fence-aware line numbers (parser-reported "line 3 in the block"
is translated back to the actual file line).
Rules
| Rule | Severity | Autofix | What it catches |
|---|---|---|---|
missing-label |
error | ✓ | 5 positional slots, the 5th is unquoted — splices "" after H so chrome elements (chart-bar, status-bar, home-indicator, divider, fab, …) become parser-valid. |
missing-coord |
error | — | 5 slots, the 5th is a quoted label. One of X/Y/W/H was dropped, NOT the label — no autofix because the linter can't guess which coord was meant. Message tells the author what to do. |
wrong-arity |
error | — | Fewer than 5 positional slots on an element line. |
invented-type |
error | ✓ | 16+ common AI hallucinations mapped to their real types: Frame → box, Stack → col, NavBar → navbar, Heading2 → heading, icon-button → button, PhoneFrame → phone-frame, … |
unknown-type |
warning | — | Element type not in the catalog and not in the canonical hallucination map. Suggests the closest known type by edit distance. |
bad-coord |
error | ✓ | Fractional or negative coords — rounds to integer, clamps below 0. |
align-vs-textAlign |
warning | ✓ | align=left|center|right on a non-container element — renames to textAlign= (since flex align uses start|middle|end|stretch). |
unclosed-block |
error | — | row / col / component / element-as-container block opened but never closed with end. |
unterminated-string |
warning | ✓ | A label or attr opens with " but never closes before end-of-line. The parser silently accepts this, but it's almost always a typo — appends a closing ". |
parse-error |
error | — | Final cross-check against @boceto/core's real parser. Reports the original error with the line mapped back to its absolute position in the source (not relative to the inside of a markdown fence). |
Usage
npm install @boceto/lint (or pnpm add,
yarn add, …). Then:
import { lint, applyFixes } from '@boceto/lint'
const source = `\`\`\`boceto
element Frame 0 0 600 400
element chart-bar 232 110 600 260
\`\`\``
const report = lint(source)
// report.issues: LintIssue[] with line + column + endColumn + fix
// report.fixed: source rewritten with every autofix applied
// report.errorCount, .warningCount, .infoCount
console.log(report.fixed)
// => `\`\`\`boceto
// element box 0 0 600 400 ""
// element chart-bar 232 110 600 260 ""
// \`\`\``
// Or apply autofixes from a custom issue subset:
const partial = applyFixes(source, report.issues.filter(i => i.rule === 'invented-type'))
Each LintIssue carries:
interface LintIssue {
rule: string // 'missing-label' | 'invented-type' | …
severity: 'error' | 'warning' | 'info'
line: number // 1-based, absolute in the source
column: number // 1-based, points at the offending token
endColumn: number // 1-based, exclusive end of the span
message: string // human-readable, safe for inline display
fix?: {
line: number
newLine: string // the replacement line (full text)
label: string // short label for UI buttons
}
}
In the playground
The playground wires the linter into
CodeMirror via @codemirror/lint — every issue gets an
inline red / yellow underline at the exact offending span, a gutter
marker, and a hover tooltip with a one-click Apply fix
action. The full issue list also renders in a panel below the editor
with a Fix all button that applies every available
autofix in a single dispatch (one undo entry).
MCP server
The linter is the foundation of @boceto/mcp, the MCP
server that lets any AI client call boceto_lint,
boceto_fix, boceto_render_svg, and friends
directly. See AI assistance → MCP server above for
the install snippet and the full tool / resource list.
Reference: row / col attributes
| Attribute | Values | Default |
|---|---|---|
gap | non-negative integer (px) | 0 |
padding | non-negative integer (px) | 0 |
align | start · middle · end · stretch | middle (row) / start (col) |
justify | start · middle · end · between · around · evenly | start |
wrap | nowrap · wrap · wrap-reverse | nowrap |
min-w / min-h / max-w / max-h | non-negative integer (px) | unset |
Reference: child flex attributes
Applies to any element, text, or component
reference that is a direct child of a row / col.
Ignored outside a flex container.
| Attribute | Values | Default |
|---|---|---|
grow | non-negative number | 0 |
shrink | non-negative number | 1 |
basis | non-negative integer (px) or auto | auto |
align-self | auto · start · middle · end · stretch | auto |
min-w / min-h / max-w / max-h | non-negative integer (px) | unset |
Reference: component header attributes
| Group | Attribute | Notes |
|---|---|---|
| Shell | direction | row · col. Presence enables shell mode. |
gap / padding | Pixels. | |
justify | See row/col reference. | |
align | See above. | |
wrap | See above. | |
| (omit) | Component becomes absolute-body. | |
| Size default | w, h | Pixels or auto. Used when call site doesn't supply. |
min-w / min-h / max-w / max-h | Pixels. | |
| Child-flex default | grow | Non-negative number. |
shrink | Non-negative number. | |
basis | Pixels or auto. | |
align-self | See child reference. |
For the formal grammar, see the spec.