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.

component.json
{
  "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

FieldMeaning
id<namespace>/<name>. The namespace is your organization's; the name uses lowercase letters, digits and dashes.
versionA semantic version such as 1.2.0. Each published version is permanent, so bump it for every publish.
titleThe name the designer shows.
descriptionOne or two sentences about what the component does.
icon{ "kind": "builtin", "name": "list" } (such as component, list, timer, gift), or ship an icon.png.
categoryThe group the designer lists the component in, such as Pricing.
keywordsWords people search for in the designer.
implementation{ "kind": "wasm", "abi": 2 } for components built with voidhash-ui.
traitsThe designer sections and canvas behavior the component takes part in. See Traits.
children{ "kind": "none" }, { "kind": "any" }, or { "kind": "slots", "slots": [{ "name": "default" }] }.
propsThe settings the designer shows. See Props.
eventsWhat the component emits, such as a selection, that paywalls bind to actions.
previewStatesThe states the designer can preview the component in. The first is the default.
capabilitiesWhat the component can do for the person using your app. See Capabilities.
panelWhether the component ships its own editor panel. Without one, the designer shows its props.
fallbackWhat an app that cannot run the component shows instead: placeholder, hide or children.
defaultSizeThe 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.

TraitDesigner sectionWhat instances get
framePositionPosition, size, margins and how the instance sits in its parent.
layoutLayoutPadding, gap, alignment and clipping of what the component holds.
appearanceAppearanceOpacity, blend mode, visibility and corner radius.
fillFillBackgrounds: colors, gradients and image fills.
borderBorderA border.
outlineOutlineAn outline.
effectsEffectsShadows, blurs, rotation and flips.
statesStatesStates that restyle the instance when their condition holds.
interactionsInteractionsTap actions on the instance itself.
variablesVariablesVariables declared on the instance.
sharedElementShared elementA 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.
  • width or height: 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:

FieldMeaning
titleThe prop's label. Defaults to its name.
descriptionA sentence about what it does.
groupA heading the prop is shown under. Props without one come first.
visibleWhenShows the prop only while another prop has a value: { "kind": "equals", "prop": "style", "value": "ring" }, or oneOf with values.
bindableWhether the prop can take a variable instead of a value. On for scalar props unless set false.
localizableWhether 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:

TypeValue
stringText. multiline shows a larger field; maxLength limits its length.
numberA number. min, max and step shape the field; unit is px, %, s, deg or x.
booleanOn or off.
enumOne of its options: [{ "value": "ring", "title": "Ring" }]. An option can add an icon.
colorA hex color, such as #7c3aed.
fillA color or fill the component paints with.
assetAn image from the project's assets: { "kind": "asset", "accepts": ["image"] }.
productOne of the project's products.
objectA group of fields, each a prop of its own.
listA 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,
) -> View

An 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.