Markdown Editor & Preview
A zero-dependency markdown editor and renderer with toolbar configuration, image upload callbacks, split view, and full dark-mode and theme support.
Basic Usage
Editor with Live Preview
Edit and preview markdown with view toggling, keyboard shortcuts, and a complete toolbar.
Markdown Editor Showcase
A zero-dependency editor with full inline and block formatting. Inline marks: bold, italic, *bold italic*, strikethrough, and inline code.
Links and images
Visit FluxoUI on utilsware — or check an autolink: https://utilsware.com.
Lists
Unordered with nesting:
First item
Second item
- Nested one
- Nested two
- Even deeper
Third item
Ordered list:
Wire up the toolbar
Paste an image
Flush uploads on submit
Task list:
Parser supports GFM
Split view with mobile tabs
Syntax highlighting (coming soon)
Blockquote
"Markdown should be easy to write — and even easier to read."
— every documentation author ever
Fenced code
import { MarkdownEditor } from 'fluxo-ui';
const editor = <MarkdownEditor value={md} onChange={setMd} />;Table
| Feature | Inline | Block |
|---|---|---|
| Headings | — | yes |
| Images | yes | yes |
| Tables | — | yes |
| Task lists | — | yes |
Try editing any of the above, or click the image, link, or table buttons in the toolbar.
import { MarkdownEditor } from 'fluxo-ui';
const [value, setValue] = useState('# Hello');
<MarkdownEditor
value={value}
onChange={setValue}
defaultView="split"
minHeight={420}
/>View Modes
Edit / Split / Preview
Fully controlled view mode — wire it to your own UI if you want external toggles.
View Modes
Switch between Edit, Split, and Preview from the top-right toolbar.
Sample content
Inline: bold, italic, strike, inline code, and a link.
Edit-only hides the preview pane
Split shows both side-by-side (tabs on mobile)
Preview hides the editor entirely
Try the Edit button
Try Split
Try Preview
Each view can be controlled externally via the
viewprop.
<MarkdownEditor view={view} onViewChange={setView} />| Mode | Editor | Preview |
|---|---|---|
| edit | yes | no |
| split | yes | yes |
| preview | no | yes |
const [view, setView] = useState<EditorViewMode>('split');
<MarkdownEditor
value={value}
onChange={setValue}
view={view}
onViewChange={setView}
/>Preview Only
Preview Only
Render markdown without the editor — ideal for displaying user-generated content.
Preview-only Renderer
Use MarkdownPreview when you only need to render markdown — comments, blog posts, or read-only docs.
What this renderer supports
Inline: bold, italic, *bold italic*, strike, code, and links.
Lists with nesting
First top-level item
Second with a nested list
- Nested bullet
- Another nested bullet
- Deep ordered
- Deep ordered two
Third top-level item
Task list
Safe URL sanitization
Lazy-loaded images
Custom image resolver
Blockquote
Blockquotes can span multiple lines and contain inline formatting.
They can even contain
codeand links.
Fenced code block
import { MarkdownPreview } from 'fluxo-ui';
<MarkdownPreview value={markdown} openLinksInNewTab />Table with alignment
| Left-aligned | Centered | Right-aligned |
|---|---|---|
| one | two | three |
| short | long | text |
| a | b | c |
Unsafe URLs like javascript:alert(1) are automatically blocked.
import { MarkdownPreview } from 'fluxo-ui';
<MarkdownPreview value={markdown} openLinksInNewTab />Custom Toolbar
Minimal Toolbar
Use the built-in minimal preset for simple comment boxes.
Custom Selection
Pass an explicit list of toolbar actions in your preferred order.
No Toolbar
Disable the toolbar entirely — keyboard shortcuts still work.
import { MarkdownEditor, MINIMAL_MARKDOWN_TOOLBAR } from 'fluxo-ui';
<MarkdownEditor toolbar={MINIMAL_MARKDOWN_TOOLBAR} />
<MarkdownEditor
toolbar={['bold', 'italic', 'divider', 'h2', 'h3', 'divider', 'link', 'quote']}
/>
<MarkdownEditor toolbar={false} />Image Upload — Immediate
Immediate Upload Strategy
Every selected image uploads right away and the final URL is inserted into the markdown.
<MarkdownEditor
value={value}
onChange={setValue}
uploadStrategy="immediate"
uploadImage={async (file) => {
const url = await myUploader(file);
return url;
}}
maxImageSize={5 * 1024 * 1024}
acceptedImageTypes={['image/png', 'image/jpeg', 'image/webp']}
/>Image Upload — Deferred
Deferred Upload Strategy
Images are inserted as blob URLs immediately for instant preview, then uploaded only when you flush.
const editorRef = useRef<MarkdownEditorHandle>(null);
<MarkdownEditor
ref={editorRef}
value={value}
onChange={setValue}
uploadStrategy="deferred"
uploadImage={async (file) => {
return await myUploader(file);
}}
/>
<Button
onClick={async () => {
const finalMarkdown = await editorRef.current?.flushUploads();
submitForm(finalMarkdown);
}}
>
Submit
</Button>Read-only
Read-only Editor
All toolbar actions and keyboard shortcuts are disabled.
Read-only Mode
The editor can be rendered in read-only mode — the textarea remains selectable, but the toolbar is disabled.
What you can see
Inline formatting still renders: bold, italic, strike, inline code, and links.
Useful for displaying the editor with its chrome intact
Prevents any formatting changes
Still allows copy to clipboard
Open this in split view
Try clicking a toolbar button — it's disabled
Text remains selectable so users can copy it
Read-only doesn't mean "hidden". It means "visible but immutable."
<MarkdownEditor value={markdown} readOnly defaultView="split" />| Behavior | Read-only |
|---|---|
| Toolbar enabled | no |
| Text selectable | yes |
| Shortcuts work | no |
| Preview updates | yes |
<MarkdownEditor value={value} readOnly defaultView="split" />Import
import { MarkdownEditor, MarkdownPreview } from 'fluxo-ui';
import type { MarkdownEditorProps, MarkdownEditorHandle, MarkdownPreviewProps } from 'fluxo-ui';MarkdownEditor Props
valuestringControlled markdown value.
valuestringControlled markdown value.
defaultValuestringInitial value when uncontrolled.
defaultValuestringInitial value when uncontrolled.
onChange(value: string) => voidCalled whenever the markdown changes.
onChange(value: string) => voidCalled whenever the markdown changes.
placeholderstring"'Write markdown...'"Placeholder shown when empty.
placeholderstring"'Write markdown...'"Placeholder shown when empty.
readOnlyboolean"false"Disable all editing while keeping the chrome.
readOnlyboolean"false"Disable all editing while keeping the chrome.
disabledboolean"false"Fully disable the editor.
disabledboolean"false"Fully disable the editor.
minHeightstring | number"'320px'"Minimum height of the editor body.
minHeightstring | number"'320px'"Minimum height of the editor body.
maxHeightstring | numberOptional max height (scrolls beyond).
maxHeightstring | numberOptional max height (scrolls beyond).
view'edit' | 'split' | 'preview'Controlled view mode.
view'edit' | 'split' | 'preview'Controlled view mode.
defaultView'edit' | 'split' | 'preview'"'edit'"Initial view when uncontrolled.
defaultView'edit' | 'split' | 'preview'"'edit'"Initial view when uncontrolled.
onViewChange(view: EditorViewMode) => voidCalled when the user toggles views.
onViewChange(view: EditorViewMode) => voidCalled when the user toggles views.
allowedViewsEditorViewMode[]Restrict which view modes appear in the switcher.
allowedViewsEditorViewMode[]Restrict which view modes appear in the switcher.
toolbarMarkdownToolbarItem[] | false"DEFAULT_MARKDOWN_TOOLBAR"Toolbar configuration or false to hide.
toolbarMarkdownToolbarItem[] | false"DEFAULT_MARKDOWN_TOOLBAR"Toolbar configuration or false to hide.
showToolbarboolean"true"Hide the toolbar entirely.
showToolbarboolean"true"Hide the toolbar entirely.
showStatusBarboolean"true"Show word/char count footer.
showStatusBarboolean"true"Show word/char count footer.
showWordCountboolean"true"Toggle word count in the status bar.
showWordCountboolean"true"Toggle word count in the status bar.
uploadImage(file: File) => Promise<string>Async upload callback — must resolve with the final URL.
uploadImage(file: File) => Promise<string>Async upload callback — must resolve with the final URL.
uploadStrategy'immediate' | 'deferred'"'immediate'"Upload immediately on selection or defer to flushUploads().
uploadStrategy'immediate' | 'deferred'"'immediate'"Upload immediately on selection or defer to flushUploads().
maxImageSizenumberMaximum file size in bytes.
maxImageSizenumberMaximum file size in bytes.
acceptedImageTypesstring[]Array of MIME types accepted for upload.
acceptedImageTypesstring[]Array of MIME types accepted for upload.
onUploadError(message: string, file?: File) => voidCalled when validation or upload fails.
onUploadError(message: string, file?: File) => voidCalled when validation or upload fails.
openLinksInNewTabboolean"true"Open preview links with target="_blank".
openLinksInNewTabboolean"true"Open preview links with target="_blank".
spellCheckboolean"true"Enable browser spellcheck on the textarea.
spellCheckboolean"true"Enable browser spellcheck on the textarea.
autoFocusboolean"false"Focus the editor on mount.
autoFocusboolean"false"Focus the editor on mount.
ariaLabelstring"'Markdown editor'"Accessible label for the textarea.
ariaLabelstring"'Markdown editor'"Accessible label for the textarea.
MarkdownPreview Props
valuestringMarkdown source to render.
valuestringMarkdown source to render.
openLinksInNewTabboolean"true"Render links with target="_blank" rel="noopener".
openLinksInNewTabboolean"true"Render links with target="_blank" rel="noopener".
sanitizeUrl(url: string) => string | nullCustom URL sanitizer — return null to block.
sanitizeUrl(url: string) => string | nullCustom URL sanitizer — return null to block.
imageResolver(src: string) => stringRewrite image URLs before rendering.
imageResolver(src: string) => stringRewrite image URLs before rendering.
emptyFallbackReact.ReactNodeShown when value is empty.
emptyFallbackReact.ReactNodeShown when value is empty.
Features
Zero Dependencies
Custom markdown parser and renderer with no third-party libraries.
Image Uploads
Plug in any async upload callback. Immediate or deferred (flush on submit) strategies.
Toggle Views
Edit-only, preview-only, or split — with mobile tabs and keyboard shortcuts.
Accessible
Full keyboard operability, ARIA roles, focus trap in dialogs, high-contrast themes.
Configurable Toolbar
Pick any subset of 20+ toolbar actions, or hide the toolbar entirely.
Theme-aware
All colors flow from --eui-* variables — auto-supports light, dark, and brand themes.