RazorRichText 10.0.1

dotnet add package RazorRichText --version 10.0.1
                    
NuGet\Install-Package RazorRichText -Version 10.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RazorRichText" Version="10.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RazorRichText" Version="10.0.1" />
                    
Directory.Packages.props
<PackageReference Include="RazorRichText" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RazorRichText --version 10.0.1
                    
#r "nuget: RazorRichText, 10.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RazorRichText@10.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RazorRichText&version=10.0.1
                    
Install as a Cake Addin
#tool nuget:?package=RazorRichText&version=10.0.1
                    
Install as a Cake Tool

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.

RazorRichText Editor in action


Table of Contents


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.Models brings ToolbarGroups, RichTextTheme, ContentFormat etc. into scope in every view. Without it you get CS0103: 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. Calling RenderRichTextAssets() 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.

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:

  1. Accept POST multipart/form-data with a file field
  2. 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:

  1. Enum syntax — pass the flag name only, not the type prefix:

    @* Wrong *@  rte-toolbar-groups="ToolbarGroups.All"
    @* Right *@  rte-toolbar-groups="All"
    
  2. [RichText] attribute — add it to your view-model property so the model binder finds it:

    [RichText]
    public string Content { get; set; } = string.Empty;
    
  3. RichTextModelBinderProvider — register it in Program.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.1 80 9/18/2026
10.0.0 131 6/10/2026
1.3.0 82 9/18/2026
1.0.0 123 6/9/2026