---
title: "ui.element@1 · OpenVibe.Services"
canonical: "https://openvibe.services/docs/contracts/ui.element"
updated: null
---

# ui.element@1 · OpenVibe.Services

HTML: [ui.element@1 · OpenVibe.Services](https://openvibe.services/docs/contracts/ui.element)

# UiElement `ui.element@1`

Generated at 2026-11-13 06:25:48 UTC from [`openvibe-contracts v0.419.0`](https://github.com/OpenVibers/OpenVibe.Contracts/tree/v0.419.0) and [`openvibe-sdk v0.161.0`](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.161.0).

- Version: 1.0.0
- Owner: contracts
- Visibility: public
- Status: active
- Compatibility: backward
- Schema: [`https://openvibe.network/contracts/ui/element.v1.json`](/docs/contracts/ui.element.json)

ui.element@1: an Element people make and place on a surface: a look, a behaviour and, only when a recipe is not enough, a mod. LOOK draws from OpenVibe.Design's components and the theme's tokens, so it follows the person's theme, the seasons and dark/light; it carries no markup and no URL. RECIPE is data only: one or more triggers from ui.trigger@1, the conditions that decide who may fire it and what it costs, and one or more actions from ui.action@1, checked against CAPABILITIES. A capability is an action kind the element declares; the host refuses an action the element did not declare, and the product named by the action still checks the presser's role, the cooldown, the cost and its own limits. MOD is an optional reference to a code module (id, version and digest) for the stage where a recipe is not enough; the module never adds a power. Conditions use channel points only in stage 1: no Vibes, no money, and Billing still confirms any charge itself. The id and revision are assigned by the store; a draft has neither.

## Fields

| Field | Type | Required | Description | Constraints |
| --- | --- | --- | --- | --- |
| `id` | string |  | Assigned by the store when the element is saved; absent in a draft. | - `pattern ^el_[0-9A-HJKMNP-TV-Z]{26}$` |
| `revision` | integer |  | Assigned by the store; every saved change moves it by one. | - `minimum 1` |
| `name` | string | yes |  | - `minLength 1`<br>- `maxLength 80` |
| `description` | string |  |  | - `maxLength 500` |
| `author` | object | yes | The author's display name and, for a private or link element, the subject. A gallery or copy answer carries the display name only, never the account. | - `no other fields` |
| `author.name` | string | yes |  | - `minLength 1`<br>- `maxLength 80` |
| `author.subject` | [identity.subject-ref](/docs/contracts/identity.subject-ref) |  |  |  |
| `sharing` | enum | yes |  | - `one of "private", "link", "gallery"` |
| `look` | object | yes |  | - `no other fields` |
| `look.component` | string | yes | An OpenVibe.Design component name (design.component@1), e.g. button or meter. A name the page does not have is refused, never fetched. | - `pattern ^[a-z][a-z0-9-]{0,63}$` |
| `look.label` | string | yes | The words on the element, e.g. Make it dance. | - `minLength 1`<br>- `maxLength 40` |
| `look.icon` | string |  | A glyph name from the design icon set. | - `pattern ^[a-z][a-z0-9-]{1,31}$` |
| `look.props` | object |  | The design component's own props (the sliders the editor shows). Numbers, strings and booleans only; no nesting. |  |
| `look.style` | `https://openvibe.network/contracts/ui/element.v1.json#/$defs/style` |  |  |  |
| `recipe` | object | yes |  | - `no other fields` |
| `recipe.triggers` | [ui.trigger](/docs/contracts/ui.trigger) | yes |  | - `minItems 1`<br>- `maxItems 8` |
| `recipe.conditions` | `https://openvibe.network/contracts/ui/element.v1.json#/$defs/conditions` |  |  |  |
| `recipe.actions` | [ui.action](/docs/contracts/ui.action) | yes |  | - `minItems 1`<br>- `maxItems 16` |
| `capabilities` | `https://openvibe.network/contracts/ui/action.v1.json#/$defs/capability` | yes | The action kinds this element may request. Every action in the recipe must be declared here; the host refuses the rest. | - `minItems 1`<br>- `maxItems 32`<br>- `unique items` |
| `initial_state` | `https://openvibe.network/contracts/ui/element-state.v1.json#/$defs/values` |  | The element's state before anything runs; the counter starts at 0, never at a value the element did not declare. |  |
| `mod` | object |  | A code module for an element whose recipe is not enough (stage 3). It runs sandboxed; the reference is data, and the host checks every request the module makes against the element's capabilities. | - `no other fields` |
| `mod.id` | string | yes |  | - `pattern ^mod_[0-9A-HJKMNP-TV-Z]{26}$` |
| `mod.version` | string | yes |  | - `pattern ^(0\|[1-9][0-9]*)\.(0\|[1-9][0-9]*)\.(0\|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$` |
| `mod.sha256` | string | yes |  | - `pattern ^[0-9a-f]{64}$` |
| `mod.capabilities` | `https://openvibe.network/contracts/ui/action.v1.json#/$defs/capability` |  | The subset of the element's capabilities the module itself may request; omitted means the element's declared capabilities, and a product may grant fewer. | - `maxItems 32`<br>- `unique items` |
| `created_at` | string |  |  | - `format date-time` |
| `updated_at` | string |  |  | - `format date-time` |

## Examples

From the contract's own test fixtures: valid ones validate, rejected ones must fail.

 Valid: chat-counter

~~~~
{
  "name": "Headlight tally",
  "author": {
    "name": "Ada"
  },
  "sharing": "private",
  "look": {
    "component": "counter",
    "label": "Headlights",
    "style": {
      "tint": "surface",
      "shape": "square",
      "opacity": 0.8,
      "show_label": true
    }
  },
  "recipe": {
    "triggers": [
      {
        "kind": "chat_command",
        "command": "!headlights"
      }
    ],
    "conditions": {
      "role": "everyone",
      "cooldown": {
        "per_person_seconds": 30,
        "overall_seconds": 5
      }
    },
    "actions": [
      {
        "kind": "screen.counter",
        "product": "live",
        "key": "headlights",
        "by": 1
      },
      {
        "kind": "chat.message",
        "product": "chat",
        "text": "Headlights on!"
      }
    ]
  },
  "capabilities": [
    "screen.counter",
    "chat.message"
  ],
  "initial_state": {
    "headlights": 0
  }
}
~~~~

Valid: dance-button

~~~~
{
  "id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
  "revision": 3,
  "name": "Make it dance",
  "description": "The robot spins for a moment and confetti follows.",
  "author": {
    "name": "Maticus",
    "subject": {
      "type": "user",
      "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
    }
  },
  "sharing": "gallery",
  "look": {
    "component": "button",
    "label": "Make it dance",
    "icon": "robot",
    "props": {
      "size": "large"
    },
    "style": {
      "tint": "accent",
      "shape": "pill",
      "opacity": 1,
      "show_label": true
    }
  },
  "recipe": {
    "triggers": [
      {
        "kind": "press"
      }
    ],
    "conditions": {
      "role": "follower",
      "cost": {
        "points": 50
      },
      "cooldown": {
        "per_person_seconds": 60,
        "overall_seconds": 10
      },
      "live_only": true,
      "daily_cap": {
        "per_person": 5
      },
      "chance": {
        "one_in": 1
      }
    },
    "actions": [
      {
        "kind": "robot.drive_pulse",
        "product": "bot",
        "direction": "spin_left",
        "ms": 800,
        "speed": 0.5
      },
      {
        "kind": "screen.confetti",
        "product": "live",
        "intensity": "medium"
      }
    ]
  },
  "capabilities": [
    "robot.drive_pulse",
    "screen.confetti"
  ],
  "initial_state": {
    "dances": 0
  },
  "created_at": "2026-11-11T20:00:00.000Z",
  "updated_at": "2026-11-11T20:30:00.000Z"
}
~~~~

Valid: mod-wave

~~~~
{
  "name": "Wheel of chatters",
  "author": {
    "name": "Ada"
  },
  "sharing": "link",
  "look": {
    "component": "wheel",
    "label": "Spin the wheel"
  },
  "recipe": {
    "triggers": [
      {
        "kind": "press"
      },
      {
        "kind": "chat_command",
        "command": "!wheel"
      }
    ],
    "actions": [
      {
        "kind": "robot.say",
        "product": "bot",
        "text": "The wheel says hello."
      }
    ]
  },
  "capabilities": [
    "robot.say"
  ],
  "mod": {
    "id": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ",
    "version": "1.2.0",
    "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "capabilities": [
      "robot.say"
    ]
  }
}
~~~~

Rejected: missing-actions

~~~~
{
  "name": "Bad",
  "author": {
    "name": "Ada"
  },
  "sharing": "private",
  "look": {
    "component": "button",
    "label": "Bad"
  },
  "recipe": {
    "triggers": [
      {
        "kind": "press"
      }
    ]
  },
  "capabilities": [
    "robot.say"
  ]
}
~~~~

Rejected: nested-state

~~~~
{
  "name": "Bad",
  "author": {
    "name": "Ada"
  },
  "sharing": "private",
  "look": {
    "component": "button",
    "label": "Bad"
  },
  "recipe": {
    "triggers": [
      {
        "kind": "press"
      }
    ],
    "actions": [
      {
        "kind": "robot.say",
        "product": "bot",
        "text": "hi"
      }
    ]
  },
  "capabilities": [
    "robot.say"
  ],
  "initial_state": {
    "wheel": {
      "entries": [
        "a"
      ]
    }
  }
}
~~~~

Rejected: unknown-action

~~~~
{
  "name": "Bad",
  "author": {
    "name": "Ada"
  },
  "sharing": "private",
  "look": {
    "component": "button",
    "label": "Bad"
  },
  "recipe": {
    "triggers": [
      {
        "kind": "press"
      }
    ],
    "actions": [
      {
        "kind": "robot.dance",
        "product": "bot"
      }
    ]
  },
  "capabilities": [
    "robot.say"
  ]
}
~~~~

## Validate

~~~~
const contracts = require('openvibe-contracts');
contracts.validate('ui.element@1', value);   // { valid, errors: [{ path, message }] }
~~~~
