by humanspeak
๐ Markdown and HTML renderer for Svelte 5 โ built for streaming AI agent output from Claude Code, ChatGPT, and agentic workflows. XSS-safe defaults, token caching, TypeScript types.
# Add to your Claude Code skills
git clone https://github.com/humanspeak/svelte-markdownGuides for using ai agents skills like svelte-markdown.
No comments yet. Be the first to share your thoughts!
on* handler stripping)extensions prop (e.g., KaTeX math, alerts)npm i -S @humanspeak/svelte-markdown
Or with your preferred package manager:
pnpm add @humanspeak/svelte-markdown
yarn add @humanspeak/svelte-markdown
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
const source = `
# This is a header
This is a paragraph with **bold** and <em>mixed HTML</em>.
* List item with \`inline code\`
* And a [link](https://svelte.dev)
* With nested items
* Supporting full markdown
`
</script>
<SvelteMarkdown {source} />
Modern AI coding agents โ Claude Code, Codex, agentic workflows โ increasingly emit HTML alongside markdown for richer output (design mockups, dashboards, reports, interactive artifacts). @humanspeak/svelte-markdown is built for this:
javascript: URLs and on* handlers stripped from agent output before render, no opt-in required (see Security)streaming is enabled, each token is sanitized as it's emitted; mid-tag partials buffer until well-formed, so progressive HTML from an LLM renders without flicker<tool-call>, <thinking>, or your own design-system tags to your own components via renderers.html (see Custom HTML Tags)<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { StreamingChunk } from '@humanspeak/svelte-markdown'
let markdown: { writeChunk: (chunk: StreamingChunk) => void } | undefined
async function streamFromAgent(response: Response) {
const reader = response.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
markdown?.writeChunk(decoder.decode(value, { stream: true }))
}
}
</script>
<SvelteMarkdown bind:this={markdown} source="" streaming />
For background on why HTML has become a common agent output format, see Thariq's post: Using Claude Code: The Unreasonable Effectiveness of HTML. For the full streaming API (offset chunks, reset, websocket patterns), see LLM Streaming below.
The package is written in TypeScript and includes full type definitions:
import type {
Renderers,
Token,
TokensList,
SvelteMarkdownOptions,
MarkedExtension
} from '@humanspeak/svelte-markdown'
You can import renderer maps and helper keys to selectively override behavior.
import SvelteMarkdown, {
// Maps
defaultRenderers, // markdown renderer map
Html, // HTML renderer map
// Keys
rendererKeys, // markdown renderer keys (excludes 'html')
htmlRendererKeys, // HTML renderer tag names
// Utility components
Unsupported, // markdown-level unsupported fallback
UnsupportedHTML // HTML-level unsupported fallback
} from '@humanspeak/svelte-markdown'
// Example: override a subset
const customRenderers = {
...defaultRenderers,
link: CustomLink,
html: {
...Html,
span: CustomSpan
}
}
// Optional: iterate keys when building overrides dynamically
for (const key of rendererKeys) {
// if (key === 'paragraph') customRenderers.paragraph = MyParagraph
}
for (const tag of htmlRendererKeys) {
// if (tag === 'div') customRenderers.html.div = MyDiv
}
Notes
rendererKeys intentionally excludes html. Use htmlRendererKeys for HTML tag overrides.Unsupported and UnsupportedHTML are available if you want a pass-through fallback strategy.These helpers make it easy to either allow only a subset or exclude only a subset of renderers without writing huge maps by hand.
buildUnsupportedHTML(): returns a map where every HTML tag uses UnsupportedHTML.allowHtmlOnly(allowed): enable only the provided tags; others use UnsupportedHTML.
'strong' or tuples like ['div', MyDiv] to plug in custom components.excludeHtmlOnly(excluded, overrides?): disable only the listed tags (mapped to UnsupportedHTML), with optional overrides for non-excluded tags using tuples.buildUnsupportedRenderers(): returns a map where all markdown renderers (except html) use Unsupported.allowRenderersOnly(allowed): enable only the provided markdown renderer keys; others use Unsupported.
'paragraph' or tuples like ['paragraph', MyParagraph] to plug in custom components.excludeRenderersOnly(excluded, overrides?): disable only the listed markdown renderer keys, with optional overrides for non-excluded keys using tuples.The HTML helpers return an HtmlRenderers map to be used inside the html key of the overall renderers map. They do not replace the entire renderers object by themselves.
Basic: keep markdown defaults, allow only a few HTML tags (others become UnsupportedHTML):
import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers, // keep markdown defaults
html: allowHtmlOnly(['strong', 'em', 'a']) // restrict HTML
}
Allow a custom component for one tag while allowing others with defaults:
import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
html: allowHtmlOnly([['div', MyDiv], 'a'])
}
Exclude just a few HTML tags; keep all other HTML tags as defaults:
import SvelteMarkdown, { defaultRenderers, excludeHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
html: excludeHtmlOnly(['span', 'iframe'])
}
// Or exclude 'span', but override 'a' to CustomA
const renderersWithOverride = {
...defaultRenderers,
html: excludeHtmlOnly(['span'], [['a', CustomA]])
}
Disable all HTML quickly (markdown defaults unchanged):
import SvelteMarkdown, { defaultRenderers, buildUnsupportedHTML } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
html: buildUnsupportedHTML()
}
Allow only paragraph and link with defaults, disable others:
import { allowRenderersOnly } from '@humanspeak/svelte-markdown'
const md = allowRenderersOnly(['paragraph', 'link'])
Exclude just link; keep others as defaults:
import { excludeRenderersOnly } from '@humanspeak/svelte-markdown'
const md = excludeRenderersOnly(['link'])
Disable all markdown renderers (except html) quickly:
import { buildUnsupportedRenderers } from '@humanspeak/svelte-markdown'
const md = buildUnsupportedRenderers()
You can combine both maps in renderers for SvelteMarkdown.