Component definition
The component.json reference.
A component's definition, component.json, describes it to the paywall designer, the Voidhash
agent and publishing. #[component] builds the same definition into your module, and publishing
checks that the two agree on everything the component does: its traits, props, events, preview
states, capabilities and size. The id, version, title, description, icon, category and keywords
are the bundle's own.
{
"id": "acme/plan-picker",
"version": "1.0.0",
"title": "Plan picker",
"description": "Selectable plans with a purchase button.",
"icon": { "kind": "builtin", "name": "list" },
"category": "Pricing",
"keywords": ["plans", "pricing"],
"implementation": { "kind": "wasm", "abi": 2 },
"traits": { "frame": { "resize": "width" }, "appearance": {}, "effects": {} },
"children": { "kind": "none" },
"props": [
{ "name": "plans", "type": { "kind": "list", "of": { "kind": "product" } }, "default": [] },
{ "name": "accent", "title": "Accent", "type": { "kind": "color" }, "default": "#7c3aed" }
],
"events": [
{ "name": "selected", "payload": [{ "name": "productId", "type": { "kind": "string" } }] }
],
"previewStates": ["default"],
"capabilities": ["commerce"],
"panel": false,
"fallback": "placeholder",
"defaultSize": { "width": "fill", "height": "hug" }
}A bundle you publish to your organization needs a
component.json. Components in your project's .voidhash working copy need none: their versions
take the definition #[component] builds into the module.
Fields
| Field | Meaning |
|---|---|
id | <namespace>/<name>. The namespace is your organization's; the name uses lowercase letters, digits and dashes. |
version | A semantic version such as 1.2.0. Each published version is permanent, so bump it for every publish. |
title | The name the designer shows. |
description | One or two sentences about what the component does. |
icon | { "kind": "builtin", "name": "list" } (such as component, list, timer, gift), or ship an icon.png. |
category | The group the designer lists the component in, such as Pricing. |
keywords | Words people search for in the designer. |
implementation | { "kind": "wasm", "abi": 2 } for components built with voidhash-ui. |
traits | The designer sections and canvas behavior the component takes part in. See Traits. |
children | { "kind": "none" }, { "kind": "any" }, or { "kind": "slots", "slots": [{ "name": "default" }] }. |
props | The settings the designer shows. See Props. |
events | What the component emits, such as a selection, that paywalls bind to actions. |
previewStates | The states the designer can preview the component in. The first is the default. |
capabilities | What the component can do for the person using your app. See Capabilities. |
panel | Whether the component ships its own editor panel. Without one, the designer shows its props. |
fallback | What an app that cannot run the component shows instead: placeholder, hide or children. |
defaultSize | The size a new instance starts at: fill, hug or a number of points, per axis. |
Traits
A trait gives every instance of your component a designer section and the canvas behavior that goes with it, exactly as the built-in layers have them. An instance keeps only the settings of the traits its definition declares; the paywall shows nothing else.
| Trait | Designer section | What instances get |
|---|---|---|
frame | Position | Position, size, margins and how the instance sits in its parent. |
layout | Layout | Padding, gap, alignment and clipping of what the component holds. |
appearance | Appearance | Opacity, blend mode, visibility and corner radius. |
fill | Fill | Backgrounds: colors, gradients and image fills. |
border | Border | A border. |
outline | Outline | An outline. |
effects | Effects | Shadows, blurs, rotation and flips. |
states | States | States that restyle the instance when their condition holds. |
interactions | Interactions | Tap actions on the instance itself. |
variables | Variables | Variables declared on the instance. |
sharedElement | Shared element | A shared element id, so the instance flies between screens. |
Text layers have one more trait, typography, for their font and text style. Component instances
have no text style of their own, so declaring typography gives them nothing. Expose the text
settings your component needs as props instead.
The same traits decide what an editor panel can edit: a panel reaches only the style of the traits its component declares.
Resizing
frame takes resize, which sets the handles the canvas offers:
both: width and height.widthorheight: handles for that axis only.none: no handles; the instance keeps its size.
Add aspect: "intrinsic" to keep the instance's proportions while it is resized. A component
without frame sizes itself from defaultSize and cannot be resized on the canvas.
In Rust, declare traits on the component:
#[component(traits(frame(resize = "width"), appearance, effects), size(width = "fill", height = "hug"))]Slots
A component with children: { "kind": "slots" } shows layers placed inside it where its code
renders slot() (the default slot) or slot_named("footer"). In the designer, each layer
inside the component names the slot it goes in.
Props
Each prop has a name, a type and a default. The designer reads the other fields to show it
well:
| Field | Meaning |
|---|---|
title | The prop's label. Defaults to its name. |
description | A sentence about what it does. |
group | A heading the prop is shown under. Props without one come first. |
visibleWhen | Shows the prop only while another prop has a value: { "kind": "equals", "prop": "style", "value": "ring" }, or oneOf with values. |
bindable | Whether the prop can take a variable instead of a value. On for scalar props unless set false. |
localizable | Whether the prop has a value per locale, for translated paywalls. Props of any kind can. |
The type sets what the prop holds and the control the designer shows for it:
| Type | Value |
|---|---|
string | Text. multiline shows a larger field; maxLength limits its length. |
number | A number. min, max and step shape the field; unit is px, %, s, deg or x. |
boolean | On or off. |
enum | One of its options: [{ "value": "ring", "title": "Ring" }]. An option can add an icon. |
color | A hex color, such as #7c3aed. |
fill | A color or fill the component paints with. |
asset | An image from the project's assets: { "kind": "asset", "accepts": ["image"] }. |
product | One of the project's products. |
object | A group of fields, each a prop of its own. |
list | A list of another type: { "kind": "list", "of": { "kind": "product" } }. maxItems caps it. |
Scalar props are those of every type but object and list.
Props in Rust
Every parameter after cx in a #[component] function is a prop. Its name is the parameter's
name in camelCase, and its type follows the Rust type: String, numbers, bool, Color, Fill,
Image or Asset, Product or ProductId, an enum deriving PropEnum, and Option or Vec
of those. #[prop(…)] sets the rest:
pub fn Countdown(
cx: Cx,
#[prop(default = 900, min = 0, max = 3600, step = 1, unit = "s", group = "Timing")]
duration: f64,
#[prop(title = "Headline", localizable, max_length = 40)] title: String,
#[prop(accepts(image))] badge: Asset,
#[prop(visible_when(prop = "style", equals = "ring"))] thickness: f64,
#[prop(max_items = 4)] features: Vec<String>,
style: Style,
) -> ViewAn enum deriving PropEnum becomes an enum prop with one option per variant. Name each option
with #[prop_enum(title = "…")] when its variant name does not read well.
The definition must declare what the component's code declares: the same traits, props, events, preview states, capabilities, panel and default size. Publishing names every difference it finds.
Older app versions
People keep using older versions of your app after you publish. Voidhash only sends a paywall to an app whose SDK can run every component in it. An app with an older SDK gets the answer it gets for a placement with nothing assigned, so it shows your own fallback instead of a broken paywall. Updating the SDK in your app makes the paywall reach it.
The same check applies to experiments. When an app cannot show the variant it is assigned, it is shown nothing at that placement and is not counted as exposed to the variant.
Some components run natively inside the SDK. A paywall with one still reaches an app whose SDK is
recent but does not include that component: the app shows the component's fallback in its
place. Choose placeholder, hide or children with that in mind.