Props and scope
Studio writes this format for you. Editing an instance's props in the Inspector's Content tab (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, with 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, and 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 and overridden per instance.
A prop may share its name with a built-in element property such as
title
,
lang
or
hidden
. Setting it still updates the component, including a value that arrives after the element is on the page, and the matching HTML attribute is set as well.
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, so 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
Attribute props
A component that lists an entry in
observedAttributes
can also be given that prop from markup, which is how a component reached from plain HTML or from another framework gets its values:
{
"tagName": "my-card",
"observedAttributes": ["title", "count", "featured"],
"state": {
"title": { "type": "string", "default": "Untitled", "attribute": "title" },
"count": { "type": "number", "default": 1, "attribute": "count" },
"featured": { "type": "boolean", "default": false, "attribute": "featured" }
}
}
An attribute is text, so it is coerced by the entry's type on the way in: a
string
is taken as written, a
number
goes through
Number()
, and a
boolean
is read by presence, where the literal text
false
counts as absent. An attribute already on the element when it connects is read before
$props
are merged, so a property a parent set still wins.
Removing an attribute restores the entry's declared default , rather than leaving the prop holding nothing:
<my-card title="Hello" count="7"></my-card>
<!-- After removeAttribute("count"): count is 1, its declared default, not 0 -->
That is what makes a numeric prop able to say "unset". A component whose entry is declared in the shorthand form (
"title": "Untitled"
) has that literal as its default; a computed entry, one written with
$expression
,
$prototype
,
$ref
or
$src
, has no default and a removal leaves it alone.
Give any prop you expect a host to clear a
default
you would be happy to see. It is the value the prop returns to, and it is the value a reader gets after any code path removes the attribute.
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, which 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