Components
Markdown
Streaming-safe GFM: unterminated syntax closed while streaming, no raw HTML, images only from hosts you allow.
Example
The limiter belongs at the top of the handler, before streamText opens the stream:
- Read the client's IP from
x-forwarded-for. - Call
ratelimit.limit(ip)and return a 429 when it fails.
| Window | Requests | Burst |
|---|---|---|
| Sliding, 10 s | 10 | smoothed |
| Fixed, 10 s | 10 | up to 20 at the edge |
const { success } = await limit(ip);if (!success) return tooMany();Interactive, and the same on the components page. Try the keyboard below on it.
Installation
npm i signoff-ui aiimport { Markdown } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/markdown.jsonimport { Markdown } from '@/components/signoff-ui/markdown';The styles are once per app; Getting started has the Tailwind v4 and plain CSS options.
Usage
<Markdown streaming={part.state === 'streaming'} citations={sources.length}>
{part.text}
</Markdown>How it works
Streaming-safe GitHub-flavored markdown: unterminated syntax is closed while the text streams, raw HTML is never rendered, and javascript: and data: URLs are stripped. [n] markers link to the message's sources, and a caret follows the text while it streams. It takes the same allowedImageHosts as AgentMessage.
The whole text is parsed again on every delta, which gets slow for very long streamed answers; Performance and limits has the numbers.
API reference
Generated from the types the package ships, so it matches the version you install.
Markdown
Streaming-safe markdown (GFM). While streaming, unterminated emphasis, code
and links are closed before parsing so partial output never flashes raw syntax.
Raw HTML is not rendered, links and images with unsafe protocols (javascript:,
data:) are stripped, and images load only from allowedImageHosts.
Props
childrenrequiredstringstreamingbooleanDefaultfalseRepairs unterminated syntax and shows a caret while text is still arriving.
citationsnumberDefault0Number of available sources;
[n]markers up to this count become citation links.citationPrefixstringDefault'source'Must match the
idPrefixof theSourcesthe citations point to.allowedImageHostsreadonly string[]Where images may load from. Default: nowhere. The browser fetches an image as soon as it renders, so a URL in model output can leak data (
); a blocked image renders as a link with its alt text, and nothing is requested.- A host name of http(s) URLs, matched exactly:
'images.example.com'(any port), or'localhost:3000'(that port only). 'self': relative URLs (/logo.png,./chart.png), which load from your own origin.//host/xis not relative. An absolute URL to your own site needs its host listed.'*': every image.
Compared by value, so an inline array does not re-render the markdown.
- A host name of http(s) URLs, matched exactly:
componentsComponentsElement overrides, merged over the defaults. Keep the object stable: a new one re-renders the markdown.
classNamestringlabelsSignoffLabelsInputWords to use instead of the English defaults: see
SignoffLabelsProvider. Keep the object stable.
Accessibility
- Raw HTML in model output is never rendered, and links and images with
javascript:ordata:URLs are stripped. - Images load only from
allowedImageHosts. Others render as a link with their alt text, read as "Image:", and nothing is requested. [n]markers up tocitationsbecome links named "Source n".- The streaming caret is hidden from screen readers.
Theming
Every color, radius and font is a CSS variable, so the overrides in Theming apply, on :root or scoped to any element. These hooks are read from the component's markup.
- Slots (data-slot)
- signoff-markdown
- Tokens it uses
- --signoff-accent--signoff-accent-fg--signoff-bg--signoff-border--signoff-border-strong--signoff-fg--signoff-fg-muted--signoff-fg-subtle--signoff-font-mono--signoff-font-sans--signoff-ring--signoff-surface-2
/* Only this component, and only inside .settings-panel */
.settings-panel [data-slot='signoff-markdown'] {
--signoff-radius: 4px;
}