A customizable, TypeScript-based React markdown editor with sanitization
npm install @akiwiki/markdown-editorA customizable React markdown editor component library built on react-markdown and bundled with commonly used features for the AkiWiki.com CMS.
- Edit-in-Place - Click to edit, click outside to save with auto-resize
- Lock Toggle - Protect content with read-only mode (controlled or uncontrolled)
- Fully Customizable Styles - Style each markdown element independently
- Custom Component Injection - Embed interactive React components directly in markdown
- XSS Protection - Built-in HTML sanitization using rehype-sanitize
- Real-time Preview - Live markdown rendering with debounced updates
- Github-Flavoured Markdown (GFM) Support - Tables, strikethrough, task lists, autolinks
- LaTeX Math - KaTeX support via remark-math and rehype-katex
- Syntax Highlighting - Code blocks with customizable highlighter
- TypeScript - Full type safety
- Tested - Comprehensive test suite with 56+ passing tests
- Lightweight - Minimal dependencies, optimized bundle size
``bash`
npm install @akiwiki/markdown-editor
Important: To use LaTeX math features, import KaTeX CSS in your app:
`tsx`
// In your main App.tsx or index.tsx
import 'katex/dist/katex.min.css';
š View Live Demo
To run the interactive demo locally:
`bashNavigate to the demo directory
cd examples/demo
The demo will be available at
http://localhost:5173 and showcases all features including:
- Click-to-edit functionality
- Card components with save indicators
- Custom styling examples
- Syntax highlighting options
- LaTeX math supportQuick Start
`tsx
import { MarkdownEditor } from '@akiwiki/markdown-editor';function App() {
const [value, setValue] = useState('# Hello World');
return (
value={value}
onChange={setValue}
/>
);
}
`Live Preview Modes
$3
`tsx
import { MarkdownEditorWithPreview } from '@akiwiki/markdown-editor';function App() {
const [value, setValue] = useState('');
return (
value={value}
onChange={setValue}
defaultMode="split" // 'edit' | 'split' | 'preview'
showModeToggle={true} // Show mode switch buttons
debounceMs={100} // Debounce preview updates
enableMath={true}
enableGfm={true}
/>
);
}
`$3
- Edit Mode: Focus on writing markdown
- Split Mode: Edit and preview side-by-side (default)
- Preview Mode: View rendered output only
$3
Use
debounceMs to delay preview rendering during rapid typing:`tsx
value={value}
onChange={setValue}
debounceMs={300} // Update preview 300ms after typing stops
/>
`Edit-in-Place Mode
$3
`tsx
import { EditInPlaceMarkdown } from '@akiwiki/markdown-editor';function MyNotes() {
const [content, setContent] = useState('');
return (
value={content}
onChange={setContent}
emptyText="Click to start writing..."
showEditIcon={true}
/>
);
}
`$3
`tsx
import { EditInPlaceMarkdownCard } from '@akiwiki/markdown-editor'; title="Meeting Notes"
value={content}
onChange={setContent}
showSaveIndicator={true}
enableMath={true}
/>
`$3
- Click to edit, click outside to save
- Keyboard shortcuts (Escape to cancel)
- Empty state with custom placeholder
- Hover effects and edit indicators
- Optional save indicator with timestamp
$3
- Escape: Cancel and revert changes
$3
Protect content from accidental edits with the lock toggle feature:
`tsx
import { EditInPlaceMarkdown } from '@akiwiki/markdown-editor';function ProtectedContent() {
const [content, setContent] = useState('# Important Content');
const [isLocked, setIsLocked] = useState(false);
return (
value={content}
onChange={setContent}
locked={isLocked}
onLockedChange={setIsLocked}
showLockToggle={true} // Show lock/unlock button
/>
);
}
`Features:
- š Visual lock/unlock button with icons
- Controlled or uncontrolled lock state
- Prevents editing when locked
- Customizable lock button visibility
- Color-coded feedback (gray background when locked)
Props:
-
locked?: boolean - Controlled lock state
- onLockedChange?: (locked: boolean) => void - Lock state change callback
- showLockToggle?: boolean - Show/hide lock button (default: false)Uncontrolled Mode:
`tsx
value={content}
onChange={setContent}
showLockToggle={true} // Component manages lock state internally
/>
`Custom Component Injection
Inject interactive React components directly into your markdown content using PascalCase syntax. This powerful feature allows you to create rich, interactive documentation and content.
$3
`tsx
import { MarkdownRenderer } from '@akiwiki/markdown-editor';// 1. Define your custom components
const Alert = ({ children, type = 'info' }) => {
const colors = {
info: 'bg-blue-50 border-blue-200 text-blue-800',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
error: 'bg-red-50 border-red-200 text-red-800',
success: 'bg-green-50 border-green-200 text-green-800',
};
return (
p-4 border-l-4 rounded ${colors[type]} my-4}>
{children}
);
};const Badge = ({ children, color = 'blue' }) => (
inline-block px-3 py-1 text-sm font-semibold
rounded-full bg-${color}-100 text-${color}-800 mx-1}>
{children}
);
// 2. Create a customComponents object
const customComponents = {
Alert,
Badge,
};
// 3. Pass to MarkdownRenderer
function MyComponent() {
const markdown =
This is an alert with full markdown support inside!
Mix ;
return (
customComponents={customComponents}
/>
);
}
`
Create stateful components that respond to user interactions:
`tsx
import { useState } from 'react';
const Counter = (props) => {
const { initial = '0', step = '1', label } = props;
const initialValue = parseInt(String(initial), 10) || 0;
const stepValue = parseInt(String(step), 10) || 1;
const [count, setCount] = useState(initialValue);
return (
const customComponents = { Counter };
// Use in markdown:
const markdown = ;`
`tsx
import { useState } from 'react';
const Collapsible = ({ children, title, defaultOpen = false }) => {
const initialOpen = typeof defaultOpen === 'string'
? defaultOpen === 'true' || defaultOpen === 'True' || defaultOpen === '1'
: defaultOpen;
const [isOpen, setIsOpen] = useState(initialOpen);
return (
// Use in markdown:
const markdown =
This content is hidden by default. Click to expand!
- Collapsible sections
- Markdown support inside
- Perfect for FAQs and documentation
;`
All props are passed as strings from markdown and need to be parsed in your component:
`tsx
const ProgressBar = (props) => {
// Props come as strings, parse them
const value = parseFloat(String(props.value)) || 0;
const max = parseFloat(String(props.max)) || 100;
const color = props.color || 'blue';
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
return (
className={h-full bg-${color}-600 transition-all}${percentage}%
style={{ width: }}
/>
);
};
// Use in markdown:
const markdown = ;`
Custom components support full markdown syntax inside them:
`tsx
const Card = ({ children, title }) => (
{title && {title}
}
{children}
);
// Markdown with nested formatting:
const markdown =
Cards can contain any markdown content:
- Lists and formatting
- \code blocks\
- Bold and italic text
- Even other components!
;`
Custom components work with any rendering component:
`tsx
// With MarkdownRenderer
customComponents={customComponents}
/>
// With EditInPlaceMarkdown
onChange={setMarkdown}
customComponents={customComponents}
/>
// With MarkdownEditor
onChange={setMarkdown}
customComponents={customComponents}
/>
// With MarkdownEditorWithPreview
onChange={setMarkdown}
customComponents={customComponents}
/>
`
- Component names must be in PascalCase, following TSX naming convention (e.g., )
- Props are always passed as strings and need parsing if you need other types
- Components support full markdown syntax inside them
- Self-closing tags work: or
You can use your own syntax highlighter instead of the default one:
`tsx
import { MarkdownRenderer } from '@akiwiki/markdown-editor';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
syntaxHighlighter={{
component: SyntaxHighlighter,
style: atomDark,
props: {
showLineNumbers: true,
wrapLines: true
}
}}
/>
`
Works with all components that render markdown:
`tsx
onChange={setContent}
syntaxHighlighter={{
component: SyntaxHighlighter,
style: atomDark
}}
/>
onChange={setContent}
syntaxHighlighter={{
component: SyntaxHighlighter,
style: dracula,
props: { showLineNumbers: true }
}}
/>
`
`tsx`
onChange={setMarkdown}
styles={{
h1: 'text-5xl font-black text-purple-600',
p: 'my-4 text-lg',
code: 'bg-purple-100 px-2 py-1 rounded'
}}
/>
`tsx
import { useMarkdown } from '@akiwiki/markdown-editor';
function MyComponent() {
const { markdown, updateMarkdown, resetMarkdown } = useMarkdown('# Initial');
return (
);
}
`
- react-markdown - Core markdown rendering
- remark-gfm - GitHub Flavored Markdown support
- remark-math & rehype-katex - LaTeX math rendering
- rehype-sanitize - HTML sanitization for XSS protection
- react-syntax-highlighter - Code syntax highlighting
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | required | Current markdown content |
| onChange | (value: string) => void | undefined | Callback when content changes |
| placeholder | string | "Enter markdown here..." | Textarea placeholder |
| styles | MarkdownStyles | defaultStyles | Custom styles for elements |
| readOnly | boolean | false | Hide editor, show preview only |
| showPreview | boolean | true | Show/hide preview pane |
| sanitize | boolean | true | Enable HTML sanitization |
| enableGfm | boolean | true | Enable GitHub Flavored Markdown |
All contributions are welcome!
If there are any concerns/feature requests, please submit an issue!
To contribute or modify this library:
`bashClone the repository
git clone
cd markdown-editor
$3
-
src/ - Source code for the library
- components/ - React components (EditInPlaceMarkdown, MarkdownEditor, etc.)
- hooks/ - Custom hooks (useMarkdown, useClickOutside, useDebouncedValue)
- utils/ - Utilities (StyleManager)
- config/ - Default configurations and styles
- types/ - TypeScript type definitions
- __tests__/ - Test suites
- examples/demo/` - Interactive demo applicationAll components are tested with Jest and React Testing Library. Run tests before submitting changes.
MIT @ Aki W.