Skip to content

API reference

Python

TipTapWidget(forms.Textarea)

django_tiptap_editor.widgets.tiptap_widget.TipTapWidget

TipTapWidget(config=None, attrs=None, storage=None)

Renders a <textarea> carrying data-tiptap-config. Config resolution (last wins): get_default_config() → the per-instance config= → subclass overrides (get_config(self, attrs)). class Media emits the committed bundle. storage is "html" (default) or "json"; when None it resolves from settings.TIPTAP_STORAGE_FORMAT. See Storage format. A features list in the merged config is written out resolved, with the core and every dependency included and sorted, whichever get_config produced it. See Restricting features.

AdminTipTapWidget(TipTapWidget)

django_tiptap_editor.widgets.admin_tiptap.AdminTipTapWidget — admin-sized variant (taller default via admin_defaults).

TipTapModelAdminMixin

django_tiptap_editor.admin.mixin.TipTapModelAdminMixin — mix in before admin.ModelAdmin. Swaps AdminTipTapWidget onto TextFields, and makes their form field a TipTapFormField so the admin cleans them, unless the admin names its own form_class for TextField in formfield_overrides or the field in Meta.field_classes. tiptap_fields = "__all__" (default) or an explicit list of field names. An explicit list is checked at startup: a name that is not a field on the model, or one naming a field the widget cannot apply to, is a system-check error (django_tiptap_editor.E001–E003) rather than a silent no-op. The warning django_tiptap_editor.W001 is described below.

AdminTipTapWidget (in JSON storage mode for a TipTapJSONField) is only the default. A TipTap widget (a TipTapWidget subclass, or an instance of one) the project gives the field in formfield_overrides is rendered as given, config and all, so this is how to size or restrict the admin's editors:

from django_tiptap_editor.constants import STORAGE_FORMAT_JSON


class ArticleAdmin(TipTapModelAdminMixin, admin.ModelAdmin):
    formfield_overrides = {
        models.TextField: {
            "widget": AdminTipTapWidget(config={"height": "200px", "maxHeight": "600px"}),
        },
        TipTapJSONField: {
            "widget": AdminTipTapWidget(storage=STORAGE_FORMAT_JSON, config={"height": "200px"}),
        },
    }

The entry is looked up the way Django applies it (the first class in the field's MRO that has one). An entry there is class-wide, so a widget in it that is not a TipTap widget, such as a smaller Textarea for every TextField, is taken to be for the fields the mixin leaves alone, and the editor still replaces it on the fields it applies to; the admin's own AdminTextareaWidget default for TextField is one of those. A form's Meta.widgets names a single field, so any widget given there is rendered, TipTap or not, and it wins over a formfield_overrides widget. A TextField keeps TipTapFormField whichever widget renders it, so what the admin saves is sanitised either way.

For a TipTapJSONField, give the widget storage=STORAGE_FORMAT_JSON as above: the mixin uses the widget as given, and one that writes HTML into the field fails validation on every save. A TipTap widget from formfield_overrides that would do so is reported at startup, on the admin class, as the system-check warning django_tiptap_editor.W001. It skips a field the widget cannot reach: one with its own widget in the form's Meta.widgets, one the form declares itself, and one the admin leaves off its form through readonly_fields, exclude, fields or fieldsets. It is a warning rather than an error because get_form, get_fields, get_exclude, get_readonly_fields and get_fieldsets take a request and can change the answer, and no startup check can call them; silence it with SILENCED_SYSTEM_CHECKS where one of those keeps the widget off the field.

TipTapFormField(forms.CharField)

django_tiptap_editor.forms.fields.TipTapFormField — a CharField whose default widget is TipTapWidget. It cleans with sanitize_html against get_html_schema(config), where config is its widget's merged config, so a field restricted by features stores only what those features emit. A widget that is not a TipTapWidget gets the unrestricted allowlist. TipTapJSONFormField does the same for its mirror, and narrows the submitted doc with narrow_doc.

TipTapJSONField(models.JSONField)

django_tiptap_editor.fields.tiptap_json_field.TipTapJSONField — a model field storing a {doc, html} envelope. The Python value is a TipTapValue; the doc is protocol-allowlisted on save, and validated (full_clean(), a ModelForm, the admin) against the node and mark types the HTML mirror can render — see Custom nodes and JSON storage. Optional link_protocols / image_protocols tuples override the allowlists. Its form field is TipTapJSONFormField (a forms.Field whose widget is TipTapWidget(storage="json")). See Storage format.

TipTapValue

django_tiptap_editor.types.tiptap_value.TipTapValue — frozen value with .doc (canonical ProseMirror JSON) and .html (the safe HTML mirror, re-derived from doc on save and sanitised on construction, so it is safe on every instance however it was built). str(value) / {{ value }} render the mirror. TipTapValue.from_stored({...}) builds one from a {doc, html} mapping or a bare doc, and raises ValidationError for anything else — assigning an HTML string does not convert it.

sanitize_html

django_tiptap_editor.utils.sanitize_html.sanitize_html(html, *, schema=None) — reduces HTML to what the configured editor can emit and marks the result safe. Unknown tags are unwrapped (their text survives), unknown attributes dropped, script/style bodies discarded, link/image URLs protocol-allowlisted, inline styles filtered. A tag in the schema's paragraph_blocks is converted rather than unwrapped: its text becomes a paragraph, and a paragraph the sanitiser opens is never nested inside another it opened. Raises ValidationError past MAX_DOCUMENT_DEPTH. TipTapFormField applies it on clean and the tiptap_html filter applies it on display; call it directly to clean a column in a data migration. See Security.

get_html_schema / HtmlSchema

django_tiptap_editor.utils.get_html_schema.get_html_schema(config=None) returns the HtmlSchema the sanitiser enforces. With no config, or a config without features, it is the union of the HTML vocabularies of every built-in extension, plus whatever TIPTAP_EXTRA_EXTENSIONS declares. With features, it is the union of the vocabularies of resolve_features(config), plus only the extra extensions the config's own extensions names, and an undeclared extension warns only when the config names it. Link protocols come from the project default either way. Pass one to sanitize_html(..., schema=...) to sanitise against a different allowlist.

HtmlSchema.paragraph_blocks is the set of built-in block tags (h1-h6, blockquote, pre, li, td, th) a restricted schema does not admit, which sanitize_html turns into paragraphs. It is empty by default, so a schema built by hand, and the unrestricted one, unwrap those tags as before.

narrow_doc

django_tiptap_editor.utils.narrow_doc.narrow_doc(doc, *, config) returns a copy of a sanitised ProseMirror doc holding only what resolve_features(config) and the config's extensions allow. A heading or code block the config lacks becomes a paragraph; any other node it lacks is replaced by its children, with loose inline content wrapped in a paragraph; a mark or attribute it lacks is dropped, and a textStyle mark left with nothing in it goes too. With an unrestricted config it returns doc itself. Raises ValidationError past MAX_DOCUMENT_DEPTH. The table it reads, DOCUMENT_FEATURES in constants, maps each node type, mark type and feature-owned attribute to its feature.

resolve_features

django_tiptap_editor.utils.resolve_features.resolve_features(config) returns the frozenset of built-in extensions a config's features list turns on: the list, plus FEATURE_CORE, plus each entry's dependencies (FEATURE_DEPENDENCIES). It returns None when the config has no features key, or it is None, which is the unrestricted editor. It assumes a config validate_config has accepted. See Restricting features.

render_doc

django_tiptap_editor.utils.render_doc.render_doc(doc, *, link_protocols=…, image_protocols=…) — renders a ProseMirror doc dict to a safe HTML string in pure Python (no Node), covering the package's node/mark set with protocol allowlisting + CSS validation. The {{ value|tiptap_html }} filter wraps it (and uses a TipTapValue's mirror when given one). See Storage format.

BaseImageUploadView / ImageUploadError

django_tiptap_editor.views — subclass BaseImageUploadView and implement save(self, file) -> str to wire storage; see Contracts.

Template tags

{% load tiptap %}
{% tiptap_media %}            {# editor assets for the active asset mode #}
{% tiptap_config %}           {# the default config as a JSON string #}
{{ value|tiptap_html }}       {# render stored HTML / a TipTapValue / a doc, safely #}

All of TipTapWidget, AdminTipTapWidget, TipTapModelAdminMixin, TipTapFormField, TipTapJSONField, TipTapJSONFormField, TipTapValue, BaseImageUploadView, ImageUploadError, HtmlSchema, get_default_config, get_html_schema, render_doc, sanitize_html, and validate_config are re-exported from the package root.

JavaScript — window.DjangoTipTap

DjangoTipTap.init(element, config)   // explicit mount; returns the editor
DjangoTipTap.get(id)                 // editor handle | null
DjangoTipTap.destroy(id)             // unmount + tear down the shell
DjangoTipTap.autoMount(root?)        // idempotent; mounts every config-carrying textarea (incl. manualMount)
DjangoTipTap.registerExtension(name, factory)
DjangoTipTap.registerLocale(code, strings)
DjangoTipTap.renderHTML(doc)         // ProseMirror JSON -> HTML string
DjangoTipTap.htmlToJSON(html)        // HTML -> ProseMirror JSON
DjangoTipTap.htmlToStored(html)      // HTML -> { doc, html } envelope (migration)
DjangoTipTap.ui.registerButton(key, spec)
DjangoTipTap.ui.setTokens(tokens)
DjangoTipTap.ui.setRenderer(region, fn)  // "toolbar" | "statusbar" | "bubbleMenu" | "floatingMenu"
DjangoTipTap.ui.setShellRenderer(fn)     // replace the whole shell (experimental)
DjangoTipTap.tiptap                  // re-exported primitives for authors
DjangoTipTap.version                 // the django-tiptap-editor package version
DjangoTipTap.supportedTipTapVersion  // the validated TipTap version

Auto-mount (Path A)

Textareas rendered by the widget mount automatically. A MutationObserver watches the DOM, so editors mount when inserted and tear down when removed — by htmx, Turbo, Unpoly, Livewire, Alpine, Django admin inlines, or any script — with no framework-specific wiring. The textarea is hidden and kept in sync; destroy is also exposed for manual teardown.

A destructive swap re-mounts cleanly: when a textarea is replaced by a fresh one with the same id (an outerHTML swap), the stale editor is destroyed and the new node mounted — never a duplicate or an orphaned shell. Place {{ form.media }} in the page <head>, not inside a swapped partial (see Quickstart).

Idempotency is keyed to the live editor, not the data-tiptap-bound attribute, so two more swap shapes are handled:

  • htmx history (Back/Forward). With hx-boost / hx-push-url / hx-history, htmx caches a static snapshot of the page — including the rendered shell and the hidden textarea — and restores it on Back (firing htmx:historyRestore, not afterSwap). The restored field re-mounts a live editor and the dead snapshot shell is removed. For pages where you'd rather skip the snapshot entirely, set hx-history="false" on the editor page.
  • Morphing swaps (hx-swap="morph" / idiomorph). A morph reconciles the live textarea's attributes back to the server markup, stripping display:none/data-tiptap-bound; a per-editor observer re-asserts the hidden state in place (no remount). Mark the editor region hx-preserve for pure-attribute morphs. Note: once an editor is live it owns the content — a server-side value change delivered via morph is overwritten on the next edit.

Opting a field out of auto-mount — set manualMount: true (per field, or in TIPTAP_DEFAULT_CONFIG) when renderers/extensions must be registered before the editor builds. The automatic triggers (initial scan + the MutationObserver) then skip it; mount it yourself once you're ready with DjangoTipTap.autoMount() (mounts every field, including manualMount ones) or DjangoTipTap.init(el, config). See Theming → Load order.

Explicit init (Path B)

For full control — and for the onChange callback, which can't travel through the JSON data-tiptap-config attribute:

<textarea id="ed"></textarea>
<script>
  const editor = DjangoTipTap.init(document.getElementById("ed"), {
    height: "400px",
    imageUploadUrl: "/editor/upload/",
    mergeTags: [{ label: "First name", value: "{{ first_name }}" }],
    onChange(html) { /* live preview, autosave, … */ },
  });
</script>

See Extending for registerExtension / registerButton and the load order rules, and Theming for setTokens, setRenderer, and setShellRenderer.