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):

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

  1. Memoize conversions - Use React.useMemo, Vue computed, or Svelte reactive statements
  2. Debounce updates - For live editors, debounce the conversion by 200-300ms
  3. Virtual scrolling - For very long documents, consider virtual scrolling
  4. Web Workers - For large documents, consider moving conversion to a Web Worker
  5. Lazy loading - Load quikdown dynamically when needed:
// Dynamic import
const quikdown = await import('quikdown');
const html = quikdown.default(markdown);

See Also