openpencil/packages/docs/user-guide/components.md
Danila Poyarkov e0716a3a38
feat: behaviours and preview mode (#893)
* feat: author behaviours on main components

A main component or component set can behave as a Switch, Checkbox,
Slider, or Tabs, after Reka UI's primitives. The behaviour lives in
OpenPencil plugin data: boolean values bind to variant or boolean
properties with the values meaning on and off, a number keeps its own
range since Figma has no number property, and the control's
subcomponents bind to the component's slots. A Behaviour section in the
properties panel adds, binds, and removes it, each as one undo step,
and flags required bindings that are missing. The canvas-only layout's
pill becomes a component that preview will reuse.

* feat: preview instances with behaviours on the canvas

View > Preview (Cmd+Alt+Enter) puts the canvas in preview: a lone canvas
switches to the canvas-only layout with a Previewing pill, and a split
canvas previews on its own side. Clicking a Switch or Checkbox flips it,
dragging a Slider moves its thumb and range, and clicking a Tabs trigger
shows its panel. Preview keeps its state on copies of the instances it
touched, in a private graph with the document's ids, and the canvas
draws those copies in place of the originals, so the document, undo,
autosave, and collaborators never see it. Escape or the pill leaves
preview, Reset restores every control, and editing shortcuts, labels,
and outlines stay off while previewing.

* feat: translate behaviour and preview strings; cover preview with an e2e flow

* refactor(vue): reuse VariantDefinitionControl for behaviour property options

* refactor: split variant actions and preview interactions by domain

Variant authoring was one 706-line closure; it is now graph queries
(model), undo snapshots (history), property definition edits
(definitions), and the editor facade (index). Preview interactions move
into play/kinds, one module per control, registered by behaviour kind so
a new kind cannot ship without its contract and interaction. Behaviour
contracts are keyed by kind. In the Vue SDK, slot and variant authoring
controls get their own folders beside component-props and behaviour,
and the app's variant section joins slot/ and behaviour/.

* refactor: keep the behaviour model in scene-graph's plugin-data registry

Master now defines every OpenPencil plugin-data key in one typed registry
in scene-graph. The behaviour schema registers there as a field, and the
model and contracts move beside slots, exported from the package root;
the @open-pencil/core/behaviours subpath is gone.

* feat: interaction states and keyboard focus in preview

A behaviour can bind a variant property to the default, hover, pressed,
focus, and disabled states; binding it maps values named like those
states. Preview switches the instance's copy to the matching variant as
the pointer hovers, presses, and releases, keeps other values when the
set draws the combination and falls back to rest otherwise, and skips
disabled instances. Tab moves visible keyboard focus between controls,
Space, Enter, arrows, Home, and End use the focused one, and Escape
takes visible focus off before leaving preview. A Button kind covers
controls that only have states.

* feat: toggle, radio, group, progress, collapsible, and accordion behaviours

Radio group, toggle group, and accordion hold their items in a slot;
each item is an instance with its own behaviour, so a press inside the
slot goes to the group, which turns the pressed item on and the others
off through the item's own interaction. Progress shares the slider's
number handling through rangeControl, and a collapsible shows and hides
its content slot from its trigger, remembering its open state even
when no property draws it. Tabs and groups share arrow-key navigation.

* feat: text field, textarea, and number field behaviours

A behaviour value can now be text, bound to a text property, so
preview types into a copy of the field through the same property path
the editor uses. A bound Filled value switches to the placeholder
variant when the field empties. A number field keeps its own range,
shows its value through a text property, and steps from its increment
and decrement slots and the arrow keys. Text fields show focus from a
click, and the focused control receives every key; Option still types,
and only Cmd or Ctrl combinations stay shortcuts.

* fix: keep behaviour bindings when saving as .fig

Saving as .fig gives component properties new GUIDs, but behaviours
kept the old ids in their plugin data, so every binding read as missing
after reopening. The export now renames the ids behaviours bind with
the same GUIDs, on its own copy of the document.

* fix: let previewed controls resize layout imported from .fig

Layers from a .fig keep the sizes Figma computed, and auto layout
prefers them, so an opened collapsible or accordion item kept its
closed height in preview. When preview shows, hides, or retypes a
layer in a copy, it drops those sizes from the layer's copied ancestors
so auto layout sizes them again; untouched layers keep Figma's sizes.

* fix: publish behaviours and other plugin content with library assets

Every OpenPencil plugin-data field now declares its role: content that
exists only as plugin data (behaviours, OkHCL picks), format copies of
node fields written for files, or bookkeeping about where a document
or node came from. Library snapshots keep a node's content plugin data,
including other plugins' entries, and drop the rest; the asset hash
counts the same entries, so a behaviour-only change is offered as an
update while a .fig round trip still changes nothing.

* feat: name behaviour rows by meaning and create what they need

The Behaviour section named every main value "Value" under a "Values"
heading, and a component without matching properties left an empty
picker with no way forward. Rows are now named for the control (On,
Checked, Pressed, Text), rows the control needs or already uses come
first, and the optional rest folds under More options; a button keeps
its states in view. An empty row creates what it needs in one undo
step: a text layer and text property, Off and On variants on a set, or
a slot frame for a part. The missing chip names the row it means and
takes you there.

* fix(dom-css): position free layers, hug content, and round ellipses

HTML and Tailwind export stacked the layers of frames without auto
layout in block flow, wrote fixed pixel sizes for auto layout frames
set to Hug and for auto-sizing text, and drew ellipses as boxes. Layers
a parent does not lay out are now absolutely positioned at their
coordinates inside a relative frame, hugging axes are left to the
content, and ellipses get a 50% radius.

* feat: run preview as live Reka UI islands over the canvas

Preview simulated controls on the canvas: copies of instances, a
handler per kind, its own key routing, and append-only text. It now
runs them as real components. Each top-level layer that holds an
instance with a behaviour becomes an island: its layers are projected
to DOM through dom-css into a shadow root laid over the pane at its pan
and zoom, and each behaviour mounts its Reka UI primitives on its
layers, so text fields are real inputs and focus, keys, and layout are
the browser's. Core's resolvePlayState shows instances in a state on a
private graph, so the component's variants draw it, and controls are
keyed by layer path so a variant switch keeps their DOM. The canvas
leaves island layers to the islands, and the canvas play runtime and
its key routing are gone.

* fix: derive variant properties from Property=Value component names

figma.combineAsVariants and Combine as variants only derived variant
properties from slash-separated names, so components named as Figma
names variants, such as State=On, Size=Large, became a set with no
properties. Both now derive each named property and its values, after
the slash form.

* feat: script and tool access to behaviours by name

Behaviour contracts follow Reka UI's anatomy: tabs keep their triggers
in the list slot and their content panels in a panels slot, and a slot
of repeated parts names the Reka part of its children. A behaviour
spec names component properties and slots instead of ids and resolves
to the stored behaviour and back, with errors that list what the
component has.

Scripts get an `openpencil` global next to `figma`, in the Figma API's
style: setBehaviour, getBehaviour with bindValue, bindPart, states,
and missing, behaviourKinds, and createSlot. The eval tool, the CLI,
and app automation compile scripts through one compileScript, so the
CLI now returns the last expression as the others do. MCP and AI chat
get set_behaviour, get_behaviour, and create_slot.

* feat: write controls in design JSX with Reka UI's element names

`<Switch.Root modelValue="State">` renders a main component, or a set
when its children are variants, that behaves as a switch, and
`<Switch.Thumb>` the slot that draws its thumb, one slot across the
set's variants. Inputs become the text property of a field, tab
triggers and panels go in their List and Panels slots, and a group's
items are `<RadioGroup.Item of={…} />` instances in its Items slot.
JSX export writes components with behaviours the same way, so they
render back unchanged. The authoring reference documents controls, and
the codegen and chat prompts now include it verbatim instead of
dedenting its code examples.

* chore: format the CLI export test

* docs: document slots, behaviours, preview, and the openpencil API

The components guide covers slots, behaviours, and preview with its
shortcut; scripting covers the openpencil global and eval's last-
expression result; the MCP and AI chat pages list the new tools; the
features overview, README, and roadmap mention working controls. The
chat prompt says how to build a control, and the codegen prompt builds
components with behaviours on their Reka UI primitives.

* chore: format the eval CLI test

* docs: explain behaviours and preview islands, and guide the openpencil API

A development page explains the behaviour model, the four authoring
surfaces, how preview islands turn a control's state into live Reka UI
components, and how to add a kind; the architecture page links it. The
Core guide sets the rules for OpenPencilAPI: Figma-only `figma`,
OpenPencil features on `openpencil` in the same style, one
compileScript, names over ids, and docs with every member. Package
READMEs mention the openpencil global, PlayIslands, Reka-named JSX, and
the behaviour model. Design JSX's behaviour modules move into a
behaviours folder instead of a suffixed sibling.

* refactor: center pasted layers through translate

centerNodesAt repeated translate's loop, which test:dupes reports on
master too.

* fix: validate behaviour ranges and guess on and off by name

A number value now needs max above min and a positive step: the schema,
specs, and the panel reject a range a slider cannot step through. Binding
a variant property guesses on and off by value name, as specs do, and a
boolean property gets no on/off pair. Part bindings are read through
partBinding, a replaced document restarts preview from its designed
state, and the e2e preview shortcut uses ControlOrMeta.

* feat: make the Behaviour section say what to do next

A slider's range fields now carry inline Min, Max, Step, and Start labels.
States offers Add state variants, which adds a Default, Hover, Pressed,
Focus, and Disabled variant and binds them; Add Off and On variants and
Add state variants turn a lone main component into a component set first,
and a part's slot can be added to a set, in every variant under one slot
id. Rows that could do nothing are gone: no empty pickers and no hints to
combine variants by hand, and an unbound Disabled is left to the states.
A warning line names what is still needed and replaces the missing chip,
and the Switch's main value is called Checked.

* fix: keep each slot to one part and keep creating slots at hand

A slot draws one part, so the Behaviour section no longer offers a slot
another part uses, and specs (the openpencil API, tools, and JSX) reject
binding one slot to two parts. A part's picker keeps an action to add a
new slot in its footer, so adding the first slot no longer hides it for
the other parts.
2026-10-06 13:23:05 +00:00

11 KiB

title description
Components Creating reusable components, instances, component sets, overrides, and live sync in OpenPencil.

Components

Components are reusable design elements. Edit the main component and all its instances update automatically.

Browse Components

Open the Assets tab in the left panel to browse local components and enabled libraries. Use grid or list view, search by component name, and select a component to see its details. You can insert an asset by clicking it, pressing Enter, or dragging it onto the canvas.

Local assets are grouped by source page. Published library assets remain available when their revision has been downloaded, including when the remote provider is temporarily offline.

Creating a Component

Select a frame or group and press ⌥⌘K (Ctrl + Alt + K). The selection becomes a reusable component.

Any other layer, or several layers, is wrapped in a new white component at their bounding box, in the topmost layer's place in the layer list; a single wrapped layer gives the component its name.

Components display a purple label with a diamond icon above them.

Component Sets and Variants

Select two or more components and press ⇧⌘K (Shift + Ctrl + K) to combine them into a component set — a container with a dashed purple border and 20 px padding around its children, as in Figma. Sets made by scripts with figma.combineAsVariants() wrap their components exactly, as Figma's plugin API does.

Each component in a set can define values across multiple variant dimensions, such as Size=Small, State=Hover, and Theme=Dark. OpenPencil supports sparse combinations, so a set does not need every possible combination. The top-left variant is the default and is used as the fallback when an update no longer contains an exact combination.

Use the component properties panel to add, rename, reorder, and remove variant dimensions and values. Duplicate combinations are rejected.

Component Properties

Components and component sets support reusable text, boolean visibility, instance-swap, and slot properties. Link a property to a descendant field, then select an instance to edit its assigned value without detaching it. Properties and assignments are preserved when saving and reopening .fig files.

Slots

A slot is a frame of a main component whose content each instance can change. Select a frame inside a main component and choose Create slot from the context menu or the Slots section, or select other layers to wrap them in a new slot frame. Slot settings set a description, the components it prefers, and how many items it holds; an instance outside those limits shows a warning. In an instance, add, reorder, and remove a slot's items, or reset it to the component's content.

Behaviours and Preview

A behaviour makes a main component or component set work like a real control, after Reka UI's primitives: Button, Text field, Textarea, Number field, Toggle, Switch, Checkbox, Radio, Radio group, Toggle group, Slider, Progress, Tabs, Collapsible, or Accordion. Select the component and use + in the Behaviour section to choose one.

The section lists what the control needs:

  • Values — the property that holds each value: a variant or boolean property for On, Checked, Pressed, Open, Filled, or Disabled (with the variant values that mean on and off), a text property for a field's text, or a slider's own minimum, maximum, step, and default.
  • Parts — the slot that draws each part, such as a switch's thumb, a slider's track, range, and thumb, or a tab list. A group's items slot holds instances of its radios, toggles, or collapsibles.
  • States — a variant property whose values draw default, hover, pressed, focus, and disabled. Values named like those states are matched automatically.

Rows the control requires come first; the rest are under More options. When a component has nothing to bind yet, a row offers to create it: a text layer and text property, Off and On variants, or a slot, and States offers Add state variants, which adds a Default, Hover, Pressed, Focus, and Disabled variant of the component. A lone main component that gets variants this way becomes a component set, and a slot added to a set appears in every variant. Until the control works, a line under its name says what is still needed; clicking it goes there.

Press ⌥⌘↩ (Ctrl + Alt + Enter), choose View → Preview, or use ▶ next to Share to preview the canvas. Each top-level layer holding controls runs as live Reka UI components over the canvas, drawn by the component's variants: switches flip, sliders drag, tabs and accordions open, and text fields are real inputs with the browser's caret, selection, and paste. Preview never changes the document or its history. Reset puts every control back as designed, and Esc or the pill's close button returns to editing. In split view, each canvas previews on its own.

Behaviours are saved in .fig files and published with library components.

Component Libraries

A component library publishes reusable components as an immutable revision. Each published asset has stable library, asset, and revision identity, so different instances can remain on different revisions until you explicitly update them.

Publish a Library

  1. Create the components and component sets you want to share.
  2. Open Assets, then select Manage libraries.
  3. Select Publish library.
  4. Enter a stable library ID and display name. The library ID is locked after the first publication.
  5. Optionally search the change list and enter a revision description.
  6. Select the added, modified, renamed, or removed assets to include.
  7. Confirm the destination and select Publish library.

On later publications, unchecked changes remain pending. Unchanged assets keep their previous published definitions, and removed definitions remain available while documents still reference their historical revision.

Enable and Insert Library Assets

Open Assets → Manage libraries to enable a published library. Its components appear in the Assets panel alongside local components. Insert one by clicking it, using the keyboard, or dragging it onto the canvas.

Published definitions are read-only in consuming documents. Edit the source document and publish another revision to change a definition. Instances linked to those definitions remain editable through their component properties and overrides.

Review and Accept Updates

Open Manage libraries → Updates to discover newer revisions. Discovery does not modify the document. You can review the current and updated instance side by side, navigate between affected instances, and then update:

  • The selected instance
  • All instances of one asset
  • Instances on the current page
  • Instances across all pages

OpenPencil preserves compatible text, visibility, and instance-swap assignments. If an exact variant no longer exists, the review identifies the top-left fallback before you accept it. Applying an update creates an undo entry.

Local, Storage, and Offline Use

Libraries can use the local browser catalog or a configured storage provider. Remote publication uses immutable revision objects and a conditional latest pointer, preventing two publishers from silently overwriting each other.

Downloaded revisions are cached locally. A document can continue rendering and inserting downloaded definitions while offline. Integrity failures are reported instead of being hidden by cached data.

Saving Consumer Documents

Enabled-library bindings and materialized definitions are saved with .fig documents. Reopening a consumer file preserves its linked instances and revision identities, even when its remote library is unavailable.

Creating Instances

Right-click a component and select Create instance from the context menu. The instance appears 40 px to the right of the source component, visually identical.

Instance creation is available only through the context menu — there's no toolbar button.

Detaching an Instance

Select an instance and press ⌥⌘B (Ctrl + Alt + B) to detach it. The instance becomes a regular frame with no link to the original component. All overrides are baked in.

Go to Main Component

Right-click an instance and select Go to main component. The editor navigates to and selects the main component, switching pages if needed.

Live Sync

When you edit a component, all its instances update automatically. Synced properties include:

  • Width and height
  • Fills, strokes, and effects
  • Opacity and corner radii
  • Layout properties (auto layout settings)
  • Clips content setting

Sync triggers automatically after node updates, moves, and resizes within a component.

Overrides

Instances can override specific properties without breaking the sync link. When a property is overridden on an instance, that property is skipped during sync — other properties continue to update from the main component.

Overridable Properties

Child-level overrides support: name, text, font size, font weight, font family, plus all visual and layout properties (fills, strokes, effects, opacity, corner radii, size).

New Children

When you add a child to a component, all existing instances gain a cloned copy automatically. Child order in instances always matches the component.

Hit Testing

Components and instances are opaque containers — clicking on a child selects the component itself, not the child. Double-click to enter the component and select children inside it.

Visual Treatment

Element Appearance
Component label Purple with diamond icon, always visible
Instance label Purple with diamond icon, always visible
Component set border Dashed purple outline

Keyboard Shortcuts

Action Mac Windows / Linux
Create component ⌥⌘K Ctrl + Alt + K
Create component set ⇧⌘K Shift + Ctrl + K
Detach instance ⌥⌘B Ctrl + Alt + B
Preview ⌥⌘↩ Ctrl + Alt + Enter

Tips

  • Editing text inside an instance creates an override — the text won't be overwritten when the component changes.
  • Use component sets to organize multidimensional variants such as size, state, and theme.
  • Publish reusable assets from their source document; published definitions are intentionally read-only in consumer documents.
  • Review updates before accepting them when a revision removes an exact variant combination.
  • See Context Menu for all component-related actions.