Portalling UIs
React provides createPortal that lets you render some children into a different part of the DOM.
This combined with Cascaide’s WorkflowRenderer allows us to do some interesting things.
Check out the React createPortal docs for a primer.
Let’s ground this in an example.
Set Up: A standard Chat interface with a ReAct agent running. This is the pattern used to render mini chat windows in the recursive ReAct agent in the starter projects. Check out the starter templates for full code.
The Problem
WorkflowRenderer renders all active UI nodes inside a single div.
This works well for most layouts, but sometimes you need an agent’s UI to appear somewhere completely different in the page. For example, a sidebar, a fixed overlay, a panel that lives outside your main content area, etc.
The Solution: React Portals
React’s createPortal lets a component render its DOM output into any node in the document while remaining a full participant in the React tree. It keeps its Cascaide hooks, its access to workflow state, its ability to spawn nodes and signal completion works exactly as normal. Only the physical DOM placement changes.
Creating the UI Node
export default function Tracker({ nodeId }: { nodeId: string }) {
const { nodeData } = useWorkflow(nodeId);
const tasks: DelegationTask[] = nodeData?.initialContext?.history ?? [];
if (tasks.length === 0) return null;
const byDepth = tasks.reduce<Record<number, DelegationTask[]>>((acc, task) => {
const d = getDepthFromCascadeId(task.subCascadeId);
(acc[d] ??= []).push(task);
return acc;
}, {});
const target = document.getElementById('right-sidebar-slot-1');
if (!target) return null;
return createPortal(
<div className="p-3">
{Object.entries(byDepth)
.sort(([a], [b]) => Number(a) - Number(b))
.map(([depth, depthTasks]) => (
<DepthCard
key={depth}
depth={Number(depth)}
tasks={depthTasks}
/>
))}
</div>,
target
);
}Register this uiNode in your clientWorkflowConfig and add portal root elements to your HTML layout wherever you need them.
import React from 'react';
const SLOTS = [1, 2, 3, 4] as const;
export function RightSidebar() {
return (
<aside
style={{
width: 320,
minWidth: 320,
height: '100%',
flexShrink: 0,
padding: '16px', // Added padding for better spacing
borderLeft: '1px solid #eaeaea', // Optional: adds definition
overflowY: 'auto',
}}
>
<h2
style={{
fontSize: '1.25rem',
fontWeight: '600',
marginBottom: '16px',
color: '#333',
}}
>
Recursion Tracker
</h2>
{SLOTS.map(id => (
<div key={id} id={`right-sidebar-slot-${id}`} />
))}
</aside>
);
}When your agent spawns this uiNode, WorkflowRenderer will render it and the component will be portalled into your target.
With this, your UI nodes are no longer constrained. You can render them wherever you want.