⬡ FRAMEWERK
Core

Components and slots

Components are named reusable subtrees declared once and referenced anywhere; slots project layout content into them, and states (some entered by hover or press) name sets of their values.

View as Markdown

Components and slots

A component is a named, reusable layout subtree declared once under components and referenced anywhere by a component node; a slot projects layout content — not just string props — into it.

Examples

A stat card with typed props:

A stat card component
phone
SPEC.JSON
RENDERED OUTPUTFRAMEWERK RENDERER
Revenue
$12,400
4% vs last month
Orders
318
-2% vs last month
Edit the JSON to update the rendered screen.Edits stay in this preview

An app shell with a slot, applied as a screen root:

A shell with a body slot
phone
SPEC.JSON
RENDERED OUTPUTFRAMEWERK RENDERER
My App
Welcome
Edit the JSON to update the rendered screen.Edits stay in this preview

Anatomy

A prop is interpolated into a text prop with {{name}} (single braces, {name}, also work). props declares the prop names and types; defaults supplies values used when an instance omits them.

The prop list is the contract. Whoever places a component — a person or a model — sees its name, its summary and its props, never its layout. So each prop declares its real type (boolean for an on/off, number for a count), a closed set of values is an enum, and a prop whose name would not tell a stranger what to pass has a description. A value that does not fit its type is a validation warning: "yes" on a boolean, "3 pts" on a number. A boolean accepts "true"/"false" or JSON true/false; a number accepts "5" or 5.

A prop carries the value; the layout carries the words. change: 4 with "{{change}}% vs last month" in the layout, not change: "+4% vs last month". The words then stay consistent across every instance and can be translated.

A colour is a tone, never a hex. A component that shows a status takes an enum and lets the style choose the colours, so they follow the theme into both modes: tone: "success" resolves to var(--success-light) before style tokens do.

A list is one prop. Declare the shape of one row in items, and repeat over the list with forEach: "$p(<prop>) as <row>".

A prop value may read state. The fenced spelling, {$name}, in an instance's prop resolves against the state the instance is rendered in: "props": { "total": "{$total}" }. One instance then shows every value and follows a press, where a screen would otherwise stack one instance per value behind showWhen. Screen state, app state and $env all read this way. Only the fenced form is a value; a bare $role == coach in a prop is an expression, forwarded as written and evaluated where it lands. A name nothing declares passes absence and is a validation warning.

A state is a name for a set of values. states is an ordered list of { name, props }. Each key in props is a prop the component declares or one of its own stateVars, so a menu held in a state variable has an open state as easily as a banner has an error state: { "name": "switcher-open", "props": { "switcher": "true" } }. An instance pins one with "state": "switcher-open", and inside the component $state.switcher-open reads "true". Precedence is defaults, then the pinned state, then the instance's own props. A key declared as neither, a value outside a state variable's options, and a name declared as both a prop and a state variable are validation warnings.

A state can show or hide a layer. A node's overrides read $state.<name>, so one component can hold looks that differ in their layers, not only in their values: a layer only one state draws carries "hidden": true and an override { "when": "$state.hover == true", "props": { "hidden": false } }, and a layer that state does not draw is hidden by { "when": "$state.hover == true", "props": { "hidden": true } }. With no state pinned the component draws its layers as written. A Figma variant set imports this way when its variants add or remove layers. An override on a nested component node may set its state, so a state can show a nested component in another of its own states.

null removes a prop. In a node's overrides entry, or in a state's props, a value of null takes the prop away while the override applies or the state is pinned, so the node draws as if it had never been set; a key left out changes nothing (JSON Merge Patch). A disabled input that draws no fill is { "when": "$state.disabled == true", "props": { "background": null } } on a row whose background is "rgba(0,0,0,0.45)". In a state's props, null takes away the component's default for that prop; a value the instance writes itself still wins. A null for a prop the node does not set, and that no other override on it sets, removes nothing and is a validation warning. A Figma variant that lacks a property its set's most-used variant has imports this way.

A state can be entered by the pointer. A state with "interaction": "hover" is shown while the pointer is over an instance, and one with "interaction": "pressed" while the pointer is down on it; when the pointer leaves or lets go, the instance goes back to the state it pins. A set with a hover per resting look says which one each hover is entered from: { "name": "danger-hover", "interaction": "hover", "from": "danger" }. A hover without from is entered from any state that has no hover of its own. This happens only in a played prototype; a capture pins it with $forceState <instance id>:hover or <instance id>:press. focus is reserved, and nothing enters it yet.

Each instance keeps its own state. A setState inside an instance on a variable the component declares in its stateVars changes that instance only, so two switches on one screen flip separately. A variable the component does not declare is the screen's, and the write changes it for everything that reads it.

A component may also carry a summary: what it is for, and when to use a sibling instead. A component has no title and no route — only the key it is stored under — so that sentence is what separates card from athlete-card. Aim for 100 words or fewer.

A group says where the component sits in the component library: "Dashboard", or a nested path like "Forms/Inputs" (Inputs inside Forms). A component without one is listed under Ungrouped.

Reference it with a component node: { "type": "component", "ref": "stat-card", "props": { … } }.

Instance overrides. Layout props set on the instance are merged onto the component's resolved root, so one definition can adapt without a variant explosion.

Slots. ComponentDef.slots lists the slot names a component offers; a slot node names one with name and may carry a fallback node, rendered when the instance projects nothing into it. Alternatively, mark any container with slot: "body" to make it an inline slot target — projected content replaces its children while the container keeps its own layout props. A component node can be a screen's root, which is exactly how the shell above is applied.

Varying one part without a slot. nodeOverrides on an instance edits a node of the definition by its _id; the value is the same prop bag a plain node takes, and it merges rather than replaces. It may not set _id, type or children — that is what slots are for. An address the definition does not contain warns at validation with the available ids. A bag may carry overrides, which replaces that node's conditional list for this instance only (the root is addressed by its own _id). A pinned state's override wins over the instance's own props, so an instance that keeps its own value for a prop its state also sets carries the node's list without that state's entry for the prop; the Figma importer writes this.

Stability

Component and slot fields (props, defaults, slots, nodeOverrides, forEach) are part of the format contract. Adding a field is additive; changing how {{…}} interpolation or slot projection resolves is breaking.