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" })The chart node owns the value→coordinate math — give it data and domains, never hand-computed path d strings or absolutely-positioned tick labels:
- One node per chart, multi-series inside.
series: [{ data, stroke, strokeDasharray?, area?, points? }]— the 4-series pace-to-goal chart is one node with a shared scale. - Dash is the convention for projected vs actual. Solid accent line +
strokeDasharray: "6 4"reference/forecast lines — don't differentiate similar lines by colour alone. - X positions are data indexes. A 7-point booked series against a 12-point target series stops at 7/12 of the width automatically;
xDomain/yDomaindefault from the data (bars floor at 0). - Furniture is opt-in props:
gridlines: 4,xLabels/yLabels(spread evenly; empty strings skip intermediate ticks),curve: "smooth",kind: "bar"for grouped bars. - Editing a data point is a one-prop edit — update the series array, done.
- 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.
Shared chrome is a component, not a copy-paste. When the same chunk exists (or is about to exist) twice — an app shell, a stat card, a table row — promote it and instance it instead of re-authoring:
- Build it once, name the parts you'll vary (
name: "Title",name: "ActiveNav"). create_component({ canvasId, nodeId })— the subtree becomes a component and aninstancereplaces it, render-identical. The result'soverridableChildrenlists what you can override.- Stamp more copies:
I("parent", { type: "instance", componentId, overrides: { "Title": { content: "Settings" } } })— overrides match def children by name; instance-level props (width, opacity) override the def root. - Cross-canvas:
copy_nodes({ fromCanvasId, nodeIds: [instanceId], toCanvasId })— the component def travels with the copy, so 15 sibling screens share one shell definition.
canvas_evaluate's "no component instances found" advisory is the nudge; these two tools are the action. Current sharp edge: batch_design ops address the tree, so they can't edit a def after promotion — a def child's id no longer resolves (those nodes left the tree), and U(instanceId, ...) sets instance-level props, not the def. To revise a component: build the new version as a plain subtree, create_component it, point instances at the new componentId with U(), and delete the scaffold.
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."mono"and"sans"are generic shorthands. They render as CSSmonospace/sans-serif— no registration, no network, never warned. Upgrade one to a real face anytime:set_fonts({ fonts: [{ family: "mono", url: "<JetBrains Mono css2 URL>" }] })— the label you pass is what gets registered, so existingfontFamily: "mono"nodes pick up the real face with no edits.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 and registered under yourfamilylabel; the result'saliasedfield shows the mapping).- Typos surface at write time.
batch_designwarns when a call writes afontFamilythat is neither cached, registered, nor a system/generic family — don't wait three renders to notice"JetBrans Mono". 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 (gap, scalar padding, and array padding via a whole-array snap), missing layout becomesvertical, recoverable WCAG contrast failures get#000or#FFFbased on background luminance. Passapply: trueto write every fix in the same call (the result reports applied/failed per op), or run the returned ops viabatch_designyourself — either way, re-evaluate after. 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.
Which number is the gate? The heuristic directive (fast mode) is the presentation gate — always available, no API key. mode: "llm" adds a vision-model rubric critique (composition, hierarchy, polish) on top; it needs ANTHROPIC_API_KEY or OPENAI_API_KEY and degrades gracefully without one — when it's unavailable, the heuristic directive alone decides. And calibrate the evaluator to what the screen is: a data-dense product screen evaluated without genre: "dashboard" will flag its own figures as fabricated and pin the score below the bar with no path up — that's a miscalibrated gate, not a bad design (see Cliché & craft below).
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. Exception: on a dashboard/analytics mockup the realistic figures ARE the design — pass genre: "dashboard" (alias "data") so they aren't flagged. |
| 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.genre: "material"allows the purple accent and white elevated surfaces (both are intentional in Material Design);genre: "dashboard"(alias"data") allows realistic figures on data-dense product screens (relaxeshonest-content). Declare the genre the screen actually is — don't use it to dodge flags on a marketing page.
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. - Same change across many nodes? Use
replace_matching_properties, not NU()ops. It applies onesetto every node matching a property/value predicate (AND across keys; token refs like"$surface"match literally), withscope(subtree) andtypefilters. Preview wide matches withdryRun: truefirst — a common value likewidth: 150can match more nodes than intended (making a 17-row table fill its container is one call, not 68U()ops). - 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. Lost track anyway?find_nodesrecovers ids by property/text/name with a readable path per match — use it instead of eyeballingread_nodestrees; editing a guessed id is how the wrong node gets restyled. - Matching an existing app's type scale? Pin it with typography tokens. The type-scale check flags adjacent sizes at a ratio below 1.1, which a deliberately dense scale (14/13/12/11) trips by design. Declare those sizes as typography tokens (
set_variables) — a pair where both sizes are token-declared is pinned and skips the ratio check; undeclared one-offs still flag. Declaring the scale is the intentionality signal. - 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. - Row rules and accent bars are per-side borders, not layout hacks.
borderTop: { width: 1, color: "$border" }on each table row gives hairline separators withgap: 0;borderLeft: { width: 3, color: "$primary" }marks the active row.style: "dashed" | "dotted"(andstrokeStylefor the all-sidesstroke) is the convention for forecast/placeholder/draft;strokeDasharray: "6 4"dashes SVG paths — the projected-vs-actual convention in charts. Never simulate hairlines withgap: 1+ background bleed-through — it couples separation to spacing and fights the spacing linter. - 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.