Skip to content

Velloo documentation

Markdown for LLMs ↗

Extensions and live islands

Hand-registering a host-app component Velloo can't simply render, with placeholders by default and opt-in live mounts for charts.

Sometimes a screen needs a component Velloo can’t simply render for you: a chart wired to your data layer, a widget that only works inside a runtime the canvas has no way to stand up. Extensions let the agent tell Velloo the component exists, what props it takes, and where it imports from, so the node can sit in a screen tree, render as something on the canvas, and emit as a real import in code.

Declaring one

Extensions live in the extensions map of .design/config.json, usually written by the agent via the add_extension tool:

"extensions": {
  "DataTable": {
    "importPath": "@/components/data-table",
    "category": "ui",
    "description": "Sortable, paginated table",
    "props": [
      { "name": "data", "type": "any[]", "optional": false, "control": "string" },
      { "name": "sortable", "type": "boolean | undefined", "optional": true, "control": "boolean" }
    ],
    "origin": "agent"
  }
}
  • importPath is emitted by codegen exactly as written: an alias path (@/components/data-table), a relative path, or an npm package. Use the alias your app actually resolves.
  • props is a hand-authored schema: each entry has a name, a display type string, optional, and a control (boolean, number, string, color, enum with enumValues, or icon). It mirrors a library component’s manifest entry, so the canvas inspector renders the same controls for either.
  • category (ui or typography, default ui) and description drive how the component is grouped and described in the canvas Library.
  • app (monorepos only) names the config.hostApps entry the component lives in, so a live island bundles against that app’s root, aliases, and React copy. Omit it in a single-app folder.
  • origin records who registered it: "agent" (via the tool) or "manual" (you edited config.json). Informational today.

Once declared, the component is referenced from screens like any other node: in compose JSX it’s just a tag (<DataTable data={[]} sortable={true} />), stored as { "$ref": "DataTable", "props": { … } }. list_components with kind: "extension" lists what’s registered.

Folder-global, and shadowing

Extensions are folder-global: every screen, in every library the folder uses, sees them. An extension also shadows a library component with the same id: register a Button extension and your Button wins over shadcn’s on every screen. add_extension surfaces a shadowedLibraryComponent field in its response when this happens, so an accidental collision is visible immediately; rename unless you meant it.

Lifecycle is agent-driven: update_extension is a sparse patch of props, importPath, description, category, render, fit, and app, and unlisted fields keep their values; remove_extension refuses while any screen or snippet tree still references the id, and returns the offending nodes. There’s no rename: remove and re-add, which naturally surfaces every existing reference first.

What the canvas shows: Tier 1

By default (render: "static", or simply omitted) an extension node renders as a labelled placeholder card: dashed border, the component id, a compact summary of the resolved prop values, and the importPath. Not the real component, but exactly enough to design layout around: it takes up space, participates in flex and grid, and theme-flips with the rest of the canvas because it’s styled with semantic tokens.

Codegen is unaffected by the placeholder: emit_code writes a real import from the importPath and mounts the component with the node’s props.

Live islands: render: "live"

For components whose visual fidelity matters on the canvas (charts, above all), an extension can opt into a live preview:

{ "importPath": "@/components/charts/PriceChart", "render": "live", "props": [ … ] }

Velloo bundles the actual component from your app (resolved from the importPath against the host app and its node_modules, so it’s your exact recharts) and client-mounts it into the server-rendered marker. The SSR skeleton is the same placeholder card, so nothing breaks when the bundle isn’t ready; any bundle or render failure falls back to the placeholder rather than erroring the canvas.

Constraints, by design:

  • Visual-only. The mounted island keeps pointer-events: none. Clicking it selects the node like any other, so the design-mode interaction model survives. No in-canvas tooltips or chart interactions.
  • Browser-renderable. The component must resolve from the host app and carry no server-only imports.
  • Sizing is controlled by fit: "aspect-video" (default) locks a 16:9 box, right for charts that fill a responsive container, while "content" lets a fixed-height chart or an absolute-inset overlay drive its own height.

This is the one sanctioned crack in “designs are static”: real client React runs inside the island, but selection still wins and the rest of the tree stays inert. Note the built-in Chart component already ships a canvas preview and needs no extension. Reach for render: "live" when the app’s own chart components have to pixel-match.

Extensions vs snippets

SnippetExtension
What it isA composition of library componentsA component that exists only in your app
Lives insnippets/<id>.jsonextensions map in config
Canvas renderFully realPlaceholder, or live island
CodegenParameterized component or inlined subtreeA real import from importPath

If you could build it from list_components output, make a snippet. If Velloo would have to reimplement your code to render it, register an extension. Variations of an existing component need neither: that’s props and class overrides.

Next