Interactivity
Handle actions, forms, and state in OpenUI components.
OpenUI components can be interactive. The Renderer manages form state automatically and exposes callbacks for actions and persistence.
Actions
When a user clicks a button or follow-up, the component calls triggerAction. The Renderer turns the click into an event, an ActionEvent, and passes it to onAction.
<Renderer
library={myLibrary}
response={content}
onAction={(event) => {
if (event.type === "continue_conversation") {
// event.humanFriendlyMessage: button label or follow-up text
// event.formState: field values at time of click
// event.formName: scoping form name, if any
}
}}
/>ActionEvent
| Field | Type | Description |
|---|---|---|
type | string | Action type (see built-in types below). |
params | Record<string, any> | Extra parameters from the component. |
humanFriendlyMessage | string | Display label for the action. |
formState | Record<string, any> | undefined | Raw field state at time of action. |
formName | string | undefined | Form that scoped the action, if any. |
Built-in action types
// Dispatched via onAction callback
enum BuiltinActionType {
ContinueConversation = "continue_conversation",
OpenUrl = "open_url",
}ContinueConversation: sends the user's intent back to the LLM (@ToAssistant). The optional context from@ToAssistant(message, context)arrives unchanged inparams.context, and can be any value.OpenUrl: opens a URL in a new tab (@OpenUrl). The URL is inparams.url.
A custom action from your library arrives with its own name as type and its arguments in params. When the library has custom actions, onAction is typed from the library, and checking event.type narrows event.params.
The renderer runs these actions itself and does not send them to onAction:
@Run(ref): executes a Mutation or re-fetches a Query@Set($var, value): changes a reactive$variable@Reset($var1, $var2): restores$variablesto their declared defaults
In OpenUI Lang, one action can be passed on its own, and several actions go in Action([...]):
askBtn = Button("Tell me more", @ToAssistant("Tell me more about these tickets"))
submitBtn = Button("Create", Action([@Run(createResult), @Run(tickets), @Reset($title)]))See Actions for the full rules.
See Reactive State for $variables and Queries & Mutations for @Run.
Inline mode
When inlineMode is enabled in the prompt config, the LLM can respond with either:
- Code (fenced in triple backticks), for creating or changing the UI
- Text only, for answering questions without modifying the UI
The parser extracts code from fences automatically. Text outside fences is shown as chat.
Using triggerAction in components
Inside defineComponent, use the useTriggerAction hook:
const MyButton = defineComponent({
name: "MyButton",
description: "A clickable button.",
props: z.object({ label: z.string() }),
component: ({ props }) => {
const triggerAction = useTriggerAction();
return <button onClick={() => triggerAction(props.label)}>{props.label}</button>;
},
});triggerAction(userMessage, formName?, action?) accepts optional second and third arguments. To let the model choose what a component does on click, give it an action() prop and pass that prop as the third argument. See Action props in your components.
Reactive state ($variables)
In OpenUI Lang, $variables hold reactive state that components can read and write. When the model writes $days = "7" and passes it to a Select, the renderer keeps the Select and $days in sync.
For component authors building custom libraries, the useStateField hook provides this binding:
import { useStateField, reactive } from "@openuidev/react-lang";
const MySelect = defineComponent({
name: "MySelect",
props: z.object({
name: z.string(),
value: reactive(z.string().optional()), // reactive() marks this prop as accepting $variable binding
items: z.array(SelectItem.ref),
}),
component: ({ props }) => {
const field = useStateField(props.name, props.value);
// field.value: current value (reads from $variable or form state)
// field.setValue(val): updates the $variable and triggers re-evaluation
return (
<select value={field.value ?? ""} onChange={(e) => field.setValue(e.target.value)}>
{/* ... */}
</select>
);
},
});useStateField unifies form state and reactive $variable binding. When the prop is a $variable, setValue updates the store and triggers all dependent queries and expressions to re-evaluate.
See Reactive State for the language-level documentation.
Form state
The Renderer tracks field values automatically. Components use useStateField (preferred) or the lower-level useSetFieldValue and useGetFieldValue to read and write state.
Persistence
Use onStateUpdate to persist field state (e.g. to a message in your thread store) and initialState to hydrate it on load.
<Renderer
library={myLibrary}
response={content}
onStateUpdate={(state) => {
// state is a raw Record<string, any> of all field values
saveToBackend(state);
}}
initialState={loadedState}
/>onStateUpdate fires on every field change. The state format is opaque, so persist and hydrate it as-is.
Field hooks
Use these inside defineComponent renderers:
| Hook | Signature | Description |
|---|---|---|
useStateField | (name: string, value?: unknown) => { value: unknown, setValue: (v: unknown) => void } | Preferred. Unified form state + reactive $variable binding. |
useGetFieldValue | (formName: string | undefined, name: string) => any | Read a field's current value. |
useSetFieldValue | (formName: string | undefined, componentType: string | undefined, name: string, value: any, shouldTriggerSaveCallback?: boolean) => void | Write a field value. |
useFormName | () => string | undefined | Get the enclosing form's name. |
useSetDefaultValue | (options: { formName?, componentType, name, existingValue, defaultValue, shouldTriggerSaveCallback? }) => void | Set a default if no value exists. |
Validation
Form fields can declare validation rules. The Form component provides a validation context via useFormValidation.
interface FormValidationContextValue {
errors: Record<string, string | undefined>;
validateField: (name: string, value: unknown, rules: ParsedRule[]) => boolean;
registerField: (name: string, rules: ParsedRule[], getValue: () => unknown) => void;
unregisterField: (name: string) => void;
validateForm: () => boolean;
clearFieldError: (name: string) => void;
}Built-in validators include required, minLength, maxLength, min, max, pattern, and email. Custom validators can be added via builtInValidators.