Fast, customizable markdown renderer for Svelte with built-in caching, TypeScript support, and Svelte 5 runes
npm install @humanspeak/svelte-markdownA powerful, customizable markdown renderer for Svelte with TypeScript support. Built as a successor to the original svelte-markdown package by Pablo Berganza, now maintained and enhanced by Humanspeak, Inc.











- โก Intelligent Token Caching - 50-200x faster re-renders with automatic LRU cache (< 1ms for cached content)
- ๐ผ๏ธ Smart Image Lazy Loading - Automatic lazy loading with fade-in animation and error handling
- ๐ Full markdown syntax support through Marked
- ๐ช Complete TypeScript support with strict typing
- ๐จ Customizable component rendering system
- ๐ Secure HTML parsing via HTMLParser2
- ๐ฏ GitHub-style slug generation for headers
- โฟ WCAG 2.1 accessibility compliance
- ๐งช Comprehensive test coverage (vitest and playwright)
- ๐ Svelte 5 runes compatibility
- ๐ก๏ธ XSS protection and sanitization
- ๐จ Custom Marked extensions support (e.g., GitHub-style alerts)
- ๐ Improved attribute handling and component isolation
- ๐ฆ Enhanced token cleanup and nested content support
- ๐ NEW: Intelligent Token Caching - Built-in caching layer provides 50-200x speedup for repeated content
- Automatic cache hits in <1ms (vs 50-200ms parsing)
- LRU eviction with configurable size (default: 50 documents)
- TTL support for fresh content (default: 5 minutes)
- Zero configuration needed - works automatically
- Handles ~95% of re-renders from cache in typical usage
- ๐ผ๏ธ NEW: Smart Image Lazy Loading - Images automatically lazy load with smooth animations
- 70% bandwidth reduction for image-heavy documents
- IntersectionObserver for early prefetch
- Fade-in animation on load
- Error state handling for broken images
- Opt-out available via custom renderer
- Improved HTML attribute isolation for nested components
- Enhanced token cleanup for better nested content handling
- Added proper attribute inheritance control
- Implemented strict debugging checks in CI/CD pipeline
- Enhanced Playwright E2E test coverage
- Added comprehensive tests for custom extensions
- Improved test reliability with proper component mounting checks
- Added specific test cases for nested component scenarios
- Note: Performance tests use a higher threshold for Firefox due to slower execution in CI environments. See tests/performance.test.ts for details.
- Added automated debugging statement detection
- Improved release workflow with GPG signing
- Enhanced PR validation and automated version bumping
- Added manual workflow triggers for better release control
- Implemented monthly cache cleanup
``bash`
npm i -S @humanspeak/svelte-markdown
Or with your preferred package manager:
`bash`
pnpm add @humanspeak/svelte-markdown
yarn add @humanspeak/svelte-markdown
This package carefully selects its dependencies to provide a robust and maintainable solution:
- marked
- Industry-standard markdown parser
- Battle-tested in production
- Extensive security features
- github-slugger
- GitHub-style heading ID generation
- Unicode support
- Collision handling
- htmlparser2
- High-performance HTML parsing
- Streaming capabilities
- Security-focused design
`svelte
`
The package includes an automatic token caching system that dramatically improves performance for repeated content:
Performance Gains:
- First render: ~150ms (for 100KB markdown)
- Cached re-render: <1ms (50-200x faster!)
- Memory efficient: LRU eviction keeps cache bounded
- Smart invalidation: TTL ensures fresh content
`svelte
`
How it works:
- Automatically caches parsed tokens using fast FNV-1a hashing
- Cache key combines markdown source + parser options
- LRU eviction (default: 50 documents, configurable)
- TTL expiration (default: 5 minutes, configurable)
- Zero configuration required - works automatically!
Advanced cache control:
`typescript
import { tokenCache, TokenCache } from '@humanspeak/svelte-markdown'
// Use global cache (shared across app)
const cached = tokenCache.getTokens(markdown, options)
// Create custom cache instance
const myCache = new TokenCache({
maxSize: 100, // Cache up to 100 documents
ttl: 10 60 1000 // 10 minute TTL
})
// Manual cache management
tokenCache.clearAllTokens() // Clear all
tokenCache.deleteTokens(markdown, options) // Clear specific
`
Best for:
- โ
Static documentation sites
- โ
Real-time markdown editors
- โ
Component re-renders with same content
- โ
Navigation between pages
- โ
User-generated content viewed multiple times
Images are automatically lazy loaded with smooth fade-in animations and error handling:
Benefits:
- 70% bandwidth reduction - Only loads visible images
- Faster page loads - Images don't block initial render
- Better LCP - Improves Largest Contentful Paint score
- Error handling - Broken images shown with visual feedback
How it works:
`markdown`
!Alt text
Features:
- โ
Native browser lazy loading (loading="lazy")
- โ
IntersectionObserver for early prefetch (50px before visible)
- โ
Smooth fade-in animation (0.3s transition)
- โ
Error state styling (grayscale + semi-transparent)
- โ
Responsive images (max-width: 100%)
Disable lazy loading (use old behavior):
If you need eager image loading, create a custom Image renderer:
`svelte
`
Then use it:
`svelte
`
The package is written in TypeScript and includes full type definitions:
`typescript`
import type {
Renderers,
Token,
TokensList,
SvelteMarkdownOptions
} from '@humanspeak/svelte-markdown'
You can import renderer maps and helper keys to selectively override behavior.
`ts
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.
- HTML helpers
- buildUnsupportedHTML(): returns a map where every HTML tag uses UnsupportedHTML.allowHtmlOnly(allowed)
- : enable only the provided tags; others use UnsupportedHTML.'strong'
- Accepts tag names like 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()
- Markdown helpers (non-HTML)
- : returns a map where all markdown renderers (except html) use Unsupported.allowRenderersOnly(allowed)
- : enable only the provided markdown renderer keys; others use Unsupported.'paragraph'
- Accepts keys like 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):
`ts
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:
`ts
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:
`ts
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):
`ts
import SvelteMarkdown, { defaultRenderers, buildUnsupportedHTML } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
html: buildUnsupportedHTML()
}
`
Allow only paragraph and link with defaults, disable others:
`ts
import { allowRenderersOnly } from '@humanspeak/svelte-markdown'
const md = allowRenderersOnly(['paragraph', 'link'])
`
Exclude just link; keep others as defaults:
`ts
import { excludeRenderersOnly } from '@humanspeak/svelte-markdown'
const md = excludeRenderersOnly(['link'])
`
Disable all markdown renderers (except html) quickly:
`ts
import { buildUnsupportedRenderers } from '@humanspeak/svelte-markdown'
const md = buildUnsupportedRenderers()
`
You can combine both maps in renderers for SvelteMarkdown.
`svelte
`
Here's a complete example of a custom renderer with TypeScript support:
`svelte
If you would like to extend other renderers please take a look inside the renderers folder for the default implentation of them. If you would like feature additions please feel free to open an issue!
The package excels at handling complex nested structures and mixed content:
`markdown`
| Type | Content |
| ---------- | --------------------------------------- |
| Nested | bold and _italic_ |
| Mixed List |
|
| Code | inline code |
Seamlessly mix HTML and Markdown:
`markdown
### This is a Markdown heading inside HTML
And here's some bold text too!
Click to expand
- This is a markdown list
- Inside an HTML details element
- Supporting bold and _italic_ text
`
- text - Text within other elementsparagraph
- - Paragraph (
For fine-grained styling: - The | Element | Description | You can customize HTML rendering by providing your own components: const customHtmlRenderers: Partial The component emits a | Prop | Type | Description | The package includes several security features: - XSS protection through HTML sanitization MIT ยฉ Humanspeak, Inc. Made with โค๏ธ by Humanspeak)em
- - Emphasis ()strong
- - Strong/bold ()hr
- - Horizontal rule ()blockquote
- - Block quote ()del
- - Deleted/strike-through ()link
- - Link ()image
- - Image ()table
- - Table ()tablehead
- - Table head ()tablebody
- - Table body ()tablerow
- - Table row ()tablecell
- - Table cell (/)list
- - List (/)listitem
- - List item ()heading
- - Heading (-)codespan
- - Inline code ()code
- - Block of code ()html
- - HTML noderawtext
- - All other text that is going to be included in an object aboveorderedlistitem$3
- Items in ordered listsunorderedlistitem
- - Items in unordered listshtml$3
renderer is special and can be configured separately to handle HTML elements:div
| -------- | -------------------- |
| | Division element |span
| | Inline container |table
| | HTML table structure |thead
| | Table header group |tbody
| | Table body group |tr
| | Table row |td
| | Table data cell |th
| | Table header cell |ul
| | Unordered list |ol
| | Ordered list |li
| | List item |code
| | Code block |em
| | Emphasized text |strong
| | Strong text |a
| | Anchor/link |img
| | Image |`typescript`
import type { HtmlRenderers } from '@humanspeak/svelte-markdown'
div: YourCustomDivComponent,
span: YourCustomSpanComponent
}parsedEvents
event when tokens are calculated:`svelte`string \| Token[]Props
| --------- | ----------------------- | ------------------------------------- |
| source | | Markdown content or pre-parsed tokens |Partial
| renderers | | Custom component overrides |SvelteMarkdownOptions
| options | | Marked parser configuration |boolean` | Toggle inline parsing mode |
| isInline | Security
- Secure HTML parsing with HTMLParser2
- Safe handling of HTML entities
- Protection against malicious markdown injectionLicense
Credits