Markdown editor
Add optional Visual and Markdown editing to native Django form fields with djust.
On this page
The MarkdownEditor hook adds formatting controls to a native textarea.
Enable Visual mode to edit formatted content with Tiptap while storing Markdown.
FormMixin still binds, validates and submits the field. The native form
integration needs no additional frontend application, API endpoint or application
event handler.
Render safely. Server previews and published content must use a server-side Markdown sanitizer.
Native forms
Use ordinary Django fields and native as_live() or as_live_field() rendering.
1. Configure the field. Enable the editor through the textarea's attributes:
from django import forms
class PostForm(forms.Form):
body = forms.CharField(widget=forms.Textarea(attrs={
"dj-input": "validate_field",
"dj-debounce": "250",
"data-markdown-editor": "visual",
}))
2. Add the controls. Place the stable controls host beside the native rendered fields:
{% include "djust_components/markdown_controls.html" with field="body" mode="visual" %}
The host must be inside the same semantic form as the field. Keep the usual
CSRF token and dj-submit="submit_form". Put body last when rendering an entire
form so its label and controls are adjacent. To choose a different placement,
use native as_live_field() rendering in the template layout.
3. Load the assets. Include these optional files before djust mounts hooks. The usual deferred client or framework-injected client works:
{% load static %}
<link rel="stylesheet" href="{% static 'djust_components/markdown-editor.css' %}">
<script src="{% static 'djust_components/markdown-visual.js' %}"></script>
<script src="{% static 'djust_components/markdown-editor.js' %}"></script>
Omit markdown-visual.js for a lightweight Markdown-only editor. If the visual
asset is unavailable, the original textarea remains usable. The assets are
shipped prebuilt: application users do not need Node, npm or a bundler.
Existing component
The Python component and the template tag use the same editor hook. Choose the version that fits your view.
Python component
MarkdownEditor(
name="body",
value=source,
mode="visual",
event="update_body",
)
Template tag
{% markdown_editor name="body" value=source mode="visual" event="update_body" %}
Preview and toolbar options
| Option | Behavior |
|---|---|
value | Update this in the bound native event to refresh the sanitized server preview. |
preview=False | Omit the server preview. |
toolbar=False | Keep the plain textarea. |
All three renderers — the Python component, Django tag and Rust tag handler — use the same Markdown sanitizer:
djust.markdown.render_markdown(..., provisional=False)
Editing contract
- Source preservation. Switching modes alone leaves the original Markdown bytes untouched. An edit may normalize Markdown spelling or whitespace while retaining supported content.
- Supported content. Visual mode supports emphasis, headings, links, nested lists, quotes, task lists, tables, images and fenced code. The toolbar exposes the common actions; tables and images already in Markdown can be edited in place.
- Unsupported content. Unsupported HTML, footnotes, directives and unsafe/unrecognized URLs stay in Markdown mode with an explanation. Raw source is never silently replaced just to enter Visual mode. This is a conservative guard, not a claim of universal Markdown compatibility; Tiptap's Markdown extension is still beta.
- Undo and redo. Visual mode uses the editor transaction history. Source mode
uses native textarea editing; toolbar insertions preserve native undo where
insertTextis supported, with asetRangeTextfallback. Editing the source then reopening Visual begins a new document load; cross-mode undo is not promised. - Form events. Only the native textarea has a form name. Each visual edit updates it and emits its normal input event (or change for change-only bindings). No shadow hidden form, autosave endpoint, reconnect buffer or duplicate submit handler is added.
- Server updates. While the visual surface has focus its current draft wins over stale field patches, matching native focused-input behavior. Unfocused server resets are reflected in the editor. This is not collaborative document editing.
- Validation and read-only fields. Native required-field validation reveals and focuses the textarea if needed. Read-only/disabled fields keep inspection and mode switching available while editing controls are disabled.
The controls live inside server-declared dj-update="ignore" markup.
Never insert toolbar siblings around a textarea after mount: that changes VDOM
index paths. The editor does not depend on beforeUpdate being called. It
cleans up editor resources and field listeners through destroyed().
An optional space-separated data-actions attribute on the host limits toolbar
actions to those the application publishes. For example, applications whose
Markdown policy does not render tasks can omit task. Server authorization,
image policy and sanitization remain application/framework responsibilities.
Theme and contribution development
Theme the variables exposed by markdown-editor.css rather than copying
rendering code.
| Variable | Controls |
|---|---|
--dj-md-editor-bg | Editor background |
--dj-md-editor-text | Editor text |
--dj-md-editor-border | Borders |
--dj-md-editor-muted | Secondary text |
--dj-md-editor-accent | Accent color |
--dj-md-editor-code-bg | Code background |
--dj-md-editor-radius | Corner radius |
--dj-md-editor-height | Editor height |
--dj-font-mono | Monospace font |
The optional visual bundle is approximately 155 KiB gzip and does not enter the
core djust client. Its pinned MIT dependencies and license notices live with the
standalone build in js/markdown-editor/ and the generated static assets.
make markdown-editor-build
make test-markdown-editor
npx vitest run tests/js/markdown_editor.test.js
pytest python/tests/test_markdown_editor_preview.py
The dedicated Markdown editor CI job tests the real engine and hook, rebuilds assets and rejects a source/bundle mismatch. The main JavaScript suite exercises source controls and the stable DOM contract. djust.org is the first application integration; browser checks there supplement unit and transport tests.