When rendering AI-generated or user-generated content, links can direct users to unexpected or malicious sites. Streamdown’s link safety feature intercepts every link click and shows a confirmation modal before navigating away — similar to how ChatGPT handles external links.
Default behavior
Link safety is enabled by default. The default configuration is:
const defaultLinkSafetyConfig: LinkSafetyConfig = {
enabled: true,
};
When a user clicks any link, a modal appears showing the full URL with options to open it or dismiss.
LinkSafetyConfig
interface LinkSafetyConfig {
enabled: boolean;
onLinkCheck?: (url: string) => Promise<boolean> | boolean;
renderModal?: (props: LinkSafetyModalProps) => React.ReactNode;
}
Enable or disable link interception. Set to false to allow all links to open directly.
onLinkCheck
(url: string) => boolean | Promise<boolean>
Optional callback invoked before showing the modal. Return true to open the link directly without a modal (safelist). Return false to show the confirmation modal. Supports async validation.
renderModal
(props: LinkSafetyModalProps) => React.ReactNode
Optional function to replace the built-in modal with a custom component.
Disabling link safety
import { Streamdown } from "streamdown";
export default function Chat({ content }) {
return (
<Streamdown linkSafety={{ enabled: false }}>
{content}
</Streamdown>
);
}
Safelisting trusted domains
Use onLinkCheck to allow links from trusted domains to open without a modal:
<Streamdown
linkSafety={{
enabled: true,
onLinkCheck: (url) => {
return (
url.startsWith("https://your-app.com") ||
url.startsWith("https://github.com")
);
},
}}
>
{content}
</Streamdown>
The callback receives the full URL string. Return values:
true — open the link immediately, skip the modal
false — show the confirmation modal
Promise<boolean> — async checks are supported for server-side validation
Async safelist check
<Streamdown
linkSafety={{
enabled: true,
onLinkCheck: async (url) => {
const res = await fetch("/api/check-url", {
method: "POST",
body: JSON.stringify({ url }),
});
const { isSafe } = await res.json();
return isSafe;
},
}}
>
{content}
</Streamdown>
Custom modal
Replace the built-in modal with your own component using renderModal. The function receives a LinkSafetyModalProps object:
interface LinkSafetyModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
url: string;
}
| Prop | Type | Description |
|---|
isOpen | boolean | Whether the modal should be visible |
onClose | () => void | Call to dismiss the modal without navigating |
onConfirm | () => void | Call to open the link and close the modal |
url | string | The URL the user clicked |
import { Streamdown, type LinkSafetyModalProps } from "streamdown";
function MyLinkModal({ url, isOpen, onClose, onConfirm }: LinkSafetyModalProps) {
if (!isOpen) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>External link</h2>
<p>You are about to visit:</p>
<code>{url}</code>
<div className="actions">
<button type="button" onClick={onClose}>Cancel</button>
<button type="button" onClick={onConfirm}>Continue</button>
</div>
</div>
</div>
);
}
export default function Chat({ content }) {
return (
<Streamdown
linkSafety={{
enabled: true,
renderModal: (props) => <MyLinkModal {...props} />,
}}
>
{content}
</Streamdown>
);
}
TypeScript types
import type { LinkSafetyConfig, LinkSafetyModalProps } from "streamdown";
const config: LinkSafetyConfig = {
enabled: true,
onLinkCheck: (url) => url.startsWith("https://safe.example.com"),
renderModal: (props: LinkSafetyModalProps) => <MyModal {...props} />,
};
Combining with security hardening
Link safety and content hardening address different attack vectors and work together:
import { Streamdown, defaultRehypePlugins } from "streamdown";
import { harden } from "rehype-harden";
export default function SecureChat({ content }) {
return (
<Streamdown
linkSafety={{
enabled: true,
onLinkCheck: (url) => url.startsWith("https://trusted.example.com"),
}}
rehypePlugins={[
defaultRehypePlugins.raw,
defaultRehypePlugins.sanitize,
[
harden,
{
allowedLinkPrefixes: ["https://trusted.example.com"],
allowedProtocols: ["https", "mailto"],
},
],
]}
>
{content}
</Streamdown>
);
}
This gives two layers of protection:
- rehype-harden rewrites or blocks disallowed URLs at render time — malicious links never reach the DOM
- Link safety modal requires user confirmation before any navigation — even for links that passed the hardening check
See Security for more on content hardening.