Props and scope
Studio writes this format for you. Editing an instance's props in the Properties panel — see Working with components — writes the
$propsobjects on this page.
Scope is where a name can be seen. Within one document, every
state
entry is visible to every descendant element — no passing required. Across a component boundary, nothing is visible unless it is passed explicitly through
$props
. That single rule makes data flow statically knowable: you can read a file and see exactly what it depends on.
{
"tagName": "my-card",
"$props": {
"title": "Static string",
"count": { "$ref": "#/state/count" }
}
}
Component instances
A component instance is created in two steps:
register
the component document under a custom-element tag in the top-level
$elements
map, then place an element node with that tag.
$props
is optional — an instance without it renders the component with its own defaults:
{
"$elements": {
"my-counter": { "$ref": "./components/my-counter.json" },
"my-card": { "$ref": "./components/card.json" }
},
"children": [
{ "tagName": "my-counter" },
{
"tagName": "my-card",
"$props": {
"title": "Hello",
"count": { "$ref": "#/state/count" }
}
}
]
}
A bare
{ "$ref": "./card.json" }
placed directly in
children
is
not
a component instance — it renders an empty
<div>
. Register the document in
$elements
and instantiate it by its tag, as above. See spec §13.
Static and bound props
A prop value is either a plain JSON value (fixed for this instance) or a
$ref
(bound to the parent's state). Functions pass the same way, so a child can trigger behavior the parent owns:
{
"tagName": "my-card",
"$props": {
"title": "Static string",
"count": { "$ref": "#/state/count" },
"onAction": { "$ref": "#/state/handleAction" }
}
}
Inside
card.json
,
title
and
count
behave like the component's own state entries — typically declared there with defaults, overridden per instance.
Signal forwarding
When a
$props
value is a
$ref
to a reactive state entry, the child receives the
same reactive reference
, not a copy. A write in either scope triggers updates in both — parent and child stay in sync through one shared signal. A plain value, by contrast, is just an initial setting for that instance.
Scope levels
| Level | Scope | Mirrors |
|---|---|---|
window |
Application-wide | window
global |
document |
Document-wide | document
object |
| Component | Custom element boundary | CSS custom property scope |
Globals are reachable from any component via the
window#/
and
document#/
reference schemes
; everything else is bounded at the component.
Resolution order
When a name is looked up, scopes are consulted in order:
$map/context — the enclosing repeater iterationLocal component
stateExplicitly passed
$propswindowglobalsdocumentglobals
How it works
Each component builds its own reactive scope from its own
state
. When the runtime renders an instance, it resolves each
$props
entry against the
parent's
scope, then layers the results onto the child's scope — plain values as initial settings,
$ref
values as live references into the parent's reactive state. Nothing else crosses over: a template string or
$ref
inside the child can only see the child's scope plus what was passed.
Rules
$propsis the only mechanism for passing state across a component boundary — scope never leaks implicitly.Within a single component, all
stateentries are available to all descendant elements without passing.Signal scope is bounded at the component (custom element) level, like a CSS custom property.
Private
#-prefixed state entries can never be set via$props.A
$refprop is live in both directions; a plain value is per-instance and static.Every external dependency must appear in
$props— that is what makes documents statically analyzable.
Related
Components — the component model and custom elements
State — declaring the entries props override
References —
parent#/,window#/,document#/schemesDynamic switching — swapping whole components on state
Working with components — the Studio props workflow