A modern, feature-rich rich text editor for Django using Quill.js.
- Modern Editor: Built on Quill.js - a powerful, free, open-source WYSIWYG editor
- Full Media Support: Upload and embed images, videos, and file attachments
- Drag & Drop: Drag and drop images and files directly into the editor
- Paste Support: Paste images from clipboard
- Django Admin Integration: Seamless integration with Django admin
- Multiple Storage Backends: Use Django's default storage or custom backends
- XSS Protection: Built-in HTML sanitization
- Customizable: Fully configurable toolbar and editor options
- Responsive: Works great on desktop and mobile devices
- Accessible: Full screen reader support with ARIA labels for all toolbar controls
- Offline Support: Quill.js is bundled locally - no CDN dependency
- MIT Licensed: Free for personal and commercial use
pip install chedito
For HTML sanitization (recommended):
pip install chedito[sanitize] # Uses nh3 (fast, Rust-based)
# or
pip install chedito[bleach] # Uses bleach
# settings.py
INSTALLED_APPS = [
...
'chedito',
]
# urls.py
from django.urls import path, include
urlpatterns = [
...
path('chedito/', include('chedito.urls')),
]
# models.py
from django.db import models
from chedito.fields import RichTextField
class Article(models.Model):
title = models.CharField(max_length=200)
content = RichTextField()
# admin.py
from django.contrib import admin
from chedito.admin import RichTextAdminMixin
from .models import Article
@admin.register(Article)
class ArticleAdmin(RichTextAdminMixin, admin.ModelAdmin):
list_display = ['title']
{% load chedito_tags %}
<!DOCTYPE html>
<html>
<head>
{% chedito_css %}
</head>
<body>
<article>
{% render_rich_text article.content %}
</article>
</body>
</html>
Configure Chedito in your Django settings:
# settings.py
CHEDITO_CONFIG = {
# Upload settings
'upload_path': 'chedito_uploads/',
'storage_backend': 'chedito.storage.default.DefaultStorage',
# Size limits
'max_image_size': 5 * 1024 * 1024, # 5MB
'max_video_size': 50 * 1024 * 1024, # 50MB
'max_file_size': 10 * 1024 * 1024, # 10MB
# Allowed file types
'allowed_image_types': ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'allowed_video_types': ['video/mp4', 'video/webm'],
# Security
'require_authentication': False,
'staff_only_uploads': False,
'sanitize_html': True,
# Editor settings
'quill_theme': 'snow', # 'snow' or 'bubble'
'widget_height': '300px',
# Quill configuration
'quill_config': {
'modules': {
'toolbar': [
[{'header': [1, 2, 3, False]}],
['bold', 'italic', 'underline', 'strike'],
[{'color': []}, {'background': []}],
['blockquote', 'code-block'],
[{'list': 'ordered'}, {'list': 'bullet'}],
['link', 'image', 'video'],
['clean'],
]
},
'placeholder': 'Write something...',
},
}
from chedito.fields import RichTextField
class Article(models.Model):
# Basic usage
content = RichTextField()
# With custom configuration
content = RichTextField(
quill_config={
'modules': {
'toolbar': ['bold', 'italic', 'link']
}
}
)
from django import forms
from chedito.widgets import RichTextWidget
class ArticleForm(forms.Form):
content = forms.CharField(widget=RichTextWidget())
from django import forms
from chedito.forms import RichTextFormField
class ArticleForm(forms.Form):
content = RichTextFormField()
{% load chedito_tags %}
<!-- Include CSS (in <head>) -->
{% chedito_css %}
<!-- Include JS (before </body>) -->
{% chedito_js %}
<!-- Render rich text content -->
{% render_rich_text article.content %}
<!-- As a filter -->
{{ article.content|richtext }}
<!-- Strip HTML tags -->
{{ article.content|strip_tags }}
<!-- Truncate with ellipsis -->
{{ article.content|truncate_richtext:200 }}
{% load chedito_tags %}
{% chedito_editor "content" initial_value %}
Uses Django's configured default storage backend:
CHEDITO_CONFIG = {
'storage_backend': 'chedito.storage.default.DefaultStorage',
}
CHEDITO_CONFIG = {
'storage_backend': 'chedito.storage.local.LocalStorage',
}
Create your own storage backend:
from chedito.storage.base import BaseStorage
class MyCustomStorage(BaseStorage):
def save(self, file, filename, upload_type='file'):
# Save file and return URL
pass
def delete(self, filename):
# Delete file
pass
def url(self, filename):
# Return file URL
pass
def exists(self, filename):
# Check if file exists
pass
Chedito is built with accessibility in mind and provides full screen reader support:
All toolbar controls are properly labeled for screen readers (NVDA, JAWS, VoiceOver):
- Toolbar buttons: Each button has descriptive
aria-label(e.g., "Bold", "Italic", "Insert Image") - Toggle buttons: Include
aria-pressedstate that updates when activated - Dropdown menus: Properly labeled with
aria-haspopup,aria-expanded, andaria-label - Dropdown options: Each option has a descriptive label (e.g., "Heading 1", "Normal text", "Align Center")
- Editor content area: Marked as
role="textbox"witharia-multiline="true" - Toolbar container: Has
role="toolbar"with descriptive label
- Tab through toolbar controls
- Enter/Space to activate buttons
- Arrow or tab keys to navigate dropdown options
| Control | Label |
|---|---|
| Bold | "Bold" |
| Italic | "Italic" |
| Underline | "Underline" |
| Strikethrough | "Strikethrough" |
| Subscript | "Subscript" |
| Superscript | "Superscript" |
| Block Quote | "Block Quote" |
| Code Block | "Code Block" |
| Numbered List | "Numbered List" |
| Bulleted List | "Bulleted List" |
| Decrease Indent | "Decrease Indent" |
| Increase Indent | "Increase Indent" |
| Align buttons | "Align Left", "Align Center", "Align Right", "Justify" |
| Link | "Insert Link" |
| Image | "Insert Image" |
| Video | "Insert Video" |
| Clean | "Remove Formatting" |
| Heading dropdown | "Heading Style dropdown" |
| Color picker | "Text Color dropdown" |
| Background picker | "Background Color dropdown" |
Chedito sanitizes HTML content to prevent XSS attacks. Install a sanitization library:
pip install chedito[sanitize] # Recommended: uses nh3
Configure allowed tags and attributes:
CHEDITO_CONFIG = {
'sanitize_html': True,
'allowed_tags': ['p', 'br', 'strong', 'em', 'a', 'img', ...],
'allowed_attributes': {
'a': ['href', 'title'],
'img': ['src', 'alt'],
...
},
}
CHEDITO_CONFIG = {
'require_authentication': True, # Require logged-in users
'staff_only_uploads': True, # Restrict to staff users
}
from chedito.admin import RichTextAdminMixin
@admin.register(Article)
class ArticleAdmin(RichTextAdminMixin, admin.ModelAdmin):
pass
from chedito.admin import RichTextStackedInline, RichTextTabularInline
class CommentInline(RichTextStackedInline):
model = Comment
extra = 1
@admin.register(Article)
class ArticleAdmin(RichTextAdminMixin, admin.ModelAdmin):
inlines = [CommentInline]
@admin.register(Article)
class ArticleAdmin(RichTextAdminMixin, admin.ModelAdmin):
chedito_config = {
'modules': {
'toolbar': ['bold', 'italic', 'link']
}
}
- Python 3.9+
- Django 4.0+
Full documentation is available in the docs directory:
- Installation
- Quick Start
- Configuration
- Model Fields
- Form Widgets
- Admin Integration
- Template Tags
- File Uploads
- Storage Backends
- Security
- API Reference
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
MIT License - Copyright (c) 2025 Emmanuel Asamoah
See LICENSE for details.
Emmanuel Asamoah
- Email: emmanuelasamoah179@gmail.com
- GitHub: @jasonpython50