StreamdownTextPrimitive#
StreamdownTextPrimitive is the core rendering component in @assistant-ui/react-streamdown. It wraps the upstream Streamdown renderer with assistant-ui-specific concerns: message-part binding, smooth streaming, incomplete-markdown repair (remend), plugin composition, and an optional security layer.
Source file: packages/react-streamdown/src/primitives/StreamdownText.tsx
Type definitions: packages/react-streamdown/src/types.ts
Public API — StreamdownTextPrimitiveProps#
StreamdownTextPrimitiveProps is defined as a selective re-export of StreamdownProps with several props omitted and replaced by typed wrappers :
Omitted StreamdownProps key | Replaced by |
|---|---|
children | Pulled internally from useMessagePartText() |
components | StreamdownTextComponents (adds SyntaxHighlighter / CodeHeader) |
plugins | PluginConfig (adds false opt-out support) |
caret | CaretStyle ("block" |
controls | ControlsConfig (granular per-feature object) |
linkSafety | LinkSafetyConfig (adds onLinkCheck / renderModal) |
remend | RemendConfig (per-construct toggles) |
mermaid | MermaidOptions |
BlockComponent | StreamdownProps["BlockComponent"] (re-exported as-is) |
parseMarkdownIntoBlocksFn | ((markdown: string) => string[]) |
Props added (no equivalent in StreamdownProps): componentsByLanguage, preprocess, defer, smooth, containerProps, containerClassName, security .
All remaining StreamdownProps pass through via ...streamdownProps spread .
Runtime Behaviour#
Message binding & text pipeline#
The component calls useMessagePartText() to get the current message part, then optionally passes the text through a preprocess function . The text then flows through useSmooth (for typewriter animation) and optionally useDeferredValue when defer=true .
Incomplete-markdown repair (remend)#
When mode="streaming" (default) and no custom parseMarkdownIntoBlocksFn is provided, tailBoundedRemend is called to close dangling markdown syntax before passing text to Streamdown. This means parseIncompleteMarkdown is resolved to false internally once remend runs .
Plugin composition#
mergePlugins(userPlugins, {}) processes the plugins prop . Three-state semantics: false = disable, undefined = fall through to defaults, plugin instance = use explicitly. Mermaid is opt-in only — it is never auto-detected. The shikiTheme defaults to ["github-light", "github-dark"] when a code plugin is active .
Security — rehypePlugin Composition & Spreading Order#
When a security prop is provided, buildSecurityRehypePlugins constructs a fixed plugin pipeline :
rehype-raw— allows raw HTML passthroughrehype-sanitizewith an empty schema — strips all attributes/elements not in the schemarehype-harden— enforces URL allow-lists for links and images, controlled bySecurityConfig
The resulting array is assigned to rehypePlugins and passed to <Streamdown> via optionalProps .
Critical spreading order: The <Streamdown> element receives props in this order :
mode, isAnimating, components
{...optionalProps} ← contains rehypePlugins from security
{...streamdownProps} ← user's remaining StreamdownProps
Because streamdownProps spreads after optionalProps, any rehypePlugins supplied directly by the caller via a pass-through StreamdownProps key would override the security-hardened pipeline. However, rehypePlugins is not in the omitted list of StreamdownTextPrimitiveProps — it remains available as a pass-through prop. Callers who set rehypePlugins directly will silently replace the security-built pipeline. The safe pattern is to use the security prop and leave rehypePlugins unset.
SecurityConfig fields#
| Field | Default | Purpose |
|---|---|---|
allowedLinkPrefixes | ["*"] | URL prefix allow-list for links |
allowedImagePrefixes | ["*"] | URL prefix allow-list for images |
allowedProtocols | ["*"] | Protocol allow-list |
allowDataImages | true | Allow base64 data: images |
defaultOrigin | — | Resolves relative URLs |
blockedLinkClass | — | CSS class applied to blocked links |
blockedImageClass | — | CSS class applied to blocked images |
Container Rendering#
The component renders a <div> wrapper with data-status={status.type} and applies containerProps and containerClassName. The inner <Streamdown> renders inside this wrapper . containerClassName and containerProps.className are merged; neither takes precedence — both are joined .
Key References#
- StreamdownText.tsx — full component implementation
- types.ts —
StreamdownTextPrimitiveProps,SecurityConfig,PluginConfig,RemendConfig,ControlsConfig - PR #3143 — feat(react-streamdown): init — initial package introduction
- PR #3690 — docs: comprehensive Streamdown guide — usage documentation