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 5 categories (spacing, color, typography, structure, consistency) 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.
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. |
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 and deletes a fake-chrome strip. Gradient/glow, the hanging header, and fabricated copy 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 purple.
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.