Framework Integration Guide
May 21, 2026 · View on GitHub
This guide shows how to integrate quikdown into popular JavaScript frameworks.
Table of Contents
LLM and agent UIs
For streaming markdown, agent tool calling on QuikdownEditor, and pairing with the quikchat widget, see LLM Integration Guide.
Working vanilla JS demos (no React/Vue required):
- examples/llm-stream-editor/ — stream tokens into the editor
- examples/llm-tool-editor/ — simulated
read_editor/write_editortools - pages/examples/integration-llm-stream.html — parser-only stream
React
Basic Usage
import React, { useState, useMemo } from 'react';
import quikdown from 'quikdown';
function MarkdownEditor() {
const [markdown, setMarkdown] = useState('# Hello React\n\nEdit me!');
const html = useMemo(() => {
return quikdown(markdown, { inline_styles: true });
}, [markdown]);
return (
<div style={{ display: 'flex', gap: '20px' }}>
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
style={{ width: '50%', minHeight: '400px' }}
/>
<div
dangerouslySetInnerHTML={{ __html: html }}
style={{ width: '50%' }}
/>
</div>
);
}
export default MarkdownEditor;
With Bidirectional Support
⚠️ Note: Bidirectional support requires quikdown_bd, not regular quikdown.
import React, { useState, useEffect, useRef } from 'react';
// Use quikdown_bd for bidirectional support, NOT regular quikdown
import quikdown_bd from 'quikdown/bd';
function BidirectionalEditor() {
const [markdown, setMarkdown] = useState('# Bidirectional Editor\n\n**Edit** either side!');
const [isEditingHtml, setIsEditingHtml] = useState(false);
const htmlRef = useRef(null);
useEffect(() => {
if (!isEditingHtml && htmlRef.current) {
htmlRef.current.innerHTML = quikdown_bd(markdown, { bidirectional: true });
}
}, [markdown, isEditingHtml]);
const handleHtmlEdit = () => {
if (htmlRef.current) {
const newMarkdown = quikdown_bd.toMarkdown(htmlRef.current);
setMarkdown(newMarkdown);
setIsEditingHtml(false);
}
};
return (
<div style={{ display: 'flex', gap: '20px' }}>
<textarea
value={markdown}
onChange={(e) => {
setMarkdown(e.target.value);
setIsEditingHtml(false);
}}
style={{ width: '50%', minHeight: '400px' }}
/>
<div
ref={htmlRef}
contentEditable
onFocus={() => setIsEditingHtml(true)}
onBlur={handleHtmlEdit}
style={{ width: '50%', border: '1px solid #ccc', padding: '10px' }}
/>
</div>
);
}
Custom Hook
import { useState, useMemo } from 'react';
import quikdown from 'quikdown';
export function useMarkdown(initialMarkdown = '', options = {}) {
const [markdown, setMarkdown] = useState(initialMarkdown);
const html = useMemo(() => {
return quikdown(markdown, options);
}, [markdown, options]);
return { markdown, setMarkdown, html };
}
// Usage
function MyComponent() {
const { markdown, setMarkdown, html } = useMarkdown('# Hello');
// ...
}
Vue.js
Vue 3 Composition API
<template>
<div class="markdown-editor">
<textarea
v-model="markdown"
class="editor"
/>
<div
v-html="html"
class="preview"
/>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import quikdown from 'quikdown';
const markdown = ref('# Hello Vue 3\n\nEdit this **markdown**!');
const html = computed(() => {
return quikdown(markdown.value, { inline_styles: true });
});
</script>
<style scoped>
.markdown-editor {
display: flex;
gap: 20px;
}
.editor, .preview {
width: 50%;
min-height: 400px;
padding: 10px;
border: 1px solid #ccc;
}
</style>
Vue 2 Options API
<template>
<div class="markdown-editor">
<textarea
v-model="markdown"
class="editor"
/>
<div
v-html="html"
class="preview"
/>
</div>
</template>
<script>
import quikdown from 'quikdown';
export default {
data() {
return {
markdown: '# Hello Vue 2\n\nEdit me!'
};
},
computed: {
html() {
return quikdown(this.markdown, { inline_styles: true });
}
}
};
</script>
Vue Component with Bidirectional Support
<template>
<div class="bidirectional-editor">
<textarea
v-model="markdown"
@input="syncFromMarkdown"
class="editor"
/>
<div
ref="htmlEditor"
contenteditable="true"
@blur="syncFromHtml"
class="preview"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import quikdown_bd from 'quikdown/bd';
const markdown = ref('# Bidirectional\n\n**Edit** either side!');
const htmlEditor = ref(null);
let isUpdating = false;
const syncFromMarkdown = () => {
if (!isUpdating && htmlEditor.value) {
isUpdating = true;
htmlEditor.value.innerHTML = quikdown_bd(markdown.value, { bidirectional: true });
isUpdating = false;
}
};
const syncFromHtml = () => {
if (!isUpdating && htmlEditor.value) {
isUpdating = true;
markdown.value = quikdown_bd.toMarkdown(htmlEditor.value);
isUpdating = false;
}
};
onMounted(() => {
syncFromMarkdown();
});
</script>
Svelte
Basic Svelte Component
<script>
import quikdown from 'quikdown';
let markdown = '# Hello Svelte\n\n**Bold** and *italic* text';
$: html = quikdown(markdown, { inline_styles: true });
</script>
<div class="editor">
<textarea bind:value={markdown} />
<div class="preview">
{@html html}
</div>
</div>
<style>
.editor {
display: flex;
gap: 20px;
}
textarea, .preview {
width: 50%;
min-height: 400px;
padding: 10px;
border: 1px solid #ccc;
}
</style>
Svelte Store Integration
// markdownStore.js
import { writable, derived } from 'svelte/store';
import quikdown from 'quikdown';
export function createMarkdownStore(initialValue = '') {
const markdown = writable(initialValue);
const html = derived(markdown, $markdown =>
quikdown($markdown, { inline_styles: true })
);
return {
markdown,
html,
setMarkdown: markdown.set,
updateMarkdown: markdown.update
};
}
<!-- Component.svelte -->
<script>
import { createMarkdownStore } from './markdownStore.js';
const { markdown, html, setMarkdown } = createMarkdownStore('# Hello Store');
</script>
<textarea value={$markdown} on:input={(e) => setMarkdown(e.target.value)} />
<div>{@html $html}</div>
Bidirectional Svelte Component
<script>
import { onMount } from 'svelte';
import quikdown_bd from 'quikdown/bd';
let markdown = '# Bidirectional\n\n**Edit** anywhere!';
let htmlElement;
let isUpdating = false;
function updateHtml() {
if (!isUpdating && htmlElement) {
isUpdating = true;
htmlElement.innerHTML = quikdown_bd(markdown, { bidirectional: true });
isUpdating = false;
}
}
function updateMarkdown() {
if (!isUpdating && htmlElement) {
isUpdating = true;
markdown = quikdown_bd.toMarkdown(htmlElement);
isUpdating = false;
}
}
onMount(() => {
updateHtml();
});
$: if (markdown) updateHtml();
</script>
<div class="editor">
<textarea bind:value={markdown} />
<div
bind:this={htmlElement}
contenteditable="true"
on:blur={updateMarkdown}
class="preview"
/>
</div>
Angular
Angular Component
// markdown-editor.component.ts
import { Component, OnInit } from '@angular/core';
import quikdown from 'quikdown';
@Component({
selector: 'app-markdown-editor',
template: `
<div class="editor-container">
<textarea
[(ngModel)]="markdown"
(ngModelChange)="updateHtml()"
class="editor"
></textarea>
<div
[innerHTML]="html"
class="preview"
></div>
</div>
`,
styles: [`
.editor-container {
display: flex;
gap: 20px;
}
.editor, .preview {
width: 50%;
min-height: 400px;
padding: 10px;
border: 1px solid #ccc;
}
`]
})
export class MarkdownEditorComponent implements OnInit {
markdown = '# Hello Angular\n\n**Bold** text';
html = '';
ngOnInit() {
this.updateHtml();
}
updateHtml() {
this.html = quikdown(this.markdown, { inline_styles: true });
}
}
Angular Service
// markdown.service.ts
import { Injectable } from '@angular/core';
import quikdown from 'quikdown';
import quikdown_bd from 'quikdown/bd';
@Injectable({
providedIn: 'root'
})
export class MarkdownService {
toHtml(markdown: string, options?: any): string {
return quikdown(markdown, options);
}
toHtmlBidirectional(markdown: string): string {
return quikdown_bd(markdown, { bidirectional: true });
}
toMarkdown(html: string | HTMLElement): string {
return quikdown_bd.toMarkdown(html);
}
}
Angular Pipe
// markdown.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import quikdown from 'quikdown';
@Pipe({
name: 'markdown'
})
export class MarkdownPipe implements PipeTransform {
transform(value: string, options?: any): string {
return value ? quikdown(value, options) : '';
}
}
// Usage in template
// <div [innerHTML]="markdownContent | markdown"></div>
Next.js
Next.js Page with SSR
// pages/markdown-demo.js
import { useState } from 'react';
import quikdown from 'quikdown';
// Server-side rendering
export async function getServerSideProps() {
const initialMarkdown = '# Server-Rendered\n\nThis was rendered on the server!';
const initialHtml = quikdown(initialMarkdown, { inline_styles: true });
return {
props: {
initialMarkdown,
initialHtml
}
};
}
export default function MarkdownDemo({ initialMarkdown, initialHtml }) {
const [markdown, setMarkdown] = useState(initialMarkdown);
const [html, setHtml] = useState(initialHtml);
const handleChange = (e) => {
const newMarkdown = e.target.value;
setMarkdown(newMarkdown);
setHtml(quikdown(newMarkdown, { inline_styles: true }));
};
return (
<div style={{ display: 'flex', gap: '20px', padding: '20px' }}>
<textarea
value={markdown}
onChange={handleChange}
style={{ width: '50%', minHeight: '400px' }}
/>
<div
dangerouslySetInnerHTML={{ __html: html }}
style={{ width: '50%' }}
/>
</div>
);
}
Next.js API Route
// pages/api/markdown.js
import quikdown from 'quikdown';
export default function handler(req, res) {
if (req.method === 'POST') {
const { markdown, options = {} } = req.body;
try {
const html = quikdown(markdown, options);
res.status(200).json({ html });
} catch (error) {
res.status(400).json({ error: error.message });
}
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
Nuxt
Nuxt 3 Component
<!-- components/MarkdownEditor.vue -->
<template>
<div class="markdown-editor">
<textarea
v-model="markdown"
class="editor"
/>
<div
v-html="html"
class="preview"
/>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import quikdown from 'quikdown';
const markdown = ref('# Hello Nuxt 3');
const html = computed(() => quikdown(markdown.value, { inline_styles: true }));
</script>
Nuxt Plugin
// plugins/quikdown.client.js
import quikdown from 'quikdown';
import quikdown_bd from 'quikdown/bd';
export default defineNuxtPlugin(() => {
return {
provide: {
quikdown,
quikdown_bd
}
};
});
// Usage in component
// const { $quikdown } = useNuxtApp();
// const html = $quikdown(markdown);
Common Patterns
Debounced Updates
For better performance with large documents:
import { debounce } from 'lodash-es'; // or your debounce implementation
const debouncedConvert = debounce((markdown, callback) => {
const html = quikdown(markdown);
callback(html);
}, 300);
Syntax Highlighting Integration
import quikdown from 'quikdown';
import hljs from 'highlight.js';
const highlightPlugin = {
render: (code, language) => {
if (language && hljs.getLanguage(language)) {
try {
const result = hljs.highlight(code, { language });
return `<pre><code class="hljs language-${language}">${result.value}</code></pre>`;
} catch (e) {
console.error('Highlighting failed:', e);
}
}
return undefined; // Use default
}
};
const html = quikdown(markdown, {
fence_plugin: highlightPlugin
});
Sanitization
While quikdown has built-in XSS protection, you can add extra sanitization:
import quikdown from 'quikdown';
import DOMPurify from 'dompurify';
function safeRender(markdown) {
const html = quikdown(markdown);
return DOMPurify.sanitize(html);
}
TypeScript Support
All frameworks can benefit from quikdown's TypeScript definitions:
import quikdown, { QuikdownOptions } from 'quikdown';
import quikdown_bd from 'quikdown/bd';
const options: QuikdownOptions = {
inline_styles: true,
fence_plugin: {
render: (code: string, lang: string) => {
// Custom plugin logic
return `<pre class="${lang}">${code}</pre>`;
}
}
};
const html: string = quikdown(markdown, options);
const backToMarkdown: string = quikdown_bd.toMarkdown(html);
Performance Tips
- Memoize conversions - Use React.useMemo, Vue computed, or Svelte reactive statements
- Debounce updates - For live editors, debounce the conversion by 200-300ms
- Virtual scrolling - For very long documents, consider virtual scrolling
- Web Workers - For large documents, consider moving conversion to a Web Worker
- Lazy loading - Load quikdown dynamically when needed:
// Dynamic import
const quikdown = await import('quikdown');
const html = quikdown.default(markdown);