- Accordion
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- ChatContainer
- ChatInput
- ChatMessage
- ChatMultiChoiceQuestion
- ChatMultiOptionQuestion
- ChatThinking
- Checkbox
- Combobox
- Container
- CurrencyInput
- DistributionSlider
- Drawer
- Dropdown
- FilePicker
- Grid
- Heading
- Image
- Input
- InputGroup
- Label
- Logo
- MapPin
- Markdown
- Modal
- NativeSelect
- NumberInput
- OptionSlider
- OtpInput
- PhoneInput
- Popover
- Progress
- PropertyCalendar
- RadioGroup
- RadioGroupCards
- ResponsiveModal
- ScrollArea
- SearchBar
- SearchBarFallback
- SearchInput
- Select
- Separator
- Spinner
- Switch
- Table
- Tabs
- Text
- Textarea
- TimePicker
- Toast
- Toggle
- ToggleCard
- ToggleGroup
- Toolbar
- Tooltip
Markdown
Renders a Markdown string using Wander's typography primitives, with GitHub-flavored extensions and streaming-safe parsing.
pnpm add @wandercom/design-system-web
import { Markdown } from '@wandercom/design-system-web/ui/markdown';
export function Example() {
return (
<Markdown>{`# Hello\n\n**World** with [a link](https://example.com).`}</Markdown>
);
}The component accepts the source either as a children string or via the
content prop. Use content for controlled / streaming usage; if both are
provided, content wins.
A full pass over the supported Markdown features — headings, paragraphs, lists, links, inline and block code, blockquotes, GFM tables, and rules.
Markdown is safe to re-render as content grows token-by-token. The parser
tolerates incomplete trailing tokens (e.g. an open **), so streamed assistant
output renders gracefully. Layout may flicker briefly while a delimiter is
unbalanced — pair the component with one of two hooks from
@wandercom/design-system-shared for stable frames without dropping the final
token.
If you have a string that updates externally, wrap it in useThrottledValue.
If you have a Claude streaming response, pipe it through useClaudeStream;
for OpenAI, use the sibling useOpenAIStream. Both handle the vendor SSE
event shape and throttling for you.
import { useThrottledValue } from '@wandercom/design-system-shared/hooks/use-throttled-value';
const [content, setContent] = useState('');
const throttled = useThrottledValue(content, { intervalMs: 80 });
// …append tokens to `content` as they arrive
<Markdown content={throttled} />;For Anthropic SDK consumers, useClaudeStream accepts the
AsyncIterable<MessageStreamEvent> returned by client.messages.stream(...)
directly — it accumulates text_delta events, throttles re-renders, and
exposes a streaming flag driven by message_stop:
import Anthropic from '@anthropic-ai/sdk';
import { useClaudeStream } from '@wandercom/design-system-shared/hooks/use-claude-stream';
const client = new Anthropic();
function Reply({ messages }: { messages: Anthropic.MessageParam[] }) {
const stream = useMemo(
() =>
client.messages.stream({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages,
}),
[messages]
);
const { content, isStreaming, stop } = useClaudeStream(stream);
return <Markdown content={content} />;
}Pass an onEvent callback to surface non-text events (tool use, thinking,
usage). The hook deliberately ignores input_json_delta and thinking_delta
in content — route those through onEvent and parse them in your own
state.
For OpenAI consumers, useOpenAIStream accepts either streaming shape — the
Responses API (client.responses.stream(...)) or Chat Completions
(client.chat.completions.create({ stream: true })) — and discriminates
internally. Tool-call deltas are surfaced via onEvent; the wire-level
data: [DONE] sentinel of Chat Completions is consumed by the SDK before
iteration sees it, so the hook doesn't need to handle it.
import OpenAI from 'openai';
import { useOpenAIStream } from '@wandercom/design-system-shared/hooks/use-openai-stream';
const client = new OpenAI();
// Responses API
const stream = client.responses.stream({ model: 'gpt-4o', input });
const { content } = useOpenAIStream(stream);
// Chat Completions
const chatStream = client.chat.completions.create({
model: 'gpt-4o',
messages,
stream: true,
});
const { content: chatContent } = useOpenAIStream(chatStream);Pass a components map to override individual element renderers. Keys you
provide replace the design-system defaults; everything else stays mapped to the
DS primitives.
<Markdown
components={{
a: ({ href, children }) => (
<CustomLink href={href}>{children}</CustomLink>
),
}}
>
{source}
</Markdown>| Markdown | Renders as |
|---|---|
# h1 | Heading variant="display-sm" |
## h2 | Heading variant="headline-lg" |
### h3 | Heading variant="headline" |
#### h4 | Heading variant="headline-sm" |
##### h5 | Text variant="body-lg" weight="medium" |
###### h6 | Text variant="body" weight="medium" |
| Paragraph | Text variant="body-long" |
| Link | <a> with primary color, underline, external safe |
| List item | Text variant="body-long" (via asChild) |
Inline code | bg-surface-secondary rounded chip, mono |
| Code block | bg-surface-secondary rounded panel, mono |
| Blockquote | Left border + tertiary italic |
| Table | Bordered table inside an overflow-x scroll wrapper |
--- (rule) | Separator |
Raw HTML in the source is not rendered (no rehype-raw), which is the
right default for chat surfaces where the source may be untrusted assistant
output.
children?:
content?:
components?:
className?:
…rest
- No syntax highlighting on code blocks. The chat surface intentionally keeps
this minimal; the docs site already runs a server-side highlighter for its
own MDX. Wrap
code/previa thecomponentsprop if you need highlighting in a particular consumer. - External links (
http:///https://) automatically gettarget="_blank"andrel="noopener noreferrer". In-app routing should be handled by overriding thearenderer. - Mid-stream tokens may render briefly as plain text until their closing
delimiter arrives — this is
react-markdown's natural behavior.