Quill Web Editor
A powerful, full-featured rich text editor package for Flutter Web powered by Quill.js.
๐ฎ Try It Out
Experience the editor live in your browser:
Try all the features including rich text formatting, tables, media embedding, zoom controls, and more!
โจ Features
| Feature | Description |
|---|---|
| ๐ Rich Text Editing | Full formatting toolbar with bold, italic, underline, strikethrough |
| ๐ฎ Controller API | QuillEditorController for programmatic control (like TextEditingController) |
| โก Custom Actions | Register and execute user-defined actions with callbacks |
| ๐ Table Support | Create and edit tables with quill-table-better |
| ๐ผ๏ธ Media Embedding | Images, videos, and iframes with resize controls |
| ๐จ Custom Fonts | Roboto, Open Sans, Lato, Montserrat, Crimson Pro, DM Sans, Source Code Pro |
| ๐ Font Sizes | Small, Normal, Large, Huge |
| ๐ Line Heights | 1.0, 1.5, 2.0, 2.5, 3.0 |
| ๐ Links & Code | Hyperlinks, blockquotes, inline code, code blocks |
| ๐ Smart Paste | Preserves fonts and formatting when pasting |
| ๐พ HTML Export | Clean HTML export with all styles preserved |
| ๐ Preview | Full-screen preview with print support |
| ๐ Zoom Controls | 50% to 300% zoom range |
| ๐ Emoji Support | Built-in emoji picker |
| โ Checklists | Task lists with checkboxes |
| ๐ฏ Alignment | Left, center, right, justify |
๐ฆ Installation
Step 1: Add Dependency
Add to your pubspec.yaml:
dependencies:
quill_web_editor:
git:
url: https://github.com/ff-vivek/flutter_quill_web_editor.git
Step 2: Copy Web Assets
Copy the HTML files from the package to your app's web/ directory:
your_app/
โโโ web/
โโโ index.html
โโโ quill_editor.html โ Copy from package
โโโ quill_viewer.html โ Copy from package
Step 3: Import
import 'package:quill_web_editor/quill_web_editor.dart';
๐ Quick Start
๐ก New to Quill Web Editor? Try the live playground to see it in action before diving into code!
Simple Usage (No Controller)
When you don't need programmatic access, just use callbacks:
QuillEditorWidget(
onContentChanged: (html, delta) {
print('Content: $html');
},
initialHtml: '<p>Start writing...</p>',
)
Using with Controller (Recommended)
For programmatic control, use QuillEditorController:
import 'package:flutter/material.dart';
import 'package:quill_web_editor/quill_web_editor.dart';
class MyEditor extends StatefulWidget {
const MyEditor({super.key});
@override
State<MyEditor> createState() => _MyEditorState();
}
class _MyEditorState extends State<MyEditor> {
final _controller = QuillEditorController();
String _currentHtml = '';
@override
void dispose() {
_controller.dispose(); // You manage the lifecycle
super.dispose();
}
void _insertGreeting() {
_controller.insertText('Hello, World!');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: _insertGreeting,
icon: Icon(Icons.add),
),
IconButton(
onPressed: () => _controller.undo(),
icon: Icon(Icons.undo),
),
],
),
body: QuillEditorWidget(
controller: _controller,
onContentChanged: (html, delta) {
setState(() => _currentHtml = html);
},
initialHtml: '<p>Start writing...</p>',
),
);
}
}
Read-Only Viewer
QuillEditorWidget(
readOnly: true,
initialHtml: '<p>This content is read-only</p>',
)
๐ API Reference
For detailed developer documentation, including architecture, feature deep-dives, and extension guides, see the Developer Guide.
QuillEditorController
A controller for programmatic access to the editor, similar to TextEditingController.
Properties
| Property | Type | Description |
|---|---|---|
isReady |
bool |
Whether editor is ready for commands |
html |
String |
Current HTML content |
currentZoom |
double |
Current zoom level (1.0 = 100%) |
registeredActionNames |
Set<String> |
Names of registered custom actions |
Methods
| Method | Description |
|---|---|
insertText(String text) |
Insert plain text at cursor |
setHTML(String html, {bool replace = true}) |
Set editor content from HTML |
insertHtml(String html) |
Insert HTML at cursor position |
setContents(dynamic delta) |
Set content from Quill Delta |
getContents() |
Request current content |
clear() |
Clear all editor content |
focus() |
Focus the editor |
undo() |
Undo the last operation |
redo() |
Redo the last undone operation |
format(String format, dynamic value) |
Apply formatting to selection |
insertTable(int rows, int cols) |
Insert a table at cursor |
zoomIn() |
Increase zoom by 10% |
zoomOut() |
Decrease zoom by 10% |
resetZoom() |
Reset zoom to 100% |
setZoom(double level) |
Set specific zoom level (0.5 - 3.0) |
Custom Actions
Register and execute user-defined actions:
// Define a reusable action
final timestampAction = QuillEditorAction(
name: 'insertTimestamp',
onExecute: () => print('Inserting timestamp...'),
);
// Register the action
_controller.registerAction(timestampAction);
// Execute with optional parameters
_controller.executeAction('insertTimestamp', parameters: {
'format': 'ISO',
});
// Or execute one-off actions without registration
_controller.executeCustom(
actionName: 'insertSignature',
parameters: {'name': 'John Doe'},
);
QuillEditorWidget
The main editor widget that embeds Quill.js via an iframe.
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
controller |
QuillEditorController? |
null |
Controller for programmatic access |
width |
double? |
null |
Editor width (expands to fill if null) |
height |
double? |
null |
Editor height (expands to fill if null) |
onContentChanged |
Function(String html, dynamic delta)? |
null |
Callback when content changes |
onReady |
VoidCallback? |
null |
Callback when editor is initialized |
readOnly |
bool |
false |
Enable viewer mode |
initialHtml |
String? |
null |
Initial HTML content |
initialDelta |
dynamic |
null |
Initial Quill Delta content |
placeholder |
String? |
null |
Placeholder text |
editorHtmlPath |
String? |
'quill_editor.html' |
Custom editor HTML path |
viewerHtmlPath |
String? |
'quill_viewer.html' |
Custom viewer HTML path |
Note: If no
controlleris provided, an internal controller is created automatically (likeTextField).
| Method | Description |
|---|---|
setHTML(String html, {bool replace = true}) |
Set editor content from HTML |
insertHtml(String html, {bool replace = false}) |
Insert HTML at cursor position |
setContents(dynamic delta) |
Set content from Quill Delta |
insertText(String text) |
Insert plain text at cursor |
getContents() |
Request current content (triggers callback) |
clear() |
Clear all editor content |
focus() |
Focus the editor |
undo() |
Undo the last operation |
redo() |
Redo the last undone operation |
format(String format, dynamic value) |
Apply formatting to selection |
insertTable(int rows, int cols) |
Insert a table at cursor |
zoomIn() |
Increase zoom by 10% |
zoomOut() |
Decrease zoom by 10% |
resetZoom() |
Reset zoom to 100% |
setZoom(double level) |
Set specific zoom level (0.5 - 3.0) |
Properties
| Property | Type | Description |
|---|---|---|
currentZoom |
double |
Current zoom level (1.0 = 100%) |
isReady |
bool |
Whether editor is ready for commands |
๐งฉ Components
SaveStatusIndicator
Displays save status with animated transitions.
SaveStatusIndicator(status: SaveStatus.saved)
SaveStatusIndicator(status: SaveStatus.saving)
SaveStatusIndicator(status: SaveStatus.unsaved)
ZoomControls
Zoom in/out controls with percentage display.
// With controller (reactive)
ListenableBuilder(
listenable: _controller,
builder: (context, _) => ZoomControls(
zoomLevel: _controller.currentZoom,
onZoomIn: () => _controller.zoomIn(),
onZoomOut: () => _controller.zoomOut(),
onReset: () => _controller.resetZoom(),
),
)
OutputPreview
Tabbed preview showing HTML source and plain text.
OutputPreview(html: _currentHtml)
StatCard & StatCardRow
Display document statistics.
// Single stat
StatCard(label: 'Words', value: '150')
// Multiple stats in a row
StatCardRow(
stats: [
(label: 'Words', value: '150', icon: Icons.text_fields),
(label: 'Characters', value: '890', icon: Icons.format_size),
],
)
AppCard
Styled container card with optional title.
AppCard(
title: 'Document Info', // Optional
child: YourContent(),
)
HtmlPreviewDialog
Full-screen HTML preview dialog with copy and print options.
// Show preview dialog
HtmlPreviewDialog.show(context, htmlContent);
InsertHtmlDialog
Dialog for inserting raw HTML into the editor.
final result = await InsertHtmlDialog.show(context);
if (result != null) {
if (result.replaceContent) {
_controller.setHTML(result.html);
} else {
_controller.insertHtml(result.html);
}
}
โก Custom Actions
Custom actions allow you to extend the editor with your own functionality.
Defining Actions
// Create a reusable action
final timestampAction = QuillEditorAction(
name: 'insertTimestamp',
parameters: {'format': 'ISO'},
onExecute: () => print('Inserting timestamp...'),
onResponse: (response) => print('Done: $response'),
);
// Register with controller
_controller.registerAction(timestampAction);
Executing Actions
// Execute a registered action
_controller.executeAction('insertTimestamp');
// Override parameters at execution time
_controller.executeAction('insertTimestamp', parameters: {
'format': 'readable',
});
// Execute one-off action without registration
_controller.executeCustom(
actionName: 'insertSignature',
parameters: {'name': 'John Doe'},
onResponse: (response) => print('Signature inserted'),
);
Use Cases
- Insert dynamic content (timestamps, user info, templates)
- Trigger custom formatting or transformations
- Integrate with external services
- Add business-specific editor commands
๐ ๏ธ Services
DocumentService
Utility service for document operations.
Download Files
// Download as HTML (with styles)
DocumentService.downloadHtml(
htmlContent,
filename: 'document.html',
cleanHtml: true, // Remove editor artifacts
);
// Download as plain text
DocumentService.downloadText(
textContent,
filename: 'document.txt',
);
Clipboard Operations
// Copy to clipboard
final success = await DocumentService.copyToClipboard(text);
// Read from clipboard
final text = await DocumentService.readFromClipboard();
// Opens print-ready document in new tab
DocumentService.printHtml(htmlContent);
Local Storage
// Save draft
DocumentService.saveToLocalStorage('my-draft', htmlContent);
// Load draft
final draft = DocumentService.loadFromLocalStorage('my-draft');
// Check if exists
if (DocumentService.hasLocalStorage('my-draft')) {
// ...
}
// Remove
DocumentService.removeFromLocalStorage('my-draft');
๐ง Utilities
HtmlCleaner
Process HTML for export.
// Clean editor artifacts (selection classes, data attributes)
final clean = HtmlCleaner.cleanForExport(dirtyHtml);
// Extract plain text from HTML
final text = HtmlCleaner.extractText(html);
// Check if content is empty
if (HtmlCleaner.isEmpty(html)) {
print('No content');
}
// Normalize color to hex
final hex = HtmlCleaner.normalizeColor('rgb(255, 0, 0)'); // '#ff0000'
TextStats
Calculate document statistics.
final stats = TextStats.fromHtml(html);
print('Words: ${stats.wordCount}');
print('Characters: ${stats.charCount}');
print('Characters (no spaces): ${stats.charCountNoSpaces}');
print('Paragraphs: ${stats.paragraphCount}');
print('Sentences: ${stats.sentenceCount}');
ExportStyles
Generate CSS for exported HTML.
// Get full CSS string
final css = ExportStyles.fullCss;
// Generate complete HTML document
final fullHtml = ExportStyles.generateHtmlDocument(
content,
title: 'My Document', // Optional
);
๐จ Theming
Using Built-in Theme
MaterialApp(
theme: AppTheme.lightTheme,
home: MyApp(),
)
Color Palette
AppColors.accent // Primary accent color (#C45D35)
AppColors.surface // Card/surface color (white)
AppColors.background // Scaffold background
AppColors.textPrimary // Primary text color
AppColors.textSecondary // Secondary text color
AppColors.border // Border color
AppColors.success // Success state color
AppColors.warning // Warning state color
AppColors.error // Error state color
Text Styles
AppTheme.serifTextStyle // Crimson Pro, 18px
AppTheme.sansTextStyle // DM Sans, 14px
AppTheme.monoTextStyle // Source Code Pro, 14px
Decorations
Container(
decoration: AppTheme.editorContainerDecoration,
child: QuillEditorWidget(...),
)
Container(
decoration: AppTheme.cardDecoration,
child: YourContent(),
)
โ๏ธ Configuration
EditorConfig
// Zoom settings
EditorConfig.minZoom // 0.5 (50%)
EditorConfig.maxZoom // 3.0 (300%)
EditorConfig.defaultZoom // 1.0 (100%)
EditorConfig.zoomStep // 0.1 (10%)
// Table defaults
EditorConfig.defaultTableRows // 3
EditorConfig.defaultTableCols // 3
EditorConfig.maxTableRows // 20
EditorConfig.maxTableCols // 10
// Timing
EditorConfig.contentChangeThrottleMs // 200ms
EditorConfig.autoSaveDebounceMs // 500ms
AppFonts
// Available fonts
AppFonts.availableFonts // List<FontConfig>
// Available sizes
AppFonts.availableSizes // [small, normal, large, huge]
// Available line heights
AppFonts.availableLineHeights // [1.0, 1.5, 2.0, 2.5, 3.0]
๐ Project Structure
lib/
โโโ quill_web_editor.dart # Main export file
โโโ src/
โโโ core/
โ โโโ constants/
โ โ โโโ app_colors.dart # Color palette
โ โ โโโ app_fonts.dart # Font configurations
โ โ โโโ editor_config.dart # Editor settings
โ โโโ theme/
โ โ โโโ app_theme.dart # Theme data
โ โโโ utils/
โ โโโ html_cleaner.dart # HTML processing
โ โโโ text_stats.dart # Document statistics
โ โโโ export_styles.dart # Export CSS generation
โโโ widgets/
โ โโโ quill_editor_widget.dart # Main editor widget
โ โโโ quill_editor_controller.dart # Controller for programmatic access
โ โโโ save_status_indicator.dart # Save status display
โ โโโ zoom_controls.dart # Zoom UI
โ โโโ output_preview.dart # HTML/text preview
โ โโโ stat_card.dart # Statistics cards
โ โโโ app_card.dart # Styled container
โ โโโ html_preview_dialog.dart # Preview dialog
โ โโโ insert_html_dialog.dart # HTML insertion dialog
โโโ services/
โโโ document_service.dart # Document operations
web/
โโโ quill_editor.html # Editor HTML (full-featured)
โโโ quill_viewer.html # Viewer HTML (read-only)
๐งช Testing
The package includes comprehensive tests. Run them with:
flutter test
Testing with Google Fonts
The package uses Google Fonts which require special setup for testing. Font files are bundled in test/fonts/ and configured in flutter_test_config.dart.
๐ก Examples
Complete Editor with Sidebar
class EditorPage extends StatefulWidget {
@override
State<EditorPage> createState() => _EditorPageState();
}
class _EditorPageState extends State<EditorPage> {
final _controller = QuillEditorController();
String _html = '';
SaveStatus _status = SaveStatus.saved;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Editor'),
actions: [
// Undo/Redo
IconButton(
onPressed: () => _controller.undo(),
icon: Icon(Icons.undo),
tooltip: 'Undo',
),
IconButton(
onPressed: () => _controller.redo(),
icon: Icon(Icons.redo),
tooltip: 'Redo',
),
SizedBox(width: 8),
// Zoom - uses controller's reactive zoom level
ListenableBuilder(
listenable: _controller,
builder: (context, _) => ZoomControls(
zoomLevel: _controller.currentZoom,
onZoomIn: () => _controller.zoomIn(),
onZoomOut: () => _controller.zoomOut(),
onReset: () => _controller.resetZoom(),
),
),
SaveStatusIndicator(status: _status),
FilledButton.icon(
onPressed: () => DocumentService.downloadHtml(_html),
icon: Icon(Icons.save),
label: Text('Save'),
),
],
),
body: Row(
children: [
// Editor
Expanded(
child: Container(
margin: EdgeInsets.all(24),
decoration: AppTheme.editorContainerDecoration,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: QuillEditorWidget(
controller: _controller,
onContentChanged: (html, delta) {
setState(() {
_html = html;
_status = SaveStatus.unsaved;
});
},
),
),
),
),
// Sidebar
SizedBox(
width: 320,
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
children: [
AppCard(
title: 'Statistics',
child: StatCardRow(
stats: [
(
label: 'Words',
value: TextStats.fromHtml(_html).wordCount.toString(),
icon: null,
),
],
),
),
SizedBox(height: 16),
Expanded(
child: AppCard(
title: 'Preview',
child: OutputPreview(html: _html),
),
),
],
),
),
),
],
),
);
}
}
Auto-Save Implementation
Timer? _saveTimer;
void _onContentChanged(String html, dynamic delta) {
setState(() => _status = SaveStatus.unsaved);
_saveTimer?.cancel();
_saveTimer = Timer(Duration(milliseconds: 500), () {
setState(() => _status = SaveStatus.saving);
// Save to backend or local storage
DocumentService.saveToLocalStorage('draft', html);
setState(() => _status = SaveStatus.saved);
});
}
๐ Running the Example
cd example
flutter run -d chrome
๐ Deployment
For production deployments, you can host the HTML files on a CDN or static hosting service and reference them via editorHtmlPath and viewerHtmlPath parameters.
Quick Setup
QuillEditorWidget(
editorHtmlPath: 'https://your-cdn.com/quill-editor/quill_editor.html',
viewerHtmlPath: 'https://your-cdn.com/quill-editor/quill_viewer.html',
onContentChanged: (html, delta) {
// Handle changes
},
)
Files to Host
quill_editor.html- Main editor HTMLquill_viewer.html- Viewer HTMLjs/folder - Custom JavaScript overridesstyles/folder - Custom CSS (e.g.,mulish-font.css)fonts/folder - Font files (optional, if hosting locally)
See DEPLOYMENT.md for detailed deployment instructions, folder structure, and hosting configuration.
๐ License
MIT License - see LICENSE file for details.
๐ค Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
Made with โค๏ธ using Flutter & Quill.js
Libraries
- quill_web_editor
- Quill Web Editor - A rich text editor package for Flutter Web.