Skip to main content

Rendering

Every extension entry point must call Lumio.render() to mount the React component tree. This is the only way to display content in an extension surface.

Syntax

import { Lumio } from "@zaflun/lumio-sdk";

Lumio.render(<MyComponent />, { target: "editor" | "layer" | "interactive" });

Targets

TargetSurfaceEntry file
"editor"Dashboard sidebarsrc/editor.tsx
"layer"OBS Browser Sourcesrc/layer.tsx
"interactive"Standalone pagesrc/interactive.tsx

The target value must match the file's surface. Passing the wrong target causes a runtime error.

10-second timeout

Lumio.render() must be called within 10 seconds of page load. If the timeout expires, the extension is terminated and the user sees an error message with a "Reload" option.

// This will cause termination — render is called too late
setTimeout(() => {
Lumio.render(<App />, { target: "editor" });
}, 15000); // 15 seconds — too late!

The correct pattern is to render immediately with a loading state:

// Correct: render immediately, show loading state internally
function App() {
const { data, isLoading } = useQuery("getData");

if (isLoading) {
return (
<CompactView title="My Extension">
<Text content="Loading..." variant="muted" />
</CompactView>
);
}

return <MainUI data={data} />;
}

Lumio.render(<App />, { target: "editor" }); // called immediately

Single call requirement

Lumio.render() must be called exactly once per entry file. Calling it multiple times is an error:

// Wrong: calling render twice
Lumio.render(<App />, { target: "editor" });
Lumio.render(<OtherApp />, { target: "editor" }); // Error!

To conditionally render different content, use React's conditional rendering inside a single component:

// Correct: one render call, conditional inside
function Root() {
const [storage] = useExtensionStorage();
return storage.mode === "advanced" ? <AdvancedUI /> : <SimpleUI />;
}
Lumio.render(<Root />, { target: "editor" });

No direct DOM access

Extensions must use only SDK components. The React reconciler serializes the component tree to a JSON intermediate representation (VisionNode tree) that the Lumio host renders as actual DOM elements in the sandboxed iframe.

// These have no effect inside the extension sandbox:
document.createElement("div"); // no-op
document.getElementById("root"); // returns null
document.body.appendChild(element); // no-op

Use SDK layout and display components instead:

// Use SDK components for all rendering
import { Box, Text, VStack } from "@zaflun/lumio-sdk";

function Layout() {
return (
<VStack>
<Box style={{ padding: 16 }}>
<Text content="Hello World" variant="heading" />
</Box>
</VStack>
);
}

VisionStyle

All components accept a style prop of type VisionStyle — a typed subset of CSS properties that the host sanitizes before rendering.

Allowed properties:

CategoryProperties
Box modelmargin, padding, width, height, minWidth, maxWidth, minHeight, maxHeight
Flexboxdisplay, flexDirection, flexWrap, flex, alignItems, alignSelf, justifyContent, gap, flexShrink, flexGrow
Positionposition: "relative" | "absolute", top, right, bottom, left, zIndex
Visualbackground, backgroundColor, color, opacity, border, borderRadius, boxShadow
TypographyfontSize, fontWeight, lineHeight, letterSpacing, textAlign, textTransform, textDecoration
Transformtransform, transition, animation, objectFit
Misccursor, pointerEvents, overflow, overflowX, overflowY

Blocked properties (rejected by the host sanitizer):

  • position: "fixed" or position: "sticky"
  • CSS expression() values
  • javascript: URLs
  • content with external resources

Provider wrapping

Lumio.render() automatically wraps the component tree with all necessary context providers:

  • Storage context (useExtensionStorage)
  • Config context (useLumioConfig)
  • Theme context (useLumioTheme)
  • Identity context (useLumioIdentity)
  • Event subscription context (useLumioEvent)

You do not need to add any providers manually.