Skip to content
WorkspaceJSX previewMenu

JSX preview

Renders JSX written by a model without running it. Streaming tolerant, allowlisted and calm when it cannot render.

The preview renders the finished part of the JSX on every token.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/jsx-preview.json

Usage

tsx
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, no new Function, no dangerouslySetInnerHTML. A small parser turns the string into plain data and React renders it.
  • Only allowlisted HTML and SVG tags render. script, style, iframe, object, embed and similar tags are dropped with their contents.
  • Capitalized tags render only when you pass them in components. Unknown ones render their children, or nothing with unknownComponent="omit".
  • Event handlers, ref, dangerouslySetInnerHTML and unknown attributes are dropped. URLs must be http, https, mailto or tel; images may also use data:image/.
  • Links open in a new tab with noopener noreferrer. Forms never submit.
  • Styles drop url(), expression(), behavior and fixed or sticky positioning, and the root uses contain: 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.
Scripts, handlers, unsafe URLs and styles never reach the DOM.

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.

JSX that cannot parse shows a calm fallback with the source.

Props

PropTypeNotes
jsxstringJSX or a component source
streamingbooleanRender the finished prefix, hold the last good tree on errors
componentsRecord<string, ElementType>The only capitalized tags that render
bindingsRecord<string, unknown>Values for expressions
allowImagesbooleanDefaults to true
unknownComponent"unwrap" | "omit"Defaults to unwrap
onError(error: Error) => voidParse errors on complete input and render errors
renderError(error, source) => ReactNodeReplace 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.