Proteus
Proteus is a JSON-based UI specification for building interactive tool interfaces. You define your UI as a JSON document, and the Proteus renderer (@optiaxiom/proteus) turns it into interactive Axiom components.
#
Document structure
#
#
Minimal document
#
A Proteus document is a JSON object with $type: "Document". At minimum it needs a body. Every element has a $type that identifies the component. The children property holds nested content: a string, number, another element, or an array of elements.
Welcome to your dashboard
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
element={{
$type: "Document",
body: [
{
$type: "Text",
children:
"Proteus turns a JSON description into a fully interactive Axiom UI.",
},
],
title: "Welcome to your dashboard",
}}
/>
</Box>
);
}#
Document properties
#
Documents also support title, subtitle, appName, appIcon, and actions (buttons rendered at the bottom).
Opal
Create your test plan
Select how you'd like to define the page or experience.
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
import { useState } from "react";
export function App() {
const [data, setData] = useState<Record<string, unknown>>({});
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={data}
element={{
$type: "Document",
actions: [
{
$type: "Action",
appearance: "primary-opal",
children: "Create Test Plan",
},
],
appName: "Opal",
blocking: true,
body: [
{
$type: "Field",
children: {
$type: "Input",
name: "url",
placeholder: "Add a URL",
},
label: "URL",
},
{
$type: "Field",
children: {
$type: "Textarea",
name: "test_idea",
placeholder:
"e.g., Add quantity badges to product thumbnails to show how many of each item they're buying",
},
label: "Test Idea",
},
],
subtitle: "Select how you'd like to define the page or experience.",
title: "Create your test plan",
}}
onDataChange={setData}
/>
</Box>
);
}#
Components
#
#
Layout and structure
#
Use Group as a flexbox container to arrange child elements. It supports flexDirection, gap, alignItems, and justifyContent. Card, CardHeader, and CardLink provide card-based layouts. Separator adds a visual divider.
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
element={{
$type: "Document",
body: {
$type: "Group",
border: "1",
borderColor: "border.tertiary",
children: [
{
$type: "Group",
children: [
{
$type: "Text",
children: "Add quantity badges to product thumbnails",
fontWeight: "600",
},
{
$type: "Text",
children: "TSK-8526",
color: "fg.tertiary",
fontSize: "sm",
},
],
flexDirection: "column",
gap: "4",
p: "12",
},
{ $type: "Separator", borderColor: "border.tertiary" },
{
$type: "Group",
children: [
{
$type: "Text",
children: "D-Congress 2026 - Digital screen content",
fontWeight: "600",
},
{
$type: "Text",
children: "TSK-9102",
color: "fg.tertiary",
fontSize: "sm",
},
],
flexDirection: "column",
gap: "4",
p: "12",
},
],
flexDirection: "column",
rounded: "md",
},
}}
/>
</Box>
);
}#
Typography
#
Heading renders section headings, Text renders inline or block text, and Link renders a hyperlink with an href prop.
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
element={{
$type: "Document",
body: [
{ $type: "Heading", children: "Key Insight", level: "4" },
{
$type: "Text",
children:
"Initial performance metrics reveal a 12% drop in user retention post-update. Immediate deep-dive into Android Scrum Project's V2.2 onboarding flow is critical to reverse the trend and secure Q3 engagement goals.",
color: "fg.secondary",
fontSize: "sm",
},
{
$type: "Link",
children: "View task TSK-98",
href: "https://example.com/task/tsk-98",
},
],
}}
/>
</Box>
);
}#
Data display
#
DataTable renders structured data with columns, sorting, and pagination. Chart renders bar or line charts. Image and ImageCarousel display images. Avatar, Badge, and Time handle user avatars, status tags, and formatted timestamps.
Salesforce CRM
Q4 2024 Sales Performance
October 1 - December 31, 2025
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
element={{
$type: "Document",
appName: "Salesforce CRM",
body: {
$type: "DataTable",
columns: [
{ accessorKey: "rank", header: "Rank", size: 80 },
{ accessorKey: "representative", header: "Representative" },
{ accessorKey: "deals", header: "Deals Closed", size: 120 },
{ accessorKey: "revenue", header: "Revenue", size: 120 },
],
data: [
{
deals: "47",
rank: "1",
representative: "Sarah Chen",
revenue: "$2.8M",
},
{
deals: "42",
rank: "2",
representative: "Michael Wong",
revenue: "$2.5M",
},
{
deals: "39",
rank: "3",
representative: "Jenn Taylor",
revenue: "$2.3M",
},
{
deals: "35",
rank: "4",
representative: "David Smith",
revenue: "$2.1M",
},
{
deals: "33",
rank: "5",
representative: "Emily Johnson",
revenue: "$2.0M",
},
],
},
subtitle: "October 1 - December 31, 2025",
title: "Q4 2024 Sales Performance",
}}
/>
</Box>
);
}#
Icon
#
Icon renders a named icon from the icons map passed to the renderer. If name is instead an http(s):// or data: URL not present in the map, it falls back to rendering an <img> with that source — so you can drop in a remote or inline SVG without registering it. Registered icons always take precedence.
#
Custom table cells
#
By default a DataTable column renders its value as text. Give a column a cell instead to render a Proteus node for that column. Inside a cell, read the current row with DataTableRow — { $type: "DataTableRow", path: "status" } reads the row’s status field, and omitting path gives the whole row object (the same way MapIndex exposes the current index inside a Map). This lets a table column render a Badge, Icon, link, or any composition of elements, with Show picking what to render per row. A change-history / diff view, for example, is just a DataTable whose “Change” column is a badge whose intent depends on the row:
Flag Configuration Changes
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="lg" w="full">
<ProteusDocumentRenderer
data={{
changes: [
{
field: "Status",
newValue: "Running",
oldValue: "Paused",
type: "modified",
},
{
field: "Description",
newValue: "Updated experiment for Q3 launch",
oldValue: "—",
type: "added",
},
{
field: "Legacy Flag",
newValue: "—",
oldValue: "enabled",
type: "removed",
},
],
}}
element={{
$type: "Document",
body: {
$type: "DataTable",
columns: [
{ accessorKey: "field", header: "Field", size: 150 },
{ accessorKey: "oldValue", header: "Old", size: 110 },
{ accessorKey: "newValue", header: "New", size: 170 },
{
accessorKey: "type",
// `cell` is a regular Proteus node. `DataTableRow` reads the
// current row inside it, and a Show/else chain picks the badge
// intent per row.
cell: {
$type: "Badge",
children: { $type: "DataTableRow", path: "type" },
intent: {
$type: "Show",
children: "success",
else: {
$type: "Show",
children: "danger",
else: "warning",
when: {
"==": [
{ $type: "DataTableRow", path: "type" },
"removed",
],
},
},
when: {
"==": [{ $type: "DataTableRow", path: "type" }, "added"],
},
},
},
header: "Change",
size: 120,
},
],
data: { $type: "Value", path: "/changes" },
},
title: "Flag Configuration Changes",
}}
/>
</Box>
);
}#
Row selection
#
Give a DataTable a rowSelection prop — a JSON pointer such as "/selection" — to add a select-all header checkbox and a per-row checkbox column. As rows are toggled, the selected row objects are written as an array to that pointer, so an action can read them back with Value:
{
"$type": "DataTable",
"rowSelection": "/selection",
"columns": [{ "accessorKey": "name", "header": "Task Name" }],
"data": { "$type": "Value", "path": "/tasks" }
}An action can then read the selection via { "$type": "Value", "path": "/selection" } — for example to send the selected rows to an interaction, or to disable a submit button until at least one row is selected.
#
Form controls
#
Wrap inputs with Field to add a label. Available inputs include Input, Textarea, Select (composed from SelectTrigger and SelectContent), Switch, Checkbox, Range, and Question (a multi-question dynamic form). Switch and Checkbox are boolean controls bound to a data path via name.
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
import { useState } from "react";
export function App() {
const [data, setData] = useState<Record<string, unknown>>({});
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={data}
element={{
$type: "Document",
body: [
{
$type: "Field",
children: {
$type: "Select",
children: [
{ $type: "SelectTrigger", w: "full" },
{ $type: "SelectContent" },
],
name: "target_by",
options: [
{ label: "URL", value: "url" },
{ label: "Saved Pages", value: "page" },
],
},
label: "Target by",
},
{
$type: "Field",
children: {
$type: "Input",
name: "url",
placeholder: "Add a URL",
},
label: "URL",
},
{
$type: "Field",
children: {
$type: "Textarea",
name: "test_idea",
placeholder:
"e.g., Add quantity badges to product thumbnails to show how many of each item they're buying",
},
label: "Test Idea",
},
{
$type: "Field",
children: { $type: "Switch", name: "include_metadata" },
label: "Include Metadata",
},
],
}}
onDataChange={setData}
/>
</Box>
);
}#
Actions
#
Action renders a button with an onClick handler (see Interactions below). Use appearance (primary, danger, subtle, etc.) to style the button.
Content Marketing Platform
Version 2.2 Performance Optimization
Android Scrum Campaign / TSK-98
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
element={{
$type: "Document",
actions: [
{ $type: "Action", children: "Comment" },
{
$type: "Action",
appearance: "primary",
children: "View task",
},
],
appName: "Content Marketing Platform",
body: [
{
$type: "Text",
children:
"Initial performance metrics reveal a 12% drop in user retention post-update.",
},
],
subtitle: "Android Scrum Campaign / TSK-98",
title: "Version 2.2 Performance Optimization",
}}
/>
</Box>
);
}#
Styling props
#
All components accept Axiom styling props for spacing, colors, layout, and typography:
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
element={{
$type: "Document",
body: {
$type: "Group",
border: "1",
borderColor: "border.tertiary",
children: [
{
$type: "Text",
children: "TOTAL REVENUE",
color: "fg.tertiary",
fontSize: "xs",
textTransform: "uppercase",
},
{
$type: "Text",
children: "$204M",
fontSize: "2xl",
fontWeight: "700",
},
{
$type: "Text",
children: "+12% vs last quarter",
color: "fg.success.strong",
fontSize: "sm",
},
],
flexDirection: "column",
gap: "4",
p: "12",
rounded: "xl",
},
}}
/>
</Box>
);
}#
Data binding
#
#
Value
#
Reads a field from the tool response using a JSON Pointer path. Paths starting with / are absolute (from the root of the tool response). Paths without a leading / are relative (resolved from the current context, like inside a Map).
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={{
label: "TOTAL REVENUE",
trend: "+12% vs last quarter",
trendColor: "fg.success.strong",
value: "$204M",
}}
element={{
$type: "Document",
body: {
$type: "Group",
border: "1",
borderColor: "border.tertiary",
children: [
{
$type: "Text",
children: { $type: "Value", path: "label" },
color: "fg.tertiary",
fontSize: "xs",
textTransform: "uppercase",
},
{
$type: "Text",
children: { $type: "Value", path: "value" },
fontSize: "2xl",
fontWeight: "700",
},
{
$type: "Text",
children: { $type: "Value", path: "trend" },
color: { $type: "Value", path: "trendColor" },
fontSize: "sm",
},
],
flex: "1",
flexDirection: "column",
gap: "4",
p: "12",
rounded: "xl",
},
}}
/>
</Box>
);
}#
Map
#
Iterates over an array, rendering children once per item. Inside a Map, relative paths resolve against the current item:
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={{
metrics: [
{
label: "TOTAL REVENUE",
trend: "+12% vs last quarter",
trendColor: "fg.success.strong",
value: "$204M",
},
{
label: "TOTAL CLOSE RATE",
trend: "-3% vs last quarter",
trendColor: "fg.error.strong",
value: "83%",
},
{
label: "TOTAL DEALS CLOSED",
trend: "+18 vs last quarter",
trendColor: "fg.success.strong",
value: "230",
},
],
}}
element={{
$type: "Document",
body: {
$type: "Group",
children: {
$type: "Map",
children: {
$type: "Group",
border: "1",
borderColor: "border.tertiary",
children: [
{
$type: "Text",
children: { $type: "Value", path: "label" },
color: "fg.tertiary",
fontSize: "xs",
textTransform: "uppercase",
},
{
$type: "Text",
children: { $type: "Value", path: "value" },
fontSize: "2xl",
fontWeight: "700",
},
{
$type: "Text",
children: { $type: "Value", path: "trend" },
color: { $type: "Value", path: "trendColor" },
fontSize: "sm",
},
],
flex: "1",
flexDirection: "column",
gap: "4",
p: "12",
rounded: "xl",
},
path: "/metrics",
},
gap: "16",
},
}}
/>
</Box>
);
}#
Other expressions
#
MapIndex gives you the current iteration index inside a Map. Concat takes a children array and joins each resolved value into a single string. Zip takes a sources object — each key becomes a property name, and the values (arrays or expressions resolving to arrays) are zipped row-wise into an array of objects, useful as Chart.data or DataTable.data.
UTM Creation
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box, toaster } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={{
parameters: [
{ name: "Campaign Name", value: "spring-sale-2024" },
{ name: "Landing Page URL", value: "https://example.com/landing" },
{ name: "Channel", value: null },
],
}}
element={{
$type: "Document",
actions: [
{
$type: "Action",
appearance: "primary-opal",
children: "Run agent",
onClick: {
message: {
$type: "Map",
children: {
$type: "Concat",
children: [
{ $type: "Value", path: "name" },
": ",
{
$type: "Show",
children: "[Not specified]",
when: { "!": { $type: "Value", path: "value" } },
},
{
$type: "Show",
children: { $type: "Value", path: "value" },
when: { "!!": { $type: "Value", path: "value" } },
},
],
},
path: "/parameters",
separator: "\n",
},
},
},
],
body: [
{
$type: "Text",
children:
"Click the button to see how Concat formats the parameter values into a single message.",
},
],
title: "UTM Creation",
}}
onMessage={(msg) => {
toaster.create(typeof msg === "string" ? msg : JSON.stringify(msg));
}}
/>
</Box>
);
}#
Putting it together
#
This template renders a list of search results dynamically from a tool response:
Content Marketing Platform
2 results found
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={{
table_data: [
{
cmp_url: "https://example.com/task/id-123",
owner: "John Doe",
reference: "TSK-8526",
status: "Overdue",
title: "Add quantity badges to product thumbnails",
},
{
cmp_url: "https://example.com/task/id-345",
owner: "Jane Doe",
reference: "TSK-9102",
status: "In Progress",
title: "D-Congress 2026 - Digital screen content",
},
],
total_results: 2,
}}
element={{
$type: "Document",
appName: "Content Marketing Platform",
body: {
$type: "Map",
children: {
$type: "Card",
children: {
$type: "CardHeader",
children: {
$type: "CardLink",
children: { $type: "Value", path: "title" },
href: { $type: "Value", path: "cmp_url" },
},
description: { $type: "Value", path: "reference" },
},
},
path: "/table_data",
},
title: [{ $type: "Value", path: "/total_results" }, " results found"],
}}
/>
</Box>
);
}#
Conditional rendering
#
#
Show
#
Use Show to render content only when a condition is met. Add an else to render a fallback when the condition is false — this works for element children and, since Show also resolves as a value, for props too. Chain nested Show/else to express multi-way choices (e.g. mapping a value to one of several outputs, like a Badge intent):
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
import { useState } from "react";
export function App() {
const [data, setData] = useState<Record<string, unknown>>({
target_by: "url",
});
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={data}
element={{
$type: "Document",
body: [
{
$type: "Field",
children: {
$type: "Select",
children: [
{ $type: "SelectTrigger", w: "full" },
{ $type: "SelectContent" },
],
name: "target_by",
options: [
{ label: "URL", value: "url" },
{ label: "Saved Pages", value: "page" },
],
},
label: "Target by",
},
{
// Show renders `children` when the condition holds, otherwise the
// `else` branch — here a whole different field.
$type: "Show",
children: {
$type: "Field",
children: {
$type: "Input",
name: "url",
placeholder: "Add a URL",
},
label: "URL",
},
else: {
$type: "Field",
children: {
$type: "Select",
children: [
{ $type: "SelectTrigger", w: "full" },
{ $type: "SelectContent" },
],
name: "saved_page",
options: [
{ label: "Home page", value: "home" },
{ label: "Marketplace", value: "marketplace" },
{ label: "Product Details", value: "product_details" },
],
},
label: "Saved Page",
},
when: {
"==": [{ $type: "Value", path: "/target_by" }, "url"],
},
},
],
}}
onDataChange={setData}
/>
</Box>
);
}#
Operators
#
Supported operators: ==, !=, <, <=, >, >=, !! (truthy), ! (falsy). Combine conditions with and and or:
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
import { useState } from "react";
export function App() {
const [data, setData] = useState<Record<string, unknown>>({
target_by: "url",
url: "https://example.com",
});
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={data}
element={{
$type: "Document",
body: [
{
$type: "Field",
children: {
$type: "Select",
children: [
{ $type: "SelectTrigger", w: "full" },
{ $type: "SelectContent" },
],
name: "target_by",
options: [
{ label: "URL", value: "url" },
{ label: "Saved Pages", value: "page" },
],
},
label: "Target by",
},
{
$type: "Show",
children: {
$type: "Field",
children: {
$type: "Textarea",
name: "test_idea",
placeholder:
"e.g., Add quantity badges to product thumbnails",
},
label: "Test Idea",
},
when: {
or: [
{
and: [
{
"==": [{ $type: "Value", path: "/target_by" }, "url"],
},
{ "!!": { $type: "Value", path: "/url" } },
],
},
{
and: [
{
"==": [{ $type: "Value", path: "/target_by" }, "page"],
},
{ "!!": { $type: "Value", path: "/saved_page" } },
],
},
],
},
},
],
}}
onDataChange={setData}
/>
</Box>
);
}#
Interactions
#
#
Interaction handler
#
Sends the named interaction back to your server, where it can be handled and responded to. Because the handler owns the document’s data, the response can meaningfully change what the card renders — here approving or requesting changes updates status, and Show/Value swap the actions for a resolved state:
Content Marketing Platform
Version 2.2 Performance Optimization
Android Scrum Campaign / TSK-98
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
import { useState } from "react";
export function App() {
// The interaction handler owns the card's state. Each named interaction maps
// to a data update, and the document re-renders off that data via `Show` and
// `Value` — so clicking a button meaningfully changes what the card shows.
const [data, setData] = useState<Record<string, unknown>>({ status: "" });
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={data}
element={{
$type: "Document",
// While the task is pending we show Approve / Request changes; once
// resolved (via `else`) we swap in a single Reopen action that clears
// the status and returns the card to its pending state.
actions: {
$type: "Show",
children: [
{
$type: "Action",
appearance: "danger",
children: "Request changes",
onClick: { interaction: "request_changes" },
},
{
$type: "Action",
appearance: "primary",
children: "Approve",
onClick: { interaction: "approve" },
},
],
else: {
$type: "Action",
appearance: "subtle",
children: "Reopen",
onClick: { interaction: "reopen" },
},
when: { "!": { $type: "Value", path: "/status" } },
},
appName: "Content Marketing Platform",
body: [
{
$type: "Text",
children:
"Initial performance metrics reveal a 12% drop in user retention post-update.",
},
{
$type: "Show",
children: {
$type: "Alert",
children: "Approved — the task has been moved to Done.",
intent: "success",
},
when: { "==": [{ $type: "Value", path: "/status" }, "approved"] },
},
{
$type: "Show",
children: {
$type: "Alert",
children: "Changes requested — sent back to the assignee.",
intent: "danger",
},
when: {
"==": [{ $type: "Value", path: "/status" }, "changes"],
},
},
],
subtitle: "Android Scrum Campaign / TSK-98",
title: "Version 2.2 Performance Optimization",
}}
onInteraction={(name) => {
if (name === "approve") {
setData({ status: "approved" });
} else if (name === "request_changes") {
setData({ status: "changes" });
} else if (name === "reopen") {
setData({ status: "" });
}
}}
/>
</Box>
);
}#
Message handler
#
Sends a text message back to the LLM:
New Chat
How can I help you today?
Content Marketing Platform
Version 2.2 Performance Optimization
Android Scrum Campaign / TSK-98
"use client";
import { IconArrowUp, IconPlus } from "@optiaxiom/icons";
import {
ProteusDocumentRenderer,
type StructuredMessage,
} from "@optiaxiom/proteus";
import { Box, Button, Flex, Paper, Spinner, Text } from "@optiaxiom/react";
import { useRef, useState } from "react";
type Message = {
content: string;
role: "assistant" | "user";
};
// The message actions in the document send these strings back to the LLM. Each
// preset user message maps to a canned assistant reply so the mock chat can
// respond without a real backend.
const replies: Record<string, string> = {
"I'd like to leave a comment on this task.":
"Sure — what would you like the comment to say? I'll post it to TSK-98 once you confirm.",
"Show me the full details for TSK-98.":
"TSK-98 tracks the 12% retention drop after v2.2. Root cause is a slower cold-start on Android; a fix is in review, targeted for v2.2.1.",
};
const messageToText = (message: string | StructuredMessage) =>
typeof message === "string"
? message
: message.parts.map((part) => part.content).join("");
export function App() {
const [messages, setMessages] = useState<Message[]>([]);
const [pending, setPending] = useState(false);
const threadRef = useRef<HTMLDivElement | null>(null);
const onMessage = (message: string | StructuredMessage) => {
const text = messageToText(message);
setMessages((prev) => [...prev, { content: text, role: "user" }]);
setPending(true);
// A real app would stream the reply from the LLM here — we resolve a preset
// response after a short delay to mimic the round-trip.
setTimeout(() => {
setMessages((prev) => [
...prev,
{
content:
replies[text] ??
"Got it — I'll take a look and get back to you shortly.",
role: "assistant",
},
]);
setPending(false);
threadRef.current?.scrollTo({
behavior: "smooth",
top: threadRef.current.scrollHeight,
});
}, 600);
};
return (
<Box
bg="bg.default"
border="1"
borderColor="border.secondary"
display="flex"
flexDirection="column"
maxW="md"
overflow="hidden"
rounded="lg"
w="full"
>
<Box
bg="bg.secondary"
borderB="1"
borderColor="border.secondary"
display="flex"
flexDirection="column"
p="16"
>
<Text fontWeight="600">New Chat</Text>
<Text color="fg.secondary" fontSize="sm">
How can I help you today?
</Text>
</Box>
<Flex gap="16" maxH="lg" overflow="auto" p="16" ref={threadRef}>
{/* The document is the assistant's opening message — the message actions
it declares push preset user messages into the thread below. */}
<ProteusDocumentRenderer
collapsible={false}
element={{
$type: "Document",
actions: [
{
$type: "Action",
children: "Comment",
onClick: {
message: "I'd like to leave a comment on this task.",
},
},
{
$type: "Action",
appearance: "primary",
children: "View task",
onClick: {
message: "Show me the full details for TSK-98.",
},
},
],
appName: "Content Marketing Platform",
body: [
{
$type: "Text",
children:
"Initial performance metrics reveal a 12% drop in user retention post-update.",
},
],
subtitle: "Android Scrum Campaign / TSK-98",
title: "Version 2.2 Performance Optimization",
}}
onMessage={onMessage}
/>
{messages.map((message, index) =>
message.role === "user" ? (
<Paper
alignSelf="end"
bg="bg.accent.subtle"
key={index}
px="16"
py="8"
w="3/4"
>
<Text fontSize="sm">{message.content}</Text>
</Paper>
) : (
<Text alignSelf="start" fontSize="sm" key={index} w="3/4">
{message.content}
</Text>
),
)}
{pending && (
<Flex alignSelf="start" flexDirection="row" gap="8">
<Spinner size="sm" />
<Text color="fg.tertiary" fontSize="sm">
Thinking…
</Text>
</Flex>
)}
</Flex>
{/* Prompt is disabled — this demo only sends the document's preset
messages, so there's nothing to type. */}
<Flex
borderColor="border.secondary"
borderT="1"
flexDirection="row"
gap="8"
p="12"
>
<Button aria-label="Add attachment" disabled icon={<IconPlus />} />
<Box
alignItems="center"
color="fg.tertiary"
display="flex"
flex="1"
fontSize="sm"
>
Press an action above to send a message
</Box>
<Button
appearance="primary"
aria-label="Send message"
disabled
icon={<IconArrowUp />}
/>
</Flex>
</Box>
);
}#
Download handler
#
Triggers a file download:
Strategic Pitch Presentation
Google Slides
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={{
download_url:
"https://example.com/files/strategic-pitch-presentation.pptx",
}}
element={{
$type: "Document",
actions: [
{
$type: "Action",
appearance: "primary",
children: "Download",
onClick: {
action: "download",
url: { $type: "Value", path: "/download_url" },
},
},
],
body: [
{
$type: "Image",
alt: "Slide preview of Strategic Pitch Presentation",
src: "https://placehold.co/560x315",
},
],
subtitle: "Google Slides",
title: "Strategic Pitch Presentation",
}}
/>
</Box>
);
}#
Data operations
#
Actions can also edit form data directly, without a round-trip to your server. setValue writes value at a JSON-pointer path, replacing any existing value; pushValue appends to the array at path; and removeValue removes the array element at path. Paths resolve like Value (absolute /x, relative to the current context, or '' for the current context itself).
In-place data edits
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
import { useState } from "react";
export function App() {
const [data, setData] = useState<Record<string, unknown>>({
status: "draft",
});
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={data}
element={{
$type: "Document",
actions: [
{
$type: "Action",
appearance: "primary",
children: "Mark as published",
// `setValue` writes `value` at `path`, replacing any existing
// value. `pushValue` / `removeValue` cover appending to and
// removing from arrays.
onClick: {
action: "setValue",
path: "/status",
value: "published",
},
},
],
body: [
{
$type: "Text",
children: [
"Current status: ",
{ $type: "Value", path: "/status" },
],
},
],
title: "In-place data edits",
}}
onDataChange={setData}
/>
</Box>
);
}#
Scripting
#
#
Scripts
#
For logic that shouldn’t require a server round-trip, a document can ship JavaScript in a scripts map. Each module registers named handlers with register(name, fn); trigger one from any event source with { script: "module:handler", params }. Handlers run in a sandboxed Web Worker whose only capability is ctx.emit — they get exactly the authority the document already has (emitting the existing Proteus events), so treat scripts with the same trust as the rest of the document.
Scripted actions
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
import { useState } from "react";
export function App() {
const [data, setData] = useState<Record<string, unknown>>({ tags: [] });
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={data}
element={{
$type: "Document",
actions: [
{
$type: "Action",
appearance: "primary",
children: "Add tag",
// Triggers a named handler from the `scripts` map. The handler
// decides which events to emit — here it appends the input value.
onClick: {
params: { tag: { $type: "Value", path: "/tag" } },
script: "main:addTag",
},
},
],
body: [
{
$type: "Field",
children: {
$type: "Input",
name: "tag",
placeholder: "Enter a tag",
},
label: "Tag",
},
{
$type: "Group",
children: {
$type: "Map",
children: {
$type: "Badge",
children: { $type: "Value", path: "" },
},
path: "/tags",
},
flexDirection: "row",
gap: "8",
},
],
// Scripts run in a sandboxed Web Worker. A handler's only capability
// is `ctx.emit`, so it can affect the document only through the
// existing Proteus events.
scripts: {
main: [
"register('addTag', (ctx, params) => {",
" const tag = params.tag;",
" if (!tag) return;",
" ctx.emit({ action: 'pushValue', path: '/tags', value: tag });",
"});",
].join("\n"),
},
title: "Scripted actions",
}}
onDataChange={setData}
/>
</Box>
);
}#
Reactive watchers
#
watch(path, fn) is the push counterpart to register: it runs edge-triggered whenever the value at path changes (use '/' to watch all data). A watcher can only ctx.emit existing events and does not react to changes its own emit causes, so it can’t loop.
Reactive watcher
"use client";
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
import { useState } from "react";
export function App() {
const [data, setData] = useState<Record<string, unknown>>({
items: [
{ done: false, label: "Accept terms" },
{ done: false, label: "Verify email" },
{ done: false, label: "Add payment method" },
],
});
return (
<Box maxW="md" w="full">
<ProteusDocumentRenderer
data={data}
element={{
$type: "Document",
body: [
{
$type: "Text",
children: "Tick every item to unlock the next step.",
color: "fg.secondary",
},
{
$type: "Group",
children: {
$type: "Map",
children: {
$type: "Switch",
children: { $type: "Value", path: "label" },
name: "done",
},
path: "/items",
},
flexDirection: "column",
gap: "8",
},
{
$type: "Show",
children: {
$type: "Alert",
children: "All items complete — you're good to go!",
intent: "success",
},
when: { "!!": { $type: "Value", path: "/all_done" } },
},
],
// A watcher reacts to data changes (edge-triggered) — the push
// counterpart to `register`. It runs once on the false→true edge and
// its own setValue write does not re-trigger it, so it can't loop.
scripts: {
main: [
"watch('/items', (ctx, current) => {",
" const items = current || [];",
" const done = items.length > 0 && items.every((i) => i.done === true);",
" ctx.emit({ action: 'setValue', path: '/all_done', value: done });",
"});",
].join("\n"),
},
title: "Reactive watcher",
}}
onDataChange={setData}
/>
</Box>
);
}#
Theming
#
#
Overriding tokens
#
Pass themeOverride to re-skin a document without touching its element tree. It takes a partial of the Axiom theme contract — any subset of any category (colors, borderRadius, fontFamily, spacing, …) — and applies those tokens as scoped CSS variables on a wrapper around the document. Only the tokens you list are overridden; everything else inherits the ambient theme, and the overrides never leak to the rest of the page.
Because a token like bg.accent (the primary button’s face) is paired with a dark foreground token, keep overridden surfaces that hold text light enough for the text to remain legible.
System Properties
Display Properties
General · Device Manager · Hardware Profiles
import { ProteusDocumentRenderer } from "@optiaxiom/proteus";
import { Box } from "@optiaxiom/react";
export function App() {
return (
<Box maxW="md" p="24" style={{ background: "#008080" }} w="full">
<ProteusDocumentRenderer
element={{
$type: "Document",
actions: [
{ $type: "Action", appearance: "primary", children: "OK" },
{ $type: "Button", children: "Cancel" },
],
appName: "System Properties",
body: [
{
$type: "Text",
children:
"The same document, re-skinned into a Windows 98 dialog purely through the themeOverride prop — the element tree is unchanged.",
fontSize: "sm",
},
{
$type: "Group",
children: [
{ $type: "Badge", children: "Ready", intent: "success" },
{ $type: "Badge", children: "Error", intent: "danger" },
],
flexDirection: "row",
gap: "8",
},
],
subtitle: "General · Device Manager · Hardware Profiles",
title: "Display Properties",
}}
// A partial of the `theme` contract. Only the tokens listed here are
// overridden — everything else inherits the ambient theme, and the
// overrides are scoped to this document. Note the primary button face
// (`bg.accent`) stays light: its label uses a dark `fg.*` token, so a
// dark face would be unreadable.
themeOverride={{
borderRadius: {
xs: "0",
sm: "0",
md: "0",
lg: "0",
xl: "0",
full: "0",
},
colors: {
"bg.accent": "#7bc5c5",
"bg.accent.hovered": "#5fb0b0",
"bg.accent.subtle": "#000080",
"bg.default": "#c0c0c0",
"bg.page": "#c0c0c0",
"bg.pill.default": "#c0c0c0",
"border.default": "#000000",
"border.secondary": "#808080",
"border.tertiary": "#808080",
"fg.default": "#000000",
"fg.secondary": "#000080",
"fg.tertiary": "#404040",
},
fontFamily: {
heading: "'MS Sans Serif', Tahoma, Geneva, sans-serif",
sans: "'MS Sans Serif', Tahoma, Geneva, sans-serif",
},
}}
/>
</Box>
);
}#
Embedding external UI
#
#
Module federation
#
The Federated element loads a remote React component at runtime via Module Federation and renders it inline in the document. Point entry at the remote’s remoteEntry.js (or mf-manifest.json), and use exposeKey to pick which exposed module to render (defaults to "."). Provide a fallback — any Proteus node — to render if the remote fails to load.
{
"$type": "Document",
"body": [
{
"$type": "Federated",
"entry": "https://cdn.example.com/widget/remoteEntry.js",
"exposeKey": "./Dashboard",
"fallback": [
{
"$type": "Text",
"children": "Couldn't load the dashboard widget.",
"color": "fg.error"
}
]
}
]
}The remote receives the document’s current form data as props, so a federated widget can read the same state the rest of the document binds to. There’s no live demo here because the element needs a real remote served over the network; wire it against your own remoteEntry.js to try it.
#
Bridge
#
The Bridge element embeds sandboxed HTML — such as an MCP UI or OpenAI apps widget — inside an isolated iframe. Give it a resource URI; the renderer resolves it through the useResource hook you pass to ProteusDocumentRenderer, which returns the HTML to render. The iframe auto-resizes to its content, or you can pin a height. The embedded widget communicates back through the standard bridge protocol (calling tools, sending follow-up messages), which surface as the renderer’s onInteraction and onMessage callbacks.
<ProteusDocumentRenderer
element={{
$type: "Document",
body: [{ $type: "Bridge", height: 200, resource: "ui://my-widget" }],
}}
onInteraction={(name, params) => {
/* widget called a tool */
}}
onMessage={(message) => {
/* widget sent a follow-up message */
}}
useResource={(uri) => ({
data: { mimeType: "text/html+skybridge", text: "<!doctype html>…" },
isError: false,
})}
/>#
Proteus Designer
#
#
Visual editor
#
The Proteus Designer is a visual editor for building Proteus documents. You can build a component tree, edit properties in the inspector, see a live preview of how the document renders, and copy the final JSON into your resource handler. You can also provide sample data to test how dynamic values resolve.
#
Element reference
#
#
Document
#
actionsActions available for this document
NodeappearanceVisual treatment of the document shell. 'default' renders the document as a card with background, border, and padding. 'inline' strips the surrounding chrome so the document blends into its host container.
"default" | "inline" | ExpressionappIconURL or data URI for the application icon (e.g., 'https://example.com/icon.png' or 'data:image/svg+xml,...'). Rendered as an <img> element.
stringappNameThe official name of the application
stringblockingIf true, hides chat prompt and forces user interaction with document. User can press ESC or close to abandon.
booleanbody*The main content of the document.
NodecompactIf true, constrains the body to a max height and makes it scrollable when content overflows.
booleandataInitial data for the document. Not used by the renderer directly — intended for the outer component managing state to use as the starting data.
objectmetaAdditional metadata not directly consumed by Proteus. Use this to pass along any extra data.
anyscriptsMap of module name to JavaScript source. Each module runs in a sandboxed Web Worker and registers named handlers via register(name, fn). Trigger a handler from any event source with { script: 'module:handler', params }; handlers receive a single ctx ({ emit, getValue, params }) and can only affect the document by emitting the existing Proteus events through ctx.emit.
objectsubtitleA brief description or tagline that provides additional context about the Proteus document's purpose.
NodetitleA concise heading that encapsulates the essence of the Proteus document's content or intended action.
NodetitleIconURL or data URI for an icon displayed alongside the title in a block-style header.
string#
Layout
#
#
Group
#
alignItemsSet the element's align-items CSS property. Defaults to center when
flexDirection='row', and stretch when flexDirection='column'.
"stretch" | "center" | "end" | "start" | "normal" | ExpressionchildrenNodeflexDirectionSet the element's flex-direction CSS property.
Default: 'row' (CSS standard)
"column" | "column-reverse" | "row" | "row-reverse" | Expression#
Card
#
childrenNode#
CardHeader
#
addonAfterDisplay content inside the header after children.
NodeaddonBeforeDisplay content inside the header before children.
NodechildrenNodedescriptionAdd secondary text after the primary title.
NodelineClampTruncate the text at specific number of lines.
"2" | "1" | "3" | "4" | Expression#
CardLink
#
childrenNodehrefThe link href.
string | ExpressiononClickAction triggered when link is clicked
EventHandler#
Separator
#
Accepts only $type plus the standard styling props.
#
Typography
#
#
Heading
#
childrenNodelevelHeading level (1-4) that controls both the semantic HTML tag and font size.
- level="1": renders <h1> with fontSize="4xl" (default)
- level="2": renders <h2> with fontSize="3xl"
- level="3": renders <h3> with fontSize="2xl"
- level="4": renders <h4> with fontSize="xl"
Use asChild to decouple the semantic level from visual appearance.
"2" | "1" | "3" | "4" | Expression#
Text
#
childrenNodelineClampTruncate the text at specific number of lines.
"2" | "1" | "3" | "4" | ExpressiontruncateWhether to truncate the text and add an ellipsis at the end.
boolean | Expression#
Link
#
childrenNodehrefThe link href.
string | Expression#
Data display
#
#
DataTable
#
columnsArray | ExpressiondataArray | Expression | ZiprowSelectionJSON pointer where the selected rows are stored (e.g. '/selection'). When set, the table renders a select-all header checkbox and a per-row checkbox column, and writes the selected row objects as an array to this pointer so an action can read them via Value.
string#
DataTableRow
#
pathJSON pointer path to a field within the current row (e.g. 'status' or '/status'). When omitted, resolves to the whole row object. Only meaningful inside a DataTable column's cell.
string#
Chart
#
dataChart data records, either inline, a ProteusExpression, or a ProteusZip transformation
Array | Expression | ZiplayoutChart layout direction
"horizontal" | "vertical"seriesArray | ExpressiontypeChart type
"bar" | "line"xAxisKeyKey in data records for x-axis labels
string#
Image
#
altAlternative text for the image
Expression | stringsrcThe image source URL
Expression | string#
ImageCarousel
#
images*Array of image data to display in the carousel
Array | ExpressiontitleAccessible label for the carousel region.
Expression | string#
Avatar
#
childrenNodecolorSchemeControl the avatar fallback background and text colors.
"purple" | "neutral" | ExpressionfallbackThe fallback icon to display when no name or image is given.
"opal" | "team" | "user" | ExpressionnameUse name to generate initials to show inside the avatar.
string | ExpressionsizeControl the size of the avatar.
"xs" | "sm" | "md" | "lg" | "xl" | "3xl" | "2xs" | ExpressionsrcRender the image inside the avatar.
string | Expression#
Badge
#
childrenNodeintentControl the appearance by selecting between the different badge types.
"information" | "success" | "warning" | "danger" | "neutral" | "primary" | Expression#
Time
#
date*The date to display. Can be a Date object or an ISO 8601 string.
Unparseable values render nothing at runtime instead of throwing.
string | ExpressionshowDateWhether to show the date part of the value. Defaults to true.
boolean | ExpressionshowTimeWhether to show the time part of the value. Defaults to false.
boolean | Expression#
Form controls
#
#
Field
#
childrenNodedescriptionProvide description and help text for the field.
NodeinfoDisplay a help icon with additional context for the input.
NodelabelThe label of the field.
NoderequiredDisplay an asterisk for required inputs.
boolean | Expression#
Input
#
addonAfterDisplay content inside the input at the end.
NodeaddonBeforeDisplay content inside the input at the start.
NodeappearanceControl the appearance of the input.
"number" | "default" | ExpressionautoFocusWhether the input should be focused on mount.
boolean | ExpressionnameThe name of the form control element.
string | ExpressionplaceholderThe placeholder text to use when control has no value.
string | ExpressionrequiredWhether selecting this input is required.
boolean | ExpressiontypeThe input type.
"number" | "color" | "button" | "checkbox" | "radio" | "hidden" | "text" | "reset" | "range" | "search" | "time" | "image" | "tel" | "url" | "email" | "date" | "submit" | "month" | "datetime-local" | "week" | "file" | "password" | string | Expression#
Textarea
#
maxRowsLimits the height of the textarea when resize=auto is used.
1 | 2 | 3 | 4 | 5 | ExpressionnameThe name of the form control element.
string | ExpressionplaceholderThe placeholder text to use when control has no value.
string | ExpressionrequiredWhether selecting this input is required.
boolean | ExpressionresizeControl whether resizing mode is manual, automatic, or disabled.
"none" | "auto" | "vertical" | ExpressionrowsThe number of rows to display.
number | Expression#
Select
#
childrenNodenameThe name of the inner select element.
string | Expressionoptions*The select items/options we want to render.
ArrayrequiredWhether the select value is required.
boolean | Expression#
SelectTrigger
#
childrenNode#
SelectContent
#
Accepts only $type plus the standard styling props.
#
Switch
#
childrenNodedescriptionAdd secondary text after the label.
NodenameThe name of the form control element.
string | ExpressionrequiredWhether selecting this input is required.
boolean | Expression#
Checkbox
#
childrenNodedescriptionAdd secondary text after the label.
NodenameThe name of the form control element.
string | ExpressionrequiredWhether selecting this input is required.
boolean | Expression#
Range
#
marksThe marks to display on the range steps.
ArraymaxThe maximum value for the range.
number | ExpressionminThe minimum value for the range.
number | ExpressionstepThe stepping interval for the range.
number | Expression#
Question
#
interactionName of the interaction to fire when the user finishes or cancels. When set, submit and cancel send a structured { questions, answers } payload via the named interaction so a calling tool can read the answers programmatically. When omitted, a human-readable transcript is sent via the message event instead.
string | Expressionquestions*Array of questions data
Expression#
Actions
#
#
Action
#
appearanceControl the appearance by selecting between the different button types.
"default" | "danger" | "primary" | "subtle" | "danger-outline" | "default-opal" | "inverse" | "primary-opal" | ExpressionchildrenNodedisabledWhether the button is disabled.
boolean | ExpressioniconDisplay an icon before or after the button content or omit children to only show the icon.
NodetypeThe default behavior of the button.
"button" | "reset" | "submit" | Expressionaria-labelAccessible label for screen readers. Required when the button renders an icon with no text children.
stringonClickAction triggered when button is clicked
EventHandler#
Button
#
appearanceControl the appearance by selecting between the different button types.
"default" | "danger" | "primary" | "subtle" | "danger-outline" | "default-opal" | "inverse" | "primary-opal" | ExpressionchildrenNodedisabledWhether the button is disabled.
boolean | ExpressioniconDisplay an icon before or after the button content or omit children to only show the icon.
NodetypeThe default behavior of the button.
"button" | "reset" | "submit" | Expressionaria-labelAccessible label for screen readers. Required when the button renders an icon with no text children.
stringonClickAction triggered when button is clicked
EventHandler#
Data binding
#
#
Value
#
formatterOptional formatter to apply to the resolved value. Can be a string shorthand or an object with type and options for Intl formatters.
"DateTime" | "Number" | objectpath*Path to a value in the data. Absolute paths start with '/' and resolve from the root (e.g., '/title', '/options/0/label'). Inside a Map template, paths without a leading '/' are relative to the current item (e.g., 'title' resolves to each item's 'title' field).
string#
Map
#
childrenTemplate object to render for each item in the array. Value paths inside this template are relative to the current item (e.g., path='title' resolves to each item's 'title' field). Use a leading '/' to reference top-level data (e.g., path='/title' resolves to the root data's 'title').
NodeflatWhen true, flattens the result array by one level. Useful when each mapped item resolves to an array and you want a single flat list.
booleanpath*JSON pointer path to the source array in the data (e.g., '/results')
stringseparatorOptional separator to render between items. Can be a string or a ProteusNode for more complex separators.
Node#
MapIndex
#
Accepts only $type plus the standard styling props.
#
Concat
#
childrenArray of values to concatenate into a single string. Each item is resolved and joined together.
Array#
Zip
#
sources*Map of output property names to array sources. Each source should resolve to an array of the same length.
object#
Show
#
childrenContent to show when condition is true
NodeelseContent to render when the condition is false. Omitting it renders nothing. Chain nested Show/else to express multi-way choices (e.g. mapping a value to one of several outputs).
NodewhenSingle condition or array of conditions (AND logic). Each condition is an object with one operator key.
Condition | Array#
Embedding
#
#
Bridge
#
fallbackContent rendered on platforms without iframe support (Teams, Slack, mobile). If omitted, a default 'View in Opal web' message is shown.
NodeheightHeight of the iframe in pixels
numberresource*Resource URI identifying the MCP app to render (e.g., 'ui://sample-widget')
string#
Federated
#
entry*URL to the remote application's remoteEntry.js or mf-manifest.json
stringexposeKeyThe key from the remote's ModuleFederationPlugin exposes config (e.g. './App'). Defaults to '.' (root export)
stringfallbackContent rendered when the federated component fails to load
Node