JSX preview
Renders JSX written by a model without running it. Streaming tolerant, allowlisted and calm when it cannot render.
On this page
Installation
Usage
import { JsxPreview } from "@/components/ui/chatcn/jsx-preview"
<JsxPreview
jsx={part.text}
streaming={part.state === "streaming"}
components={{ Button, Badge }}
bindings={{ checks, user }}
/>Pass bare JSX or a whole component: the JSX after return is extracted, and while the component is still arriving nothing renders until its return does. While streaming, every token renders the finished part: open tags close themselves and a half-written tag is left out.
Expressions are values, not code. {user.name} reads from bindings, {flag && <Note />} renders when the binding is truthy, and {items.map((item) => <Row />)} repeats the template for each item in a bound array. Anything else in braces renders as nothing.
Safety model
- Nothing is evaluated: no
eval, nonew Function, nodangerouslySetInnerHTML. A small parser turns the string into plain data and React renders it. - Only allowlisted HTML and SVG tags render.
script,style,iframe,object,embedand similar tags are dropped with their contents. - Capitalized tags render only when you pass them in
components. Unknown ones render their children, or nothing withunknownComponent="omit". - Event handlers,
ref,dangerouslySetInnerHTMLand unknown attributes are dropped. URLs must behttp,https,mailtoortel; images may also usedata:image/. - Links open in a new tab with
noopener noreferrer. Forms never submit. - Styles drop
url(),expression(),behaviorand fixed or sticky positioning, and the root usescontain: layout paint, so nothing paints over your app. - Class names pass through so previews pick up your CSS. Remote images reveal the viewer's IP to their host: pass
allowImages={false}to block them.
Examples
Fallback
When finished JSX cannot parse, or a component throws, the preview shows what went wrong with the source one click away. Mid-stream errors never show: the last good tree stays up until the next token fixes it.
Props
| Prop | Type | Notes |
|---|---|---|
jsx | string | JSX or a component source |
streaming | boolean | Render the finished prefix, hold the last good tree on errors |
components | Record<string, ElementType> | The only capitalized tags that render |
bindings | Record<string, unknown> | Values for expressions |
allowImages | boolean | Defaults to true |
unknownComponent | "unwrap" | "omit" | Defaults to unwrap |
onError | (error: Error) => void | Parse errors on complete input and render errors |
renderError | (error, source) => ReactNode | Replace the fallback |
Also exported: parseJsx(source, { bindings, tolerant }) and extractJsx(source) for servers and tests.
Accessibility
- The preview renders real elements, so headings, lists and buttons keep their semantics.
- The fallback is an alert; Show source is a button with
aria-expanded.
Motion
- None. Elements appear as they parse, with no per-token effects, the same rule as streaming text.