RazorRichText 10.0.1
dotnet add package RazorRichText --version 10.0.1
NuGet\Install-Package RazorRichText -Version 10.0.1
<PackageReference Include="RazorRichText" Version="10.0.1" />
<PackageVersion Include="RazorRichText" Version="10.0.1" />
<PackageReference Include="RazorRichText" />
paket add RazorRichText --version 10.0.1
#r "nuget: RazorRichText, 10.0.1"
#:package RazorRichText@10.0.1
#addin nuget:?package=RazorRichText&version=10.0.1
#tool nuget:?package=RazorRichText&version=10.0.1
RazorRichText
A full-featured, drop-in rich text editor for ASP.NET Core MVC and Blazor Razor views.
Built on Quill.js with automatic HTML sanitization, content conversion, model binding, and three visual themes — all configurable with a single NuGet package.
Table of Contents
- Requirements
- Installation
- Quick Start (5 minutes)
- Step-by-Step Setup
- TagHelper Reference
- HtmlHelper Reference
- Toolbar Groups
- Themes
- Validation Attributes
- HTML Sanitization
- Content Conversion
- Auto-Save
- Image Upload
- JavaScript API
- Global Configuration
- appsettings.json Configuration
- Common Patterns
- Troubleshooting
Requirements
| Requirement | Version |
|---|---|
| .NET | 8.0 or later |
| ASP.NET Core MVC | 8.0 or later |
| Browser | Any modern browser (Chrome, Edge, Firefox, Safari) |
Blazor Applications
This package also targets Blazor applications, providing a rich text editing experience within your Blazor projects. Integration details will be provided in a future update.
Installation
Via NuGet Package Manager (Visual Studio):
Search for RazorRichText in Tools → NuGet Package Manager → Manage NuGet Packages.
Via .NET CLI:
dotnet add package RazorRichText
Via Package Manager Console:
Install-Package RazorRichText
Quick Start
This gets you a working editor in under 5 minutes.
Program.cs
using RazorRichText.Extensions;
builder.Services.AddControllersWithViews();
builder.Services.AddRazorRichText(); // ← add this line
Views/_ViewImports.cshtml
@using RazorRichText.Models
@using RazorRichText.HtmlHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, RazorRichText
Views/Shared/_Layout.cshtml — inside <head>
@Html.RenderRichTextAssets()
Your view model
using RazorRichText.Models;
using RazorRichText.Services;
public class MyViewModel
{
[RichText]
public string Content { get; set; } = string.Empty;
}
Your view
@model MyViewModel
<form method="post">
@Html.AntiForgeryToken()
<rich-text-editor asp-for="Content" rte-height="350" />
<button type="submit">Save</button>
</form>
Your controller
[HttpPost]
public IActionResult Save(MyViewModel model)
{
if (!ModelState.IsValid) return View(model);
// model.Content contains sanitized HTML
return RedirectToAction("Index");
}
That's it. The editor renders, the form submits, and the HTML is sanitized automatically.
Step-by-Step Setup
1. Register Services — Program.cs
At minimum, call AddRazorRichText() after AddControllersWithViews():
using RazorRichText.Extensions;
using RazorRichText.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(options =>
{
// Optional: automatically sanitize [RichText] properties on every POST
options.ModelBinderProviders.Insert(0, new RichTextModelBinderProvider());
});
builder.Services.AddRazorRichText(opts =>
{
opts.DefaultTheme = RichTextTheme.Light;
opts.DefaultHeight = 350;
opts.DefaultPlaceholder = "Start writing here…";
opts.SanitizeByDefault = true;
opts.AutoIncludeScripts = true;
opts.CleanPasteFromWord = true;
opts.DefaultToolbarGroups =
ToolbarGroups.TextFormatting |
ToolbarGroups.HeadingStyles |
ToolbarGroups.Lists |
ToolbarGroups.Links |
ToolbarGroups.History |
ToolbarGroups.ClearFormat;
});
Note on
RichTextModelBinderProvider: When registered, any view-model property decorated with[RichText]is automatically sanitized before your action method receives it. This is the recommended approach — you never need to call the sanitizer manually.
2. Register the TagHelper — _ViewImports.cshtml
Open Views/_ViewImports.cshtml and add these lines. All three are required:
@using RazorRichText.Models
@using RazorRichText.HtmlHelpers
@addTagHelper *, RazorRichText
@using RazorRichText.ModelsbringsToolbarGroups,RichTextTheme,ContentFormatetc. into scope in every view. Without it you getCS0103: The name 'All' does not exist.
3. Include Page Assets — _Layout.cshtml
Call @Html.RenderRichTextAssets() once in your shared layout, inside <head>:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>@ViewData["Title"]</title>
@Html.RenderRichTextAssets() @* ← Quill CSS + JS + editor CSS *@
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
@RenderBody()
</body>
</html>
This injects:
- Quill Snow CSS (or Bubble CSS, depending on your toolbar style)
- The library's custom editor CSS
- Quill JS from CDN
If you set
AutoIncludeScripts = true(the default), assets are also injected automatically per-editor. CallingRenderRichTextAssets()in the layout is still recommended to avoid duplicate script tags.
4. Add to a View Model
using System.ComponentModel.DataAnnotations;
using RazorRichText.Models;
using RazorRichText.Services;
public class ArticleViewModel
{
[Required]
[StringLength(200)]
public string Title { get; set; } = string.Empty;
// [RichText] — marks property for automatic sanitization on POST
// [RequiredRichText] — validates that the editor is not empty
// [MaxRichTextLength] — validates plain-text character count (not HTML length)
[RichText]
[RequiredRichText(ErrorMessage = "Please enter some content.")]
[MaxRichTextLength(10000)]
[Display(Name = "Article Body")]
public string Body { get; set; } = string.Empty;
[RichText]
[MaxRichTextLength(500)]
public string Summary { get; set; } = string.Empty;
}
5. Add to a View
Using the TagHelper (recommended):
@model ArticleViewModel
@{
ViewData["Title"] = "Write Article";
}
<form method="post">
@Html.AntiForgeryToken()
@Html.ValidationSummary(false)
<div class="mb-3">
<label asp-for="Title" class="form-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Body" class="form-label"></label>
<rich-text-editor asp-for="Body"
rte-height="400"
rte-toolbar-groups="All"
rte-theme="Light"
rte-placeholder="Write your article here…" />
<span asp-validation-for="Body" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Summary" class="form-label"></label>
<rich-text-editor asp-for="Summary"
rte-height="150"
rte-toolbar-groups="TextFormatting,Lists" />
<span asp-validation-for="Summary" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Publish</button>
</form>
Using the HtmlHelper (alternative):
@Html.RichTextEditorFor(m => m.Body, opts =>
{
opts.Height = 400;
opts.Theme = RichTextTheme.Dark;
opts.ToolbarGroups = ToolbarGroups.All;
opts.Placeholder = "Write your article here…";
})
6. Handle POST in the Controller
using RazorRichText.Converters;
using RazorRichText.Sanitizers;
public class ArticleController : Controller
{
private readonly IHtmlSanitizerService _sanitizer;
private readonly IContentConverter _converter;
public ArticleController(
IHtmlSanitizerService sanitizer,
IContentConverter converter)
{
_sanitizer = sanitizer;
_converter = converter;
}
[HttpGet]
public IActionResult Create() => View(new ArticleViewModel());
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(ArticleViewModel model)
{
if (!ModelState.IsValid)
return View(model);
// If RichTextModelBinderProvider is registered in Program.cs,
// model.Body is already sanitized at this point.
// If not, sanitize it manually:
var sanitized = _sanitizer.Sanitize(model.Body);
// Optional: convert to other formats for storage
var markdown = _converter.HtmlToMarkdown(sanitized.Html).Content;
var excerpt = _converter.GetExcerpt(sanitized.Html, 160);
var wordCount = _converter.CountWords(sanitized.Html);
// Save to database...
return RedirectToAction("Index");
}
}
TagHelper Reference
All rte-* attributes are optional. Omitted attributes fall back to global defaults.
<rich-text-editor
asp-for="PropertyName"
rte-id="my-editor"
rte-placeholder="Start typing…"
rte-height="350"
rte-min-height="120"
rte-max-height="600"
rte-theme="Light"
rte-toolbar="Standard"
rte-toolbar-groups="TextFormatting,Lists,Links,History"
rte-css-class="my-custom-class"
rte-output-format="Html"
rte-readonly="false"
rte-spellcheck="true"
rte-required="true"
rte-allow-images="true"
rte-image-url="/api/images/upload"
rte-autosave="true"
rte-autosave-interval="30000"
rte-onchange="myChangeHandler"
rte-onready="myReadyHandler"
rte-locale="en" />
| Attribute | Type | Default | Description |
|---|---|---|---|
asp-for |
Model expression | — | Binds to a model property. Sets name and value automatically. |
rte-id |
string | auto | HTML id of the wrapper element. Auto-generated from asp-for if not set. |
rte-placeholder |
string | global | Text shown when editor is empty. |
rte-height |
int | 300 | Editor height in pixels. Use 0 for auto-grow. |
rte-min-height |
int | 120 | Minimum height in pixels (applies when rte-height="0"). |
rte-max-height |
int | 0 | Maximum height before scrollbar appears. 0 = no limit. |
rte-theme |
Light | Dark | HighContrast |
Light |
Visual colour theme. |
rte-toolbar |
Standard | Bubble | None |
Standard |
Toolbar style. |
rte-toolbar-groups |
Flags string | global | Comma-separated toolbar groups, or All. |
rte-css-class |
string | — | Extra CSS classes on the wrapper <div>. |
rte-output-format |
Html | Markdown | Delta | PlainText |
Html |
Value format written to the hidden input. |
rte-readonly |
bool | false |
Disables editing (toolbar is hidden). |
rte-spellcheck |
bool | true |
Enables browser native spell-check. |
rte-required |
bool | false |
Adds HTML5 required attribute to the hidden input. |
rte-allow-images |
bool | false |
Enables async image upload via rte-image-url. |
rte-image-url |
string | — | Upload endpoint URL. Must return { "url": "..." }. |
rte-autosave |
bool | false |
Saves content to localStorage periodically. |
rte-autosave-interval |
int | 30000 |
Auto-save interval in milliseconds. |
rte-onchange |
string | — | Name of a global JS function called on every change. |
rte-onready |
string | — | Name of a global JS function called when editor is ready. |
rte-locale |
string | "en" |
BCP-47 locale code (reserved for future localisation). |
HtmlHelper Reference
@Html.RichTextEditorFor — Strongly-typed binding
@Html.RichTextEditorFor(m => m.Body)
@Html.RichTextEditorFor(m => m.Body, opts =>
{
opts.Height = 500;
opts.Theme = RichTextTheme.Dark;
opts.ToolbarGroups = ToolbarGroups.All;
opts.Placeholder = "Write here…";
opts.Required = true;
})
@Html.RichTextEditor — Untyped / manual binding
@Html.RichTextEditor(opts =>
{
opts.Name = "Description";
opts.Value = Model.Description;
opts.Height = 300;
opts.ToolbarGroups = ToolbarGroups.TextFormatting | ToolbarGroups.Lists;
})
@Html.RichTextDisplay — Read-only display
Renders stored HTML in a <div> without any editor or toolbar. Use to display saved content.
@Html.RichTextDisplay(Model.Body)
@Html.RichTextDisplay(Model.Body, cssClass: "article-body prose")
@Html.RenderRichTextAssets — CDN assets
Call once in your layout <head>. Injects Quill CSS and JS.
@Html.RenderRichTextAssets()
@Html.RenderRichTextAssets(theme: RichTextTheme.Dark)
@Html.RenderRichTextAssets(theme: RichTextTheme.Light, toolbarStyle: ToolbarStyle.Bubble)
Toolbar Groups
Pass toolbar groups as a comma-separated string. Do not prefix with the enum name.
@* Show all toolbar buttons *@
rte-toolbar-groups="All"
@* Show a minimal set *@
rte-toolbar-groups="TextFormatting,Lists,History"
@* Show everything except word count and fullscreen *@
rte-toolbar-groups="TextFormatting,HeadingStyles,Lists,Alignment,Links,Images,Colors,CodeBlocks,History,ClearFormat,SourceCode"
| Flag | Buttons |
|---|---|
TextFormatting |
Bold, Italic, Underline, Strikethrough |
HeadingStyles |
Heading 1–4, Normal paragraph dropdown |
Lists |
Ordered list, Bullet list, Task list, Indent / Outdent |
Alignment |
Left, Centre, Right, Justify |
Links |
Insert / edit hyperlink |
Images |
Insert image (upload or base-64 embed) |
CodeBlocks |
Inline code, fenced code block |
Colors |
Text colour picker, Highlight colour picker |
Scripts |
Subscript, Superscript |
Blockquote |
Block quote |
HorizontalRule |
Horizontal divider line |
History |
Undo, Redo |
ClearFormat |
Strip all inline formatting |
FullScreen |
Toggle fullscreen mode |
SourceCode |
Toggle raw HTML source view |
WordCount |
Live word counter in toolbar |
All |
Every group above |
None |
Empty toolbar bar |
Themes
| Value | Appearance |
|---|---|
Light |
White background, light-grey toolbar. Default. |
Dark |
Dark-charcoal background with light text. |
HighContrast |
Black background, white text — WCAG 2.1 AA/AAA. |
<rich-text-editor asp-for="Body" rte-theme="Dark" />
You can override any CSS variable from your own stylesheet:
/* Override specific tokens for an editor with class "my-editor" */
.my-editor.rte-wrapper {
--rte-bg: #fafafa;
--rte-border: #cccccc;
--rte-toolbar-bg: #f0f0f0;
--rte-focus-border:#6200ee;
--rte-font: 'Georgia', serif;
}
Validation Attributes
The library provides three custom DataAnnotations attributes that work correctly with HTML content by measuring plain-text length, not raw HTML string length.
[RequiredRichText]
Ensures the editor is not empty. Correctly rejects Quill's empty-editor placeholder <p><br></p>.
[RichText]
[RequiredRichText]
public string Body { get; set; } = string.Empty;
// Custom error message:
[RequiredRichText(ErrorMessage = "Please enter a description.")]
public string Description { get; set; } = string.Empty;
[MaxRichTextLength(n)]
Limits the plain-text character count (HTML tags not counted).
[RichText]
[MaxRichTextLength(5000)]
public string Body { get; set; } = string.Empty;
[MinRichTextLength(n)]
Requires at least n plain-text characters.
[RichText]
[MinRichTextLength(100, ErrorMessage = "Please write at least 100 characters.")]
public string Body { get; set; } = string.Empty;
Combining attributes
[RichText]
[RequiredRichText]
[MinRichTextLength(50)]
[MaxRichTextLength(10000)]
[Display(Name = "Article Content")]
public string Body { get; set; } = string.Empty;
Display validation messages in the view with the standard tag helper:
<span asp-validation-for="Body" class="text-danger"></span>
HTML Sanitization
Always sanitize user-submitted rich text before storing it or rendering it with @Html.Raw(). The library uses Ganss.Xss under the hood.
Automatic sanitization (recommended)
Register RichTextModelBinderProvider and mark your properties with [RichText]. Sanitization happens automatically before your action method runs.
// Program.cs
builder.Services.AddControllersWithViews(options =>
options.ModelBinderProviders.Insert(0, new RichTextModelBinderProvider()));
// ViewModel
[RichText] // ← triggers sanitization on POST
public string Content { get; set; } = string.Empty;
// Controller — Content is already clean when you receive it
[HttpPost]
public IActionResult Save(MyViewModel model) { ... }
Manual sanitization
Inject IHtmlSanitizerService and call it explicitly:
private readonly IHtmlSanitizerService _sanitizer;
public MyController(IHtmlSanitizerService sanitizer)
{
_sanitizer = sanitizer;
}
[HttpPost]
public IActionResult Save(MyViewModel model)
{
var result = _sanitizer.Sanitize(model.Content);
if (result.WasModified)
{
// Log what was stripped for audit purposes
foreach (var change in result.Changes)
_logger.LogWarning("Sanitized: {Change}", change);
}
var cleanHtml = result.Html;
// Save cleanHtml to database...
}
Customize the allow-list
builder.Services.AddRazorRichText(opts =>
{
// Add a tag to the allow-list
opts.AllowedTags.Add("video");
opts.AllowedTags.Add("source");
// Remove a tag
opts.AllowedTags.Remove("script"); // already not in the default list
// Allow additional attributes
opts.AllowedAttributes.Add("controls");
opts.AllowedAttributes.Add("autoplay");
// Allow a CSS property inside style="…"
opts.AllowedCssProperties.Add("display");
});
Rendering stored content safely
@* WRONG — never use @Html.Raw() on unsanitized user input *@
@Html.Raw(Model.Body)
@* CORRECT — use the library's display helper (pre-sanitized content is safe) *@
@Html.RichTextDisplay(Model.Body)
@* ALSO CORRECT — if content was sanitized before saving, @Html.Raw is acceptable *@
@Html.Raw(Model.Body)
Content Conversion
Inject IContentConverter to convert between formats:
using RazorRichText.Converters;
public class ArticleService
{
private readonly IContentConverter _converter;
public ArticleService(IContentConverter converter)
{
_converter = converter;
}
public void ProcessArticle(string html)
{
// Convert HTML to Markdown
var mdResult = _converter.HtmlToMarkdown(html);
if (mdResult.Success)
Console.WriteLine(mdResult.Content);
// Convert HTML to plain text
var txtResult = _converter.HtmlToPlainText(html);
// Generate a 160-character excerpt (great for meta descriptions)
var excerpt = _converter.GetExcerpt(html, 160);
// Count words (HTML tags not counted)
var wordCount = _converter.CountWords(html);
// General-purpose conversion
var result = _converter.Convert(html, ContentFormat.Html, ContentFormat.Markdown);
}
}
| Method | From | To |
|---|---|---|
HtmlToMarkdown(html) |
HTML | Markdown |
MarkdownToHtml(markdown) |
Markdown | HTML |
HtmlToPlainText(html) |
HTML | Plain text |
Convert(content, from, to) |
Any | Any (HTML, Markdown, PlainText) |
GetExcerpt(html, maxLength) |
HTML | Plain text excerpt |
CountWords(html) |
HTML | int word count |
Auto-Save
Automatically saves the editor content to localStorage at a regular interval. On the next page load, the draft is restored if no server-side value exists.
<rich-text-editor asp-for="Body"
rte-autosave="true"
rte-autosave-interval="15000" /> @* Save every 15 seconds *@
The localStorage key is rte_autosave_{editorId}.
Auto-save is ideal for long-form content where users might accidentally close the browser tab. The draft is cleared automatically when the form is submitted and the page reloads with a server-side value.
Image Upload
Enable server-side image uploads by providing an upload endpoint:
<rich-text-editor asp-for="Body"
rte-toolbar-groups="All"
rte-allow-images="true"
rte-image-url="/api/images/upload" />
Your endpoint must:
- Accept
POST multipart/form-datawith afilefield - Return JSON:
{ "url": "/uploads/image-name.jpg" }
Example controller:
[ApiController]
[Route("api/images")]
public class ImageUploadController : ControllerBase
{
private readonly IWebHostEnvironment _env;
public ImageUploadController(IWebHostEnvironment env)
{
_env = env;
}
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest(new { error = "No file provided" });
// Validate file type
var allowedTypes = new[] { "image/jpeg", "image/png", "image/gif", "image/webp" };
if (!allowedTypes.Contains(file.ContentType.ToLower()))
return BadRequest(new { error = "Invalid file type" });
// Save to wwwroot/uploads
var uploadsDir = Path.Combine(_env.WebRootPath, "uploads");
Directory.CreateDirectory(uploadsDir);
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var filePath = Path.Combine(uploadsDir, fileName);
await using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
return Ok(new { url = $"/uploads/{fileName}" });
}
}
JavaScript API
Every editor instance is accessible globally via window.__rte:
// Get the Quill instance for an editor with id="rte_Body"
var quill = window.__rte['rte_Body'];
// Read the current HTML content
var html = quill.root.innerHTML;
// Set content programmatically
quill.clipboard.dangerouslyPasteHTML(0, '<p>New content</p>', 'silent');
// Clear the editor
quill.setContents([]);
// Get plain text
var text = quill.getText();
// Get word count
var words = quill.getText().trim().split(/\s+/).filter(Boolean).length;
// Focus the editor
quill.focus();
// Set read-only mode
quill.enable(false); // disable
quill.enable(true); // re-enable
Change callback
<rich-text-editor asp-for="Body" rte-onchange="onBodyChange" />
// Called on every keystroke / format change
// Signature: function(html, quill, editorId)
function onBodyChange(html, quill, editorId) {
var wordCount = quill.getText().trim().split(/\s+/).filter(Boolean).length;
document.getElementById('word-count').textContent = wordCount + ' words';
}
Ready callback
<rich-text-editor asp-for="Body" rte-onready="onEditorReady" />
// Called once when the editor has fully initialised
// Signature: function(quill, editorId)
function onEditorReady(quill, editorId) {
console.log('Editor ready:', editorId);
quill.focus();
}
Global Configuration
Configure defaults in Program.cs — every editor on every page uses these unless overridden per-instance.
builder.Services.AddRazorRichText(opts =>
{
// ── Appearance ────────────────────────────────────────────────────────
opts.DefaultTheme = RichTextTheme.Light; // Light | Dark | HighContrast
opts.DefaultToolbarStyle = ToolbarStyle.Standard; // Standard | Bubble | None
opts.DefaultToolbarGroups = ToolbarGroups.TextFormatting
| ToolbarGroups.HeadingStyles
| ToolbarGroups.Lists
| ToolbarGroups.Links
| ToolbarGroups.History
| ToolbarGroups.ClearFormat;
opts.DefaultHeight = 300;
opts.DefaultPlaceholder = "Start typing…";
opts.DefaultOutputFormat = ContentFormat.Html;
// ── Sanitization ──────────────────────────────────────────────────────
opts.SanitizeByDefault = true;
opts.CleanPasteFromWord = true; // strip MS Word markup on paste
opts.MaxContentLength = 0; // 0 = unlimited
// Customise the HTML allow-list
opts.AllowedTags.Add("video");
opts.AllowedAttributes.Add("controls");
opts.AllowedCssProperties.Add("display");
// ── Assets ────────────────────────────────────────────────────────────
opts.AutoIncludeScripts = true; // inject Quill CDN links per editor
// Override CDN URLs (e.g. to use a self-hosted copy)
opts.QuillJsCdnUrl = "https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js";
opts.QuillSnowCssCdnUrl = "https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css";
opts.QuillBubbleCssCdnUrl = "https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.bubble.css";
// ── Content Security Policy ───────────────────────────────────────────
opts.CspNonce = null; // set to your per-request CSP nonce if needed
});
appsettings.json Configuration
Instead of a code callback, bind options from appsettings.json:
Program.cs:
builder.Services.AddRazorRichText(builder.Configuration);
appsettings.json:
{
"RazorRichText": {
"DefaultHeight": 400,
"DefaultTheme": "Dark",
"DefaultPlaceholder": "Start writing…",
"SanitizeByDefault": true,
"CleanPasteFromWord": true,
"AutoIncludeScripts": true,
"MaxContentLength": 20000
}
}
Combined — config file plus runtime override:
// appsettings.json is the base; the lambda overrides specific values at runtime
builder.Services.AddRazorRichText(
builder.Configuration,
opts => opts.CspNonce = HttpContext.GetCspNonce() // runtime value
);
Common Patterns
Read-only preview
@* Editor for editing *@
@if (Model.IsEditing)
{
<rich-text-editor asp-for="Body" rte-height="400" />
}
else
{
@* Display helper for viewing *@
@Html.RichTextDisplay(Model.Body, cssClass: "article-body")
}
Two editors on one page
Each editor uses its own hidden input, bound to a different model property:
<rich-text-editor asp-for="Body" rte-id="editor-body" rte-height="400" />
<rich-text-editor asp-for="Summary" rte-id="editor-summary" rte-height="150"
rte-toolbar-groups="TextFormatting,Lists" />
Dark theme for admin areas
In your admin layout, override the global default:
@Html.RenderRichTextAssets(theme: RichTextTheme.Dark)
Then in admin views:
<rich-text-editor asp-for="Body" rte-theme="Dark" />
Auto-grow (no fixed height)
<rich-text-editor asp-for="Body"
rte-height="0"
rte-min-height="200" />
Minimal blog-style editor
<rich-text-editor asp-for="Comment"
rte-toolbar="Bubble"
rte-height="120"
rte-toolbar-groups="TextFormatting,Links"
rte-placeholder="Add a comment…" />
Display excerpt in a listing page
// In the controller
var excerpt = _converter.GetExcerpt(article.Body, 160);
@* In the view *@
<p class="article-excerpt">@Model.Excerpt</p>
Troubleshooting
CS0103: The name 'All' does not exist in the current context
You are missing @using RazorRichText.Models in _ViewImports.cshtml.
@using RazorRichText.Models ← add this
@addTagHelper *, RazorRichText
Content is null after form POST
Three things to check:
Enum syntax — pass the flag name only, not the type prefix:
@* Wrong *@ rte-toolbar-groups="ToolbarGroups.All" @* Right *@ rte-toolbar-groups="All"[RichText]attribute — add it to your view-model property so the model binder finds it:[RichText] public string Content { get; set; } = string.Empty;RichTextModelBinderProvider— register it inProgram.cs:builder.Services.AddControllersWithViews(options => options.ModelBinderProviders.Insert(0, new RichTextModelBinderProvider()));
IEditorRenderService not found / build errors in QuillEditorRenderService.cs
Replace your QuillEditorRenderService.cs with the latest version from the NuGet package. Earlier versions had a namespace mismatch caused by a file-level vs block-level namespace conflict.
Quill editor is not rendering / JavaScript errors
- Ensure
@Html.RenderRichTextAssets()is in your<head>before@RenderBody(). - Check the browser console for
[RazorRichText] Quill.js is not loaded. - If you set
AutoIncludeScripts = false, you must call@Html.RenderRichTextAssets()manually.
TagHelper is not recognized in the view
Confirm _ViewImports.cshtml contains:
@addTagHelper *, RazorRichText
Toolbar buttons show but do not format text
This happens when two copies of Quill.js are loaded. Set AutoIncludeScripts = false and call @Html.RenderRichTextAssets() exactly once in the layout.
Word paste looks messy
Enable the Word paste cleaner (it is on by default):
opts.CleanPasteFromWord = true;
License
MIT — free to use in commercial and open-source projects.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- HtmlAgilityPack (>= 1.12.4)
- HtmlSanitizer (>= 9.0.892)
- Markdig (>= 1.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.