Rich-text editor chrome
tier 2 — classes
The frame, the toolbar and the body for an editor your app brings — TipTap, Quill, ProseMirror or a
plain contenteditable. Not an editor: the document model, the sanitiser and the
security surface stay with the editor you chose.
Around an editor
Live — type in it. .editor is the frame and takes a focused field's
ring while anything inside has focus. .editor-toolbar is a wrapping row of
.btn-ghost.btn-icon.btn-sm with .editor-sep between groups, and a mark
that is on is aria-pressed="true" — the attribute your editor already reports, which
the toolbar colours. .editor-body is where the editor mounts and what scrolls, capped
by --editor-max-height; add .prose to it so what is typed looks like
what is later rendered. For Markdown, the library's own editor is the
better fit.
Handover for ORD-4209. Three of five lines are reserved in EU-West.
- Hold the remaining two for stock, re-checked hourly.
- Alex Fischer owns the reservation until Monday.
<div class="editor">
<div class="editor-toolbar" role="toolbar" aria-label="Formatting">
<button class="btn btn-ghost btn-icon btn-sm" type="button" aria-pressed="true" aria-label="Bold"><i class="ri-bold"></i></button>
<button class="btn btn-ghost btn-icon btn-sm" type="button" aria-pressed="false" aria-label="Italic"><i class="ri-italic"></i></button>
<button class="btn btn-ghost btn-icon btn-sm" type="button" aria-pressed="false" aria-label="Underline"><i class="ri-underline"></i></button>
<span class="editor-sep" role="separator"></span>
<button class="btn btn-ghost btn-icon btn-sm" type="button" aria-pressed="false" aria-label="Bulleted list"><i class="ri-list-unordered"></i></button>
<button class="btn btn-ghost btn-icon btn-sm" type="button" aria-pressed="false" aria-label="Numbered list"><i class="ri-list-ordered"></i></button>
<span class="editor-sep" role="separator"></span>
<button class="btn btn-ghost btn-icon btn-sm" type="button" aria-label="Insert a link"><i class="ri-link"></i></button>
</div>
<div class="editor-body prose" contenteditable="true" role="textbox" aria-multiline="true" aria-label="Handover note">
<p><strong>Handover for ORD-4209.</strong> Three of five lines are reserved in EU-West.</p>
<ul>
<li>Hold the remaining two for stock, re-checked hourly.</li>
<li>Alex Fischer owns the reservation until Monday.</li>
</ul>
</div>
</div>
Quill, through Spillgebees.Blazor.RichTextEditor
Live — type, format, add a link. The one editor the library dresses. The
app adds the Spillgebees.Blazor.RichTextEditor
package itself — Sedna.UI references nothing third-party — and hands the chrome's
classes to the component: .editor as ContainerClass,
.editor-toolbar through ToolbarOptions, and
.editor-body.prose as EditorContainerClass. The toolbar is the
app's own: .btn-ghost.btn-icon.btn-sm buttons carrying Quill's
ql-* class, which Quill fills with its icon and marks
aria-pressed.
Prefer buttons to Quill's pickers. A <select class="ql-header">
is dressed too — Quill turns it into a dropdown labelled with the app's option text —
but the label Quill builds is a role="button" with no accessible name, and Quill
copies the select's aria-label onto an element that may not carry one. A button per
value, as here, has neither problem.
Do not link the package's stylesheet. It is unlayered, so it would beat
every rule the library has; the library carries what Quill needs to work. The link
tooltip's words come from --editor-link-open, --editor-link-edit,
--editor-link-save and --editor-link-remove, set on any ancestor
in the app's language. Built and tested against version 3.0.0 of the package (Quill 2).
@using Spillgebees.Blazor.RichTextEditor.Components
@using Spillgebees.Blazor.RichTextEditor.Components.Toolbar
@* The link tooltip's words, in the app's language. Any ancestor of the editor will do. *@
<div style='--editor-link-open: "Open"; --editor-link-edit: "Edit"; --editor-link-save: "Save"; --editor-link-remove: "Remove"'>
<RichTextEditor @bind-Content="_content"
ContainerClass="editor"
ToolbarOptions="_toolbar"
EditorContainerClass="editor-body prose"
Placeholder="Write the handover note"
UseAccessibleKeybindings="true">
<ToolbarContent>
<button class="btn btn-ghost btn-icon btn-sm ql-header" type="button" value="2" aria-label="Heading"></button>
<button class="btn btn-ghost btn-icon btn-sm ql-header" type="button" value="3" aria-label="Subheading"></button>
<span class="editor-sep" role="separator"></span>
<button class="btn btn-ghost btn-icon btn-sm ql-bold" type="button" aria-label="Bold"></button>
<button class="btn btn-ghost btn-icon btn-sm ql-italic" type="button" aria-label="Italic"></button>
<button class="btn btn-ghost btn-icon btn-sm ql-underline" type="button" aria-label="Underline"></button>
<span class="editor-sep" role="separator"></span>
<button class="btn btn-ghost btn-icon btn-sm ql-list" type="button" value="bullet" aria-label="Bulleted list"></button>
<button class="btn btn-ghost btn-icon btn-sm ql-list" type="button" value="ordered" aria-label="Numbered list"></button>
<button class="btn btn-ghost btn-icon btn-sm ql-list" type="button" value="check" aria-label="Checklist"></button>
<span class="editor-sep" role="separator"></span>
<button class="btn btn-ghost btn-icon btn-sm ql-blockquote" type="button" aria-label="Quote"></button>
<button class="btn btn-ghost btn-icon btn-sm ql-code-block" type="button" aria-label="Code block"></button>
<button class="btn btn-ghost btn-icon btn-sm ql-link" type="button" aria-label="Link"></button>
<button class="btn btn-ghost btn-icon btn-sm ql-clean" type="button" aria-label="Clear formatting"></button>
</ToolbarContent>
</RichTextEditor>
</div>
@code {
// The toolbar row takes the chrome's class through the options record.
private readonly ToolbarOptions _toolbar = new() { ToolbarContainerClass = "editor-toolbar" };
private string _content =
"<p><strong>Handover for ORD-4209.</strong> Three of five lines are reserved in EU-West.</p>"
+ "<ol><li data-list=\"ordered\">Hold the remaining two for stock, re-checked hourly.</li>"
+ "<li data-list=\"ordered\">Confirm the carrier slot for Thursday.</li>"
+ "<li data-list=\"checked\">Reservation moved to Alex Fischer.</li>"
+ "<li data-list=\"unchecked\">Tell the customer about the split delivery.</li></ol>";
}