TabBlazor.TipTap
1.7.63-beta
dotnet add package TabBlazor.TipTap --version 1.7.63-beta
NuGet\Install-Package TabBlazor.TipTap -Version 1.7.63-beta
<PackageReference Include="TabBlazor.TipTap" Version="1.7.63-beta" />
<PackageVersion Include="TabBlazor.TipTap" Version="1.7.63-beta" />
<PackageReference Include="TabBlazor.TipTap" />
paket add TabBlazor.TipTap --version 1.7.63-beta
#r "nuget: TabBlazor.TipTap, 1.7.63-beta"
#:package TabBlazor.TipTap@1.7.63-beta
#addin nuget:?package=TabBlazor.TipTap&version=1.7.63-beta&prerelease
#tool nuget:?package=TabBlazor.TipTap&version=1.7.63-beta&prerelease
TabBlazor.TipTap
A rich text (WYSIWYG) editor for TabBlazor, built on TipTap 3: a
Tabler toolbar, uploads, @ mentions and app-defined extensions. The value is HTML, or the TipTap JSON document.
builder.Services.AddTabBlazor();
builder.Services.AddTabBlazorTipTap(options => options.AllowHtml(html => sanitizer.Sanitize(html)));
<RichTextEditor @bind-Value="model.Body" Label="Body" />
<RichTextView Html="@article.Body" />
No script tag is needed: the editor loads its module and the TipTap bundle (_content/TabBlazor.TipTap/tiptap.js)
when it first renders. Its styles are part of the app's scoped CSS bundle (YourApp.styles.css).
Features
Extensions turns on built-in features. Default is Basic | Image.
| Flag | What it adds |
|---|---|
Basic |
Headings (H1–H3), bold, italic, underline, strike, inline code, lists, quotes, code blocks, horizontal rules, links, undo/redo |
Image |
Images, added through uploads only |
Table |
Tables with a size picker and row and column tools |
TaskList |
Task lists with check boxes |
TextAlign |
Left, center and right alignment |
Highlight |
Highlighted text |
Typography |
Smart quotes and dashes |
Mention |
@ mentions; needs MentionSearch |
Content of a feature that is off is reduced to plain paragraphs when it is pasted or loaded.
Toolbar
Toolbar picks the buttons and their order. RichTextToolbar.Full (the default) has every button whose feature is on;
RichTextToolbar.Minimal has bold, italic, lists and link. Or list your own, with Separator between groups:
<RichTextEditor @bind-Value="model.Comment"
Toolbar="RichTextToolbar.Minimal"
Extensions="RichTextExtensions.Basic" />
<RichTextEditor @bind-Value="model.Note"
Toolbar="@([RichTextTool.Bold, RichTextTool.Italic, RichTextTool.Separator, RichTextTool.Link])" />
The toolbar only picks buttons. What content is allowed is Extensions, so shortcuts (Ctrl+U) and pasted formatting
still work without a button; set both for a simple editor.
Markdown value
Format="RichTextEditorFormat.Markdown" edits visually but stores markdown (CommonMark with GitHub tables and task
lists):
<RichTextEditor @bind-Value="model.Notes" Format="RichTextEditorFormat.Markdown" />
- Features markdown cannot store are off and leave the toolbar: underline, highlight and alignment.
- Mentions are stored as
[@ id="42" label="Anna"];RichTextMentions.GetIds(value, RichTextEditorFormat.Markdown)reads the ids. - App extensions need their own markdown rules (
renderMarkdown/parseMarkdown), or their content is lost on save. - The support is a separate file (
tiptap-markdown.js, 19 KB gzipped) that only these editors load. A custom bundle that exportsMarkdownitself makes it unnecessary; otherwiseTipTapOptions.MarkdownScriptUrlsets where it is. - Show saved markdown with an
IMarkdownRenderer, such as the one fromTabBlazor.Markdown; the read-only editor in static rendering uses it too.
Showing saved HTML safely
The browser can post any HTML, so sanitize it where you save it and again where you show it. RichTextView shows
HTML only through the sanitizer registered with AllowHtml; without one it throws (set AllowUnsanitizedHtml = true
if you already sanitize at save time). The package has no sanitizer dependency. With
HtmlSanitizer, allow what the editor writes:
var sanitizer = new Ganss.Xss.HtmlSanitizer();
sanitizer.AllowedSchemes.Add("mailto");
foreach (var attribute in new[] { "data-type", "data-id", "data-label", "data-mention-suggestion-char", "data-checked", "rel" })
{
sanitizer.AllowedAttributes.Add(attribute); // mentions and task lists
}
sanitizer.AllowedTags.Add("input"); // task list check boxes
sanitizer.AllowedTags.Add("label");
sanitizer.AllowedAttributes.Add("type");
sanitizer.AllowedAttributes.Add("checked");
sanitizer.AllowedCssProperties.Clear();
sanitizer.AllowedCssProperties.Add("text-align"); // TextAlign
builder.Services.AddTabBlazorTipTap(options => options.AllowHtml(html => sanitizer.Sanitize(html)));
Links accept only http, https, mailto and relative URLs, and get rel="noopener noreferrer nofollow".
JSON to HTML
The JSON format (Format="RichTextEditorFormat.Json") carries no markup. RichTextJson.ToHtml turns it into the same
HTML the editor writes, in .NET, so it works without a browser: in emails, PDFs, background jobs and APIs.
<RichTextView Json="@article.Body" />
string html = RichTextJson.ToHtml(article.Body);
The HTML is safe by construction and needs no sanitizer: text and attribute values are encoded, only known attributes
are written, links keep only http, https, mailto and relative URLs, and images only http, https and relative
ones. Nodes of unknown types are left out with their content, and marks of unknown types are left out with their text
kept. Render the nodes of your own extensions with Json.Nodes, and encode every value from the document:
builder.Services.AddTabBlazorTipTap(options =>
options.Json.Nodes["callout"] = node =>
$"<div class=\"callout\" data-kind=\"{RichTextJson.Encode(node.Attr("kind"))}\">{node.ContentHtml}</div>");
Uploads
Set UploadFile to let users paste, drop or pick files. The contract is the same as MarkdownEditor's:
private async Task<EditorUploadResult> UploadAsync(EditorUploadFile file, CancellationToken token)
{
await using var stream = file.OpenReadStream(token);
var url = await storage.SaveAsync(file.Name, file.ContentType, stream, token);
return new EditorUploadResult(url);
}
Images are embedded and other files linked. Images are never stored inline as base64: images pasted from Word or web
pages as data: URLs are uploaded instead, and dropped without UploadFile. Serve uploaded files with
Content-Disposition: attachment (or from another domain) and X-Content-Type-Options: nosniff, and check the
type on the server.
Mentions
<RichTextEditor @bind-Value="model.Comment"
Extensions="RichTextExtensions.Default | RichTextExtensions.Mention"
MentionSearch="SearchPeopleAsync" />
@code {
Task<IReadOnlyList<RichTextMention>> SearchPeopleAsync(string query, CancellationToken token) =>
people.SearchAsync(query, take: 8, token);
}
A mention is stored as <span data-type="mention" data-id="42" data-label="Anna">@Anna</span>.
RichTextMentions.GetIds(html) returns the ids, for example to notify people when the content is saved. The search
runs after MentionDebounceMs (150) and a newer query cancels the older one's token.
App extensions
Write a TipTap extension in an ES module that imports TipTap from the editor's bundle, so both share one copy of ProseMirror:
// wwwroot/js/callout.js
import { Node, mergeAttributes } from '/_content/TabBlazor.TipTap/tiptap.js';
export function register(registry) {
registry.add('callout', options => Node.create({
name: 'callout',
group: 'block',
content: 'inline*',
parseHTML: () => [{ tag: 'div[data-callout]' }],
renderHTML: ({ HTMLAttributes }) => ['div', mergeAttributes(HTMLAttributes, { 'data-callout': '', class: 'alert alert-info' }), 0],
addCommands() {
return { toggleCallout: () => ({ commands }) => commands.toggleNode(this.name, 'paragraph') };
}
}));
}
builder.Services.AddTabBlazorTipTap(options => options.ExtensionModules.Add("js/callout.js"));
<RichTextEditor @bind-Value="model.Body" CustomExtensions="@(["callout"])">
<ToolbarExtra>
<button type="button" class="btn btn-ghost-secondary" data-rt-command="toggleCallout">Callout</button>
</ToolbarExtra>
</RichTextEditor>
ExtensionOptions passes options to the factory by extension name. A ToolbarExtra button with data-rt-command
(and optional JSON in data-rt-args) runs the command in the browser; RunCommandAsync and InsertContentAsync
do the same from .NET. To bundle TipTap yourself, point TipTapOptions.ScriptUrl at your build; it must export the
same names. The import path in an extension module must resolve to the same URL as ScriptUrl, or the browser loads a
second copy.
Notes
- The value reaches .NET after
DebounceMs(300 ms) without typing, when the editor loses focus, and after toolbar actions. On Blazor Server it travels in one SignalR message; raiseMaximumReceiveMessageSize(32 KB by default) for long documents. - In static server rendering the editor shows the content read-only (sanitized HTML, or its text without a sanitizer) and posts the value through a hidden input named after the bound field.
- Keyboard: Alt+F10 moves to the toolbar, Escape returns to the text, Ctrl+K edits the link.
- If TipTap cannot start in the browser (the bundle is blocked by a proxy or CSP, missing, or an extension throws), the
editor shows the raw value in a plain text area that still binds, raises
OnLoadErrorwith the reason and setsLoadFailed. It never throws into Blazor, so a Server circuit survives. The browser console has the details. - The TipTap bundles are built from
tools/tiptapin the repository (npm ci && npm run build); CI checks that the committed bundles match the lockfile and that every bundled package is MIT licensed.
| 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
- TabBlazor (>= 1.7.63-beta)
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 |
|---|---|---|
| 1.7.63-beta | 49 | 11/9/2026 |