How to design with framesmith so the result holds up across breakpoints.
Canvases live inside a Workspace > Project > Canvas hierarchy. The built-in Personal workspace + Untitled project always exist as a default home; create more once you're running multiple projects.
workspace_create({ name })— top-level container (e.g. "Client work", "Personal").project_create({ workspaceId, name })— group related canvases inside a workspace.canvas_create({ name, projectId })— drops the canvas in the given project (defaults to Untitled).canvas_move({ canvasId, projectId })— reassign a canvas between projects.canvas_archive/canvas_unarchive— soft-delete: canvas stays on disk but hides from default listings. Reach for this when iterating; reach forcanvas_delete(permanent) only when sure.
workspace_delete and project_delete refuse to remove non-empty containers. Clear the contents first or canvas_move them out.
- Author desktop-first at one design width. Pick a width (1200 or 1440 is typical), compose the design there.
- Adapt down with
responsivehints + fluid widths. The renderer derives the mobile/tablet layout from the same scene graph — you don't author it twice.
Pick the right width per node — this is the single biggest lever for responsive quality.
| Use | When | Example |
|---|---|---|
Fixed pixels (width: 360) |
Icons, badges, small chips, fixed UI elements that should not reflow | Avatar width: 40, badge width: 80 |
Percentage string (width: "50%") |
Column splits inside a parent — child should be a fraction of available row | Two-column hero, sidebar + main |
Fluid + cap (width: "100%", maxWidth: 600) |
Content that should fill the row on narrow viewports but cap on wide screens | Article body, dashboard cards |
Floor (width: "50%", minWidth: 240) |
Column splits where the child has a minimum readable size | Card grid where 50% would otherwise become unreadably narrow |
width: "fit-content" |
Hugs its content; lets the parent's gap and alignItems do the spacing |
Buttons, pills, link-style text |
Default to fluid. Reach for fixed pixel widths only when the content genuinely shouldn't scale.
Set responsive on container nodes (frames with layout: "horizontal" and children). The renderer emits the right media-query / flex-wrap rules.
| Hint | Effect | Use when |
|---|---|---|
responsive: "stack" |
Horizontal container flips to vertical below 768px | Multi-column rows that should become a single column on mobile. This is the most common case — almost every card row, hero with side-by-side panels, footer link group |
responsive: "wrap" |
Children wrap to the next line instead of overflowing | Tag clouds, badge groups, card grids that can have an irregular last row |
responsive: "fixed" |
Never reflows | Toolbars, navbars, fixed-position headers — anywhere reflow would break the layout intent |
Pricing tiers (3 cards side-by-side → single column on mobile):
row=I("document", { type: "frame", layout: "horizontal", gap: 24, responsive: "stack" })
c1=I(row, { type: "frame", width: "100%", maxWidth: 360, padding: 32, fill: "#0F172A", cornerRadius: 16 })
// ...c2, c3 the same shapeTwo-column hero (text + image, stacks below 768px):
hero=I("document", { type: "frame", layout: "horizontal", gap: 48, responsive: "stack", alignItems: "center" })
text=I(hero, { type: "frame", width: "50%", layout: "vertical", gap: 16 })
img=I(hero, { type: "image", src: "...", width: "50%" })Tag list (wraps to next line as the row narrows):
tags=I("document", { type: "frame", layout: "horizontal", gap: 8, responsive: "wrap" })
// each tag: width: "fit-content", padding: [4, 12], cornerRadius: 999Toolbar (never reflows):
bar=I("document", { type: "frame", layout: "horizontal", gap: 16, responsive: "fixed", padding: 16, alignItems: "center" })- Three fixed-pixel cards in a horizontal row.
width: 360× 3 in a row withoutresponsive: "stack"clips on a 390px mobile viewport instead of reflowing. Either setresponsive: "stack"or usewidth: "100%", maxWidth: 360. - Setting
marginon every node. Usegapon the parent flex container andpaddingon the child. The renderer doesn't surfacemargin. - Setting
fontFamilyon every text node. The renderer defaults to a system sans-serif stack at the body level. Only setfontFamilywhen you want a different face — and prefersystem-ui, -apple-system, sans-serif-style stacks; if you need quoted multi-word names ('Segoe UI'), they're supported, but keep them inside the double-quoted value. - Hardcoding pixel font sizes everywhere. Large sizes get a
clamp()treatment by the renderer to scale down on small viewports — only setfontSizeto the desktop value and let the renderer handle the rest. - Unicode glyphs as icon stand-ins.
✓ ● ▾ ○read as unfinished. Use theiconnode type — two sets render by name as inline SVG: 1,900+ Lucide (icon: "check") and 3,800+ Material Symbols (icon: "material:check",iconStyle: "outlined" | "rounded" | "sharp",-fillsuffix for filled variants).I("parent", { type: "icon", icon: "material:check", iconSize: 16, iconColor: "$primary" }). For Material-style design systems, prefer the Material set. - Baking uppercase into
content. UsetextTransform: "uppercase"(withletterSpacingfor tracking) so the underlying copy stays editable and case-styling lives in the design, not the data. - Faking form controls from frames + ellipses.
toggle,checkbox,radio, andselectare real node types:I("parent", { type: "toggle", checked: true }). They style themselves from$accent/$border/$bg-surfacetokens (neutral fallbacks when unthemed) and stay pixel-consistent;fill/stroke/coloroverride. - Hand-building app furniture node by node. A data table, form field, toolbar, stat card, or settings row is one
apply_structurestamp: component-kind structures insert under anytargetIdwith re-keyed IDs and return anidMapfor populating. Stampdata-table, fill the placeholders, copy rows withC()— don't place ~80 nodes by hand.
Design tokens (colors, spacing, radius, typography) can live at three levels — workspace, project, and canvas. At render time the renderer merges them with the rightmost layer winning:
workspace.designSystem ──┐
├─→ merged tokens used to resolve $name references
project.designSystem ──┤
│
canvas.variables ──┘ (override layer)
Authoring rules:
- Reach for workspace tokens before hex codes. If you're working inside the
Coideworkspace, set the brand palette once viaworkspace_set_design_system({ workspaceId, variables: { colors: { primary: "..." } } }). Every canvas under that workspace can then referencefill: "$primary"and resolve to the brand value — no per-canvas redefinition. - Project layer is for sub-brand overrides. A
Coide → Marketingproject might overrideprimarywith the marketing accent while inheriting everything else from the workspace. - Canvas-level variables are escape hatches, not the primary surface. Use them when one canvas legitimately diverges from the design system; otherwise leave them empty and let the workspace tokens flow through.
- Presets work at every layer.
workspace_apply_preset({ workspaceId, preset: "dark" })copies the dark-preset tokens into the workspace;project_apply_presetand the existingapply_preset(canvas-level) do the same at their respective layers.
Merge semantics are per-category: a project that only sets colors doesn't reset the workspace's spacing/radius/typography. A canvas that only overrides colors.primary keeps every other workspace color.
Name the family and it loads. Set fontFamily in a typography token (or on a node) and the renderer resolves it from Google Fonts automatically — at token-write time and again as a render-time backstop. Binaries are cached under ~/.framesmith/fonts/, so after the first resolve, rendering is offline and deterministic. A family that can't be resolved degrades to the system fallback stack with a warning in the tool result — if a screenshot reports a font warning, act on it; the render is not showing the face you asked for.
// This is the whole happy path — no set_fonts call needed:
workspace_set_design_system({
workspaceId,
variables: { typography: { body: { fontSize: 16, fontFamily: 'Inter' }, code: { fontSize: 13, fontFamily: 'JetBrains Mono' } } },
});typography.body.fontFamilyis the document default (alias:base). Text nodes without an explicitfontFamilyrender in it — set it once at the workspace and the whole canvas follows.set_fontsis for everything else: non-Google sources (direct.woff2/.ttfbinary URLs,data:URIs), explicit registration by name (families: ["Inter"]), or pasting a Google Fonts stylesheet URL (fonts.googleapis.com/css2?...— faces are extracted automatically).font-display: swapis automatic. Paint isn't blocked on slow fonts.- System families never resolve (
system-ui,Roboto,Arial, …) — they're already on every render's fallback stack. Only the first non-system family of a stack is loaded.
A screen that already ships doesn't need redrawing — canvas_import_html (snippet + optional CSS) and canvas_import_url (live page) turn it into an editable, token-mapped canvas, and canvas_sync_from_url pixel-diffs the canvas against the live page later to catch drift.
The report is the contract. Imports are lossy by design; what makes them trustworthy is that they say exactly what happened. After every import, read:
report.snapped— values rewritten to$tokenrefs (Tailwind class intent likebg-surface, plus nearest-color matches against your design system).literalslists colors that found no token; near-ties are reported and left literal, never guessed.scaleMatchesnotes numbers that equal a scale token (gap 16 ≙$md) — informational, since number-typed props can't hold refs.report.layout— how each container's structure was reconstructed:table(rows of proportional columns),grid(rows from the computed track template),centered(auto-margin/max-width content kept centered at its real width),geometry(multi-column CSS clustered from bounding boxes). Astack-fallbackentry is your to-do list: that one container looked multi-column but couldn't be reconstructed confidently — fix it by hand; everything else arrived structurally correct, so don't rebuild imported tables or grids node-by-node.report.warnings/unmatchedFonts/unmatchedIcons— dropped background images, truncations, fonts and SVGs that didn't resolve.
Practical notes:
- A bare Tailwind snippet has no Tailwind runtime. The intent mapper covers the common utilities + the bundled v4 palette; pass the compiled stylesheet via
cssfor everything else. Live URLs always have real CSS, so this only matters for pasted snippets. - Token snapping defaults to the target project's merged design system — import into the right project and
tokenMatchneeds no configuration. Passtailwind: { theme }to map custom utility names. - Auth for gated pages (
auth.headers/cookies) lives in a throwaway browser context and is never persisted. - After importing:
screenshotto review fidelity, fix what the report flagged, then the canvas is the design-of-record — wirecanvas_sync_from_urlinto your workflow to keep it honest.
canvas_evaluatescores the design on 6 categories (spacing, color, typography, structure, consistency, cliche) and surfaces actionable issues withnodeIdreferences. Use it in a generator-evaluator loop:batch_design→canvas_evaluate→ fix the returned nodeIds.canvas_autofixrunscanvas_evaluateinternally and returns just the subset of issues that have a mechanically derived fix — off-scale spacing snaps to scale, missing layout becomesvertical, recoverable WCAG contrast failures get#000or#FFFbased on background luminance. Each fix is a ready-to-pastebatch_designUpdate op. Run those ops viabatch_design, then re-evaluate — closes the loop without judgment calls on your part.canvas_evaluatewithmode: "llm"runs fast-mode heuristics plus a vision-model critique against a fixed rubric (Claude or GPT-4.1, picked from env). Returns the heuristic result with an extrallmCritiquefield: five axes — hierarchy, execution, specificity, restraint, variety — each scored 1–5 with a rationale, plus a derived overall,summary,suggestions, andneedsRevision/failingAxes(any axis below thefloor, default 3). The verdict is stamped on the canvas + build log so quality is auditable over time. Use this for the "is this visually well-designed?" question heuristics can't answer — composition, hierarchy, polish. Costs one API call per run; reach for it after the heuristic score plateaus.canvas_revisecloses the loop: it judges, and for any failing axis asks the model for targetedbatch_designops, applies them, and re-judges — up tomaxIterationspasses (1–3). It mutates the canvas, reverts any pass that doesn't improve the overall, and stops on pass / cap / no-improvement. Opt-in and costly (≥2 API calls per pass); reach for it when you want the model to act on its own critique instead of you hand-translating it.screenshot_responsiverenders the same scene at mobile / tablet / desktop. Inspect all three; ifresponsivehints are set correctly the mobile layout will look right with no extra work.snapshot_layoutreturns computed bounding boxes — useful for asserting alignment or detecting overflow programmatically.- The human can point. The user leaves comments by toggling Comment mode in the viewer and clicking any element — no prose archaeology about which card they meant.
get_feedbackreturns those comments (each with a node snapshot — type / name / text — so you can act without extra lookups;orphaned: truemeans the node is gone but the concern likely still applies to its replacement). Check it whenever you pick up an existing canvas — feedback may have arrived while you were away, and the running server picks up viewer-written comments automatically. Open feedback blocks presenting, exactly like open inspector comments: address every item, then close each withresolve_feedbackand a one-line note saying what changed — the note is your reply to the user, shown in the viewer's Feedback tab. You won't miss waiting comments:canvas_listrows andcanvas_evaluateresults carry anopenFeedbackcount (the evaluate directive stays blocking while any are open), andinitreports the workspace total at session start.
The bar is "designers say wow," not "competent." The cliché tells below are the don'ts; these are the do's. Apply them up front — the evaluator is the safety net, not the plan.
- Start from a pattern, don't start blank.
list_structures→apply_structurestamps a taste-vetted page scaffold (every one is regression-tested to > 95 with zero cliché tells across themes). Adapt it — swap copy, set$tokens, vary the structure — rather than inventing layout from nothing. A blank canvas is where slop comes from. - Use the whole toolkit — a real UI uses these, so your design must too. Icons (
{ type: "icon", icon: "search" }— Lucide ormaterial:), fonts by name, real controls (toggle/checkbox/radio/select), components, and$tokens. Never fake them (no Unicode-glyph icons, no ellipse "toggles") and never omit them where a real UI has them: nav rows get a leading icon, metrics get an icon, feature lists get check icons, empty states get a glyph, forms use real controls. Starting from a pattern gives you all of this — don't strip it out. - One focal point per screen. Decide what the eye hits first (usually the headline or the primary action) and make everything else quieter. Two competing focal points = no focal point.
- Build real hierarchy. Size, weight, and color should encode importance in steps you can see — a display heading, a clearly smaller subhead, body, then muted captions. Avoid near-equal sizes (a 16→15→14 ramp reads as one blurry tier); aim for ~1.2–1.6× jumps.
- Keep one type scale and one spacing scale. Pick a small set of sizes and a spacing rhythm (e.g. 8 / 16 / 24 / 32 / 48) and reuse them. Off-scale one-offs are the most common craft tell.
- Let it breathe. Generous, consistent padding and whitespace read as designed; cramped, uneven spacing reads as machine-filled. Restraint beats density.
- One accent, flat color, no effects. A single accent hue plus neutrals. Flat
$surfacefills over gradients; a subtle near-black shadow over any glow. Off-black/off-white over pure#000/#fff. - Honest content. Labeled placeholders (
"Metric — to confirm") over invented numbers, names, or logos.
Polishing the design to the bar is your job, not the user's — they should never have to point out a missing icon, an off-scale gap, or a low score. So the loop is part of designing, not an afterthought:
canvas_evaluatethe canvas.- Resolve every warning and every cliché tell —
canvas_autofixfor the mechanical subset (spacing/contrast/known-default accent),batch_designfor the rest. (Cliché tells are info/warning but they're the slop signal — always fix them.) Pure advisories like "consider extracting components" are optional refinements that don't block. - Re-run
canvas_evaluate. - Repeat until there are zero warnings, zero cliché tells, and the score is > 95.
canvas_evaluate's result includes a directive field — it says READY TO PRESENT or NOT READY with what's left. Only present a design once it says READY. Don't ship the first attempt; ship the one that passes the bar.
canvas_evaluate scores craft (contrast, scale, structure) and a cliche category — the visual tells that read as machine-made. The bar is "designers say wow," not "competent": flat color and restraint beat effects. Steer away from these before you draw; the evaluator is the safety net, not the plan.
| Tell | What flags | Do this instead |
|---|---|---|
| Default purple / indigo accent | An accent (button, stroke, icon, accent text) in the indigo→violet band — especially the Tailwind defaults #6366f1 / #8b5cf6 / #7c3aed |
Pick an accent that fits the brand — a considered blue, green, or warm hue. Set it once as a $accent token. |
| Gradient / glow overuse | 3+ gradient nodes, or a colored glow/bloom shadow (large blur + a saturated or translucent-white color) | Flat $surface fills. Reserve a gradient for at most one deliberate focal moment; use a subtle near-black low-alpha shadow, not a halo. |
| Fake browser / OS chrome | A row of ≥3 small circular dots (mac traffic lights) wrapping content | Frame the content directly. Skip the fake window — it adds nothing and dates the mockup. |
| Hanging eyebrow header | A small eyebrow/tag beside a large heading in a horizontal row | Stack the eyebrow above the heading (layout: "vertical", left-aligned). |
| Fabricated content | Invented metrics / testimonials / brand logos in placeholder copy ("99.9% uptime", "— Jane Doe, CEO", "TechCrunch") |
Use a labeled placeholder until real data exists: "Uptime — to confirm" + a neutral block. Don't ship invented numbers. |
| Eyebrow rhythm | More eyebrow labels (small uppercase / letter-spaced text) than ~1 per 3 sections — an eyebrow above nearly every heading | Keep eyebrows rare (≤ ceil(sections / 3)). Let most headings stand alone; reserve the eyebrow for sections that genuinely need a kicker. |
| Slop copy | Stock AI phrasing in short copy — filler verbs ("Elevate", "Seamless", "Unleash"), scroll cues ("Scroll to explore"), placeholder names ("Jane Doe"), hype labels ("BETA", "Early access"), section-number eyebrows ("01 / Index") |
Write specific, branded copy that names the concrete benefit. |
| Radius consistency | 4+ distinct corner radii across the page — no single radius system | Consolidate to one small radius scale (e.g. 8 / 12 / 999 for pills). Define it as a $token and reuse. |
| Pure black / white | #000000 ink (text / icon / stroke / fill) or a #ffffff page background |
Use a designed off-black (#0A0A0A) for ink and an off-white (#FAFAFA) for the page surface. |
| Accent consistency | 3+ competing saturated accent hues (excludes neutrals + the page background) | Pick one accent hue — plus neutrals and at most one status color. Set it once as $accent. |
clicheis advisory — tells arewarning/info, never a hard error; they dent the score, they don't block.canvas_autofixfixes the mechanical ones — it swaps a known-default purple accent, deletes a fake-chrome strip, and softens pure black (#000000) ink to off-black. Gradient/glow, the hanging header, fabricated copy, eyebrow rhythm, slop copy, mixed radius systems, and competing accents carry a suggestion but no op (taste/judgment calls).- Genre relaxes intentional tells — pass
genre(or stamp a preset via provenance) so a style that legitimately uses a tell isn't nagged. Todaygenre: "material"allows the purple accent and white elevated surfaces (both are intentional in Material Design).
A few operational details that aren't obvious from the tool schemas:
- Scope to a repo with
init(orcanvas_bind) — binding re-keys IDs. Binding rewrites every project/canvas ID torepo-*form, so IDs captured before the bind stop resolving.initbinds and returns the fresh IDs in one call (prefer it); after a barecanvas_bind, re-list withproject_list/canvas_list. - Record
batch_design'snodeIdsmap.batch_designreturns{ ok, nodeIds, results }wherenodeIdsmaps each bound variable (header=I(...)) to the node ID it created. Bindings only live within a single call, so keep that map and target the real IDs in later calls rather than re-deriving them. - Typography
$tokensresolve.fontSizeonly. A$headingreference substitutes the token's font size;fontWeight/fontFamily/lineHeighton the token are not applied through the reference. Set those explicitly on the node alongside the$token. - Prefer the structured form for gradients & shadows.
gradient: { type, angle?, stops: [...] }andshadows: [{ x, y, blur, spread?, color, inset? }]. A raw CSS string is accepted too, but the structured form is canonical and diffs cleanly. import_design_mdis best-effort. It reads tokens per heading section in list / table /name: valueform (see the tool description for the exact accepted schema) and silently skips what it can't parse — colors deliberately reject shadow/gradient strings. Set anything it misses withset_variables. It honors explicit named spacing values and only synthesizes a scale from a statedBase unit:— it won't fabricate one otherwise.apply_presetrespects an inherited design system. It won't overwrite tokens a canvas resolves through the workspace/project layers; those are reported aspreservedFromDesignSystem. Pass them explicitly viaset_variablesif you actually want the preset's values.