Skip to content

Behaviours and Preview ​

A behaviour makes a main component or component set work as a real control: a Switch, Slider, Tabs, Text field, and so on, after Reka UI's primitives. It adds no node types. It describes which of the component's existing properties hold the control's values, which of its slots draw the control's parts, and which variants draw its interaction states. Preview then runs the control as the real Reka UI component, and code export can generate the same.

The model ​

The model lives in @open-pencil/scene-graph (packages/scene-graph/src/behaviours/).

  • Kinds and contracts (kinds.ts). BEHAVIOUR_CONTRACTS gives each kind its values and parts, keyed by kind, so a kind without a contract does not type-check. Contracts follow Reka UI's anatomy: every Reka subcomponent is a slot of the component, the component itself is the root, and a slot that holds repeated parts names the Reka part of its children, such as the tab list's triggers or a group's items.
  • Storage (schema.ts, model.ts). A behaviour is stored as the behaviour field of 4B Pen's plugin data on the component or set, with properties and slots bound by id. Its role is content, so it travels with library assets and counts towards their update hash. .fig export renames the bound ids to the GUIDs it gives the properties.
  • Values: a boolean is a variant property (with the values that mean on and off) or a boolean property; text is a text property; a number is the behaviour's own range, since Figma has no number property; a choice is which child of a slot is active.
  • States: a variant property whose values draw rest, hover, pressed, focus, and disabled.
  • Specs (spec.ts). behaviourFromSpec and behaviourToSpec convert between the stored behaviour and a spec that names properties and slots, which is how every authoring surface except the panel writes it. Errors name what the component has.

Authoring surfaces ​

All of them write the same model.

SurfaceWhereNotes
Behaviour sectionsrc/components/properties/component-properties/behaviour/, useBehaviour in packages/vueBinds by id; Core's editor actions (setBehaviour, addBehaviourText, addBehaviourVariant, addBehaviourPart) create what is missing in one undo step.
openpencil scripting APIpackages/core/src/openpencil-api/The openpencil global next to figma in every script runner, through compileScript.
Toolspackages/core/src/tools/create/behaviours.tsset_behaviour, get_behaviour, create_slot, built on the openpencil API.
Design JSXpackages/design-jsx/src/behaviours/, export/behaviours.tsReka-named elements (Switch.Root, Switch.Thumb) render and export components with behaviours.

Preview islands ​

Preview is a per-pane mode (EditorViewState.play). It runs controls as real components; it never simulates them on the canvas.

  1. Islands. playIslandRoots (Core) picks the top-level layers of the page that hold an instance with a behaviour. The renderer skips them while the pane previews, and PlayIslands (packages/vue/src/canvas/islands/) mounts one island per root, laid over the canvas at the pan and zoom of the canvas's last drawn frame, so the islands never run ahead of a canvas still catching up while panning and zooming.
  2. Shadow DOM. Each island renders into its own shadow root: app CSS does not reach in, island CSS does not leak out, fonts already loaded by the app apply, and the island moves with the canvas in the same frame. Wheel gestures over an island are passed to the canvas.
  3. State to design. An island keeps each control's state (values, choice, number, typed text, hover, pressed, focus). Core's resolvePlayState copies the island's layers into a private graph and shows each instance in its state: the matching variant, boolean properties, and layers a control reveals, such as tab panels. The copy comes from the page extractor history and export use (extractPageContext), with every variant of each instance's set so it can switch, and the document's variables and images shared rather than copied, since a state never changes them. The document is never changed.
  4. Design to DOM. The private graph is projected to DOM through @open-pencil/dom-css, the same projection HTML export uses, so island fidelity and export fidelity improve together.
  5. DOM to Reka. Each projected layer has a role: a control's root, a part, a group's item, a tab trigger or panel, or a text input. behaviourControls and controlRoles in @open-pencil/dom-css (packages/dom-css/src/behaviours/) assign them, so code export reads the same roles. Roles wrap the layer in its Reka UI primitive with asChild, so Reka renders through the designed element. Typing, focus, keyboard navigation, and accessibility are the browser's and Reka's.

Controls and DOM keys use layer paths (names below the island root), not node ids: a variant switch rebuilds an instance's layers with new ids, and a stable path keeps the DOM in place, including a focused input.

Code export ​

Export reads the same model as preview. behaviourArgs gives a component set its props: a boolean value drawn by an on/off variant property becomes checked, pressed, open, or disabled.

stateStyles merges a set's variants into one markup tree, matching layers by layer path:

  • The rest variant is the base. Each other variant becomes a rule holding only what it changes, under conditions on the control's root.
  • The conditions are the attributes Reka and Radix set, data-state and data-disabled; the browser's :hover, :active, and :focus-visible; and a data-* attribute for any other variant property, which the generated component sets from its prop.
  • Layers only some variants draw stay in the tree, hidden where absent, and a label that reads differently is a layer per state. A combination the set doesn't draw combines from its parts, and a drawn combination keeps only what its parts don't already give.

stateStylesToCSS writes the result as a stylesheet with a readable class per layer. stateStylesToTailwind writes it as utilities behind state variants, with a named group per control so a control nested in another reacts only to its own root.

Adding a kind or a part ​

  1. Add the kind to BEHAVIOUR_KINDS and its contract to BEHAVIOUR_CONTRACTS in scene-graph; part ids follow Reka's part names.
  2. Map it in preview: its root and parts in packages/vue/src/canvas/islands/controls/, with the Reka primitives it uses.
  3. Name its elements in design JSX: REKA_ELEMENTS in packages/design-jsx/src/behaviours/index.ts and the export's root table in export/behaviours.ts.
  4. Label it in the Behaviour section (labels.ts) and translate the strings.
  5. Cover the model and spec in scene-graph tests, preview in tests/e2e/components/behaviours.spec.ts, and JSX in packages/core/tests/design-jsx/behaviours.test.ts.

Released under the MIT License.