Velloo documentation
Markdown for LLMs ↗Snippets
Reusable subtrees with typed params; your version of a primitive, kept in sync across every instance.
A snippet is a named, reusable subtree with typed parameters. It’s Velloo’s answer to “I want my own version of this component”. Instead of forking a Button into the design folder, you wrap it once and instantiate it everywhere. Every instance renders from the same definition, so a body edit lands in all of them at once.
Snippets live in snippets/<id>.json:
{
"id": "feature-card",
"name": "Feature Card",
"params": [
{ "name": "title", "type": "string" },
{ "name": "body", "type": "string" },
{ "name": "icon", "type": "icon", "default": "Sparkles" }
],
"tree": {
"$ref": "Card",
"props": { "className": "p-6 flex flex-col gap-3" },
"children": [
{ "$ref": "Icon", "props": { "name": { "$param": "icon" } } },
{ "$ref": "Heading", "props": { "level": 3, "children": { "$param": "title" } } },
{ "$ref": "Text", "props": { "children": { "$param": "body" } } }
]
}
}
Params
Each param declares a name and a type; the type drives both validation and the canvas inspector’s control:
| Type | Inspector control | Notes |
|---|---|---|
string | text input | |
number | numeric input | honors min / max / step |
boolean | checkbox | pair with $if for branching |
icon | lucide icon picker | string-valued; baked into emitted JSX as a literal tag |
color | color swatch, token-aware | string-valued |
enum | select | declare allowed strings in enum |
node | subtree slot | the instance passes a full node: icon blocks, overlays, actions |
A param may carry a default (used when the arg is omitted) or optional: true (may be omitted with no default; an omitted node slot simply renders nothing). One subtlety worth knowing: icon picks one glyph at design time. If the icon should vary per instance by status or priority, use a node param instead: a lucide name has to be a literal JSX tag in emitted code, so an icon param would collapse every instance to the same glyph.
Control forms: $param and $if
Inside a snippet body, two control forms are recognized anywhere a value appears, either as a child node or nested inside any props value:
{ "$param": "name" }is replaced with the matching arg at render time.{ "$if": "name", "then": value, "else": value }picks a branch on the truthiness ofargs.name.
A boolean param paired with $if can swap an entire className, so an “active” row and a muted row share one definition:
"className": {
"$if": "active",
"then": "rounded-md px-3 py-2 bg-accent text-accent-foreground font-medium",
"else": "rounded-md px-3 py-2 text-muted-foreground hover:bg-muted"
}
Use $if with boolean params. Other values are tested for JavaScript truthiness, so an empty string is false and the string "false" is true.
Instantiating
A screen references a snippet with a $snippet node. Agents don’t write that node by hand: in compose JSX, a snippet is a tag named after its id or name in PascalCase (feature-card / “Feature Card” → <FeatureCard>), and list_components with kind: "snippet" lists every snippet under its tag. Attributes are the args, and they’re validated against the declared params before anything is written. A missing required param or an unknown attribute is rejected with its line and column:
{
"screenId": "landing",
"mode": "append",
"parentPath": "@features",
"jsx": "<FeatureCard vellooId=\"fc-speed\" title=\"Fast\" body=\"Snappy by default.\" className=\"border-primary\" />"
}
That lands in the screen as:
{
"$snippet": "feature-card",
"$id": "fc-speed",
"args": { "title": "Fast", "body": "Snappy by default." },
"$extraClassName": "border-primary"
}
Non-string args use JSON braces (count={3}, active={true}). A snippet that declares a children param takes it from the tag’s content: text for a string param, or one element for a node param.
Three per-instance levers, in escalating order of specificity. All three are edited later through one tool, update_snippet_instance { screenId, path, … }, and they compose in a single call:
args: the declared params. Patch them withargPatch.$extraClassName: one-off Tailwind classes merged into the body’s root element at render time, for “this one should be wider” without touching the definition. Set it with theclassNameattribute on the tag, and change or clear it withextraClassName(a string, ornullto clear).$overrides: per-instance prop patches on nodes inside the body, keyed by an inner selector (an"@id"of a body node, a dotted index path like"0.2", or""for the root). Set them withinnerPathpluspropPatch. This is the escape hatch for “this one instance needs its badge red.” Instances carrying overrides are inlined byemit_coderather than emitted as calls to the shared component, because a shared React component can’t express them.
{
"screenId": "landing",
"path": "@fc-speed",
"argPatch": { "body": "Faster than it looks." },
"innerPath": "2",
"propPatch": { "className": "text-primary" }
}
From the screen’s perspective the instance is opaque: you can’t address body nodes with a screen path (see Nodes). The three levers above are the entire per-instance surface.
Editing the body
Snippet bodies are first-class editable surfaces. The node-level tools update_props, move_node, remove_node, and set_node_id accept a virtualized screen id of the form snippet:<snippetId>, routing the edit into the body with the same path semantics and error model as a screen:
{ "screenId": "snippet:feature-card", "patches": [{ "path": [0], "style": "size-5 text-primary" }] }
The canvas’s snippet editor drives the exact same surface. Edits broadcast to every screen using the snippet. Two caveats: compose only targets real screens, and $param / $if forms aren’t component nodes, so adding nodes or introducing a new placeholder means update_snippet with a patched tree. See Editing a snippet body.
For a surgical definition-level prop change there’s also update_snippet’s innerPatch ({ innerPath, propPatch }), the definition-side counterpart of update_snippet_instance’s innerPath + propPatch. It uses the same addressing, but the patch lands in the body and every instance sees it, with no full-tree resend:
{
"snippetId": "feature-card",
"patch": { "innerPatch": { "innerPath": "1", "propPatch": { "level": 2 } } }
}
After defining or reworking a snippet, render it in isolation with render_snippet: $param wiring bugs and $if truthiness mistakes are silent at definition time and only surface when rendered. Its result includes any class or theme diagnostics next to the screenshot.
To find snippets nothing uses any more, for example after a board delete or a screen rewrite, call list_components with unusedOnly: true.
Snippets vs extensions
- Snippet: a composition of components that already exist in the folder’s library. Lives in the design folder, renders faithfully on the canvas, emits as a parameterized component (or inlined) via
emit_snippet. - Extension: a component that exists only in your app (a bespoke DataTable, a chart). Velloo can’t render its internals, so the canvas shows a placeholder (or a live island) and codegen emits a real import.
Rule of thumb: if you could build it from list_components output, it’s a snippet.
Next
- Registering app-owned components: Extensions.
- The snippet tool surface in full: MCP snippets reference.