CKEditor.Blazor
1.11.1
dotnet add package CKEditor.Blazor --version 1.11.1
NuGet\Install-Package CKEditor.Blazor -Version 1.11.1
<PackageReference Include="CKEditor.Blazor" Version="1.11.1" />
<PackageVersion Include="CKEditor.Blazor" Version="1.11.1" />
<PackageReference Include="CKEditor.Blazor" />
paket add CKEditor.Blazor --version 1.11.1
#r "nuget: CKEditor.Blazor, 1.11.1"
#:package CKEditor.Blazor@1.11.1
#addin nuget:?package=CKEditor.Blazor&version=1.11.1
#tool nuget:?package=CKEditor.Blazor&version=1.11.1
ckeditor5-blazor
CKEditor 5 for Blazor - a lightweight multiplatform WYSIWYG editor integration for ASP.NET Core Blazor Server and WebAssembly. It works with Razor components and .NET forms. Easy to set up, it supports self-hosted assets, CDN loading, multiple editor types, shared contexts, localization, and custom plugins.
This integration is unofficial and not maintained by CKSource. For official CKEditor 5 documentation, visit ckeditor.com. If you encounter any issues in editor, please report them on the GitHub repository.
<p align="center"> <img src="docs/intro-classic-editor.png" alt="CKEditor 5 Classic Editor in .NET / Blazor application"> </p>
Table of Contents
- ckeditor5-blazor
- Table of Contents
- Installation ๐
- Basic Usage ๐
- Configuration โ๏ธ
- Providing the License Key ๐๏ธ
- Localization ๐
- Editor Types ๐๏ธ
- Advanced configuration โ๏ธ
- Context ๐ค
- Custom plugins ๐งฉ
- Editors and Contexts registry ๐
- Development โ๏ธ
- Psst... ๐
- Trademarks ๐
- License ๐
Installation ๐
Choose between two installation methods based on your needs. Both approaches provide the same editor API in Razor, but differ in how CKEditor 5 assets are loaded and managed.
๐ Compatibility
| CKEditor 5 Version | Integration Version |
|---|---|
| 43.x โ 47.x | <= 1.10.x |
| >= 48.0 | >= 1.11.x |
๐ Self-hosted via MSBuild
Bundle CKEditor 5 with your application for full control over assets, versioning, and offline support. During build, the package downloads required assets automatically.
Complete setup:
Add NuGet dependency:
dotnet add package CKEditor.Blazor(Optional) Override MSBuild asset options in your
.csproj:<PropertyGroup> <CKEditorVersion>48.1.0</CKEditorVersion> <CKEditorIncludePremiumAssets>false</CKEditorIncludePremiumAssets> <CKBoxVersion>2.8.0</CKBoxVersion> <CKBoxIncludeAssets>true</CKBoxIncludeAssets> <CKEditorAssetsOutputPath>$(MSBuildProjectDirectory)/wwwroot</CKEditorAssetsOutputPath> <CKEditorNpmRegistryUrl>https://registry.npmjs.org</CKEditorNpmRegistryUrl> </PropertyGroup>Register CKEditor services in
Program.cs:using CKEditor.Blazor.Services; builder.Services.AddCKEditor();By default the package infers the correct asset URL from build metadata, so no extra configuration is needed for the typical setup.
If your static files are served from a non-standard base path (e.g. behind a reverse proxy with a path prefix, or assets placed in a subdirectory), you can adjust the base paths:
using CKEditor.Blazor.Model.SelfHosted; using CKEditor.Blazor.Services; builder.Services.AddCKEditor(options => options .ExtendDefaultPreset(preset => preset .WithSelfHosted(new SelfHostedConfig { AssetsBasePath = "/static/ckeditor", IntegrationBasePath = "/custom/_content/CKEditor.Blazor" })));AssetsBasePathapplies to the bundled CKEditor 5 assets directory (editor scripts, stylesheets, translations).IntegrationBasePathapplies to the internal Blazor integration script (default:/_content/CKEditor.Blazor).
Both values should be specified without a trailing slash and must match what the browser actually uses to fetch the files.
Build your project to download and prepare assets:
dotnet buildAdd self-hosted assets component in
<head>(e.g.App.razor):@using CKEditor.Blazor.Components.Assets <HeadContent> <CKE5Assets /> </HeadContent>Use editor components anywhere in your Razor UI:
@using CKEditor.Blazor.Components <CKE5Editor Value="@("<p>Hello world!</p>")" />
๐ก CDN Distribution
Load CKEditor 5 from CKSource CDN using import maps. This method avoids local asset downloads and is good for quick setup.
Complete setup:
Add NuGet dependency:
dotnet add package CKEditor.Blazor(Optional) Override MSBuild asset options in your
.csproj:<PropertyGroup> <CKEditorIncludeAssets>false</CKEditorIncludeAssets> <CKEditorIncludePremiumAssets>false</CKEditorIncludePremiumAssets> <CKBoxIncludeAssets>false</CKBoxIncludeAssets> </PropertyGroup>Build your project to download and prepare assets:
dotnet buildRegister CKEditor with cloud preset in
Program.cs:using CKEditor.Blazor.Model.Cloud; using CKEditor.Blazor.Services; builder.Services.AddCKEditor(options => options .SetLicenseKey("your-license-key-here") .ExtendDefaultPreset(preset => preset .WithCloud(new CloudConfig { EditorVersion = "48.1.0", Premium = false })));If your app is served from a non-standard base path (e.g. behind a reverse proxy), you can customize the path to the internal Blazor integration script by setting
IntegrationBasePath(default:/_content/CKEditor.Blazor). Because this setup uses a CDN, there is noAssetsBasePathparameter since the CKEditor 5 assets are loaded externally.Add cloud assets component in
<head>:@using CKEditor.Blazor.Components.Assets @using CKEditor.Blazor.Model.License <HeadContent> <CKE5Assets Distribution="DistributionChannel.Cloud" /> </HeadContent>Use editor components anywhere in your Razor UI:
@using CKEditor.Blazor.Components <CKE5Editor Value="@("<p>Hello world!</p>")" />
That's it! ๐
Basic Usage ๐
Get started with the most common usage pattern. This example shows how to render an editor in Razor and keep content synced with .NET state.
Simple Editor โ๏ธ
Create a basic editor with default toolbar and plugins.
@using CKEditor.Blazor.Components
@using CKEditor.Blazor.Model
<CKE5Editor
EditorType="EditorType.Classic"
Value="@("<p>Initial content</p>")"
EditableHeight="300"
@bind-Value="content" />
@code {
private EditorValue content = "<p>Initial content</p>";
}
EditorValue can be initialized from a plain string (mapped to the main root) or from a Dictionary<string, string> for multi-root editors, where each key is a root name:
@using CKEditor.Blazor.Components
@using CKEditor.Blazor.Model
<CKE5Editor
EditorType="EditorType.Multiroot"
@bind-Value="content" />
@code {
private EditorValue content = new Dictionary<string, string>
{
["header"] = "<p>Header content</p>",
["main"] = "<p>Main content</p>",
["footer"] = "<p>Footer content</p>"
};
}
All available parameters ๐
All parameters accepted by <CKE5Editor> with short inline comments. Copy and remove what you don't need.
@using CKEditor.Blazor.Components
@using CKEditor.Blazor.Model
@using CKEditor.Blazor.Model.Events
@using Microsoft.JSInterop
<CKE5Editor
@* --- Content --- *@
@bind-Value="content"
OnChange="HandleChange"
@* --- Identity & Appearance --- *@
Id="my-editor"
Class="my-editor-class"
Style="display:block;width:100%"
EditableHeight="300"
@* --- Editor Type & Configuration --- *@
EditorType="EditorType.Classic"
Preset="default"
Config="editorConfig"
MergeConfig="editorMergeConfig"
@* --- Localization --- *@
Language="en"
CustomTranslations="editorTranslations"
@* --- Forms & Context --- *@
Name="body"
Required="true"
ContextId="shared-context"
@* --- Behavior & Performance --- *@
SaveDebounceMs="300"
Watchdog="true"
Interactive="false"
RootAttributes="editorRootAttributes"
@* --- Lifecycle Events --- *@
OnReady="HandleReady"
OnFocus="HandleFocus"
OnBlur="HandleBlur"
OnImageUpload="HandleImageUpload"
/>
@code {
private EditorValue content = "<p>Hello world!</p>";
// Shallow-replaces the default configuration
private Dictionary<string, object> editorConfig = new()
{
["plugins"] = new[] { "Essentials", "Paragraph", "Bold", "Italic", "Undo" },
["toolbar"] = new Dictionary<string, object>
{
["items"] = new[] { "bold", "italic", "|", "undo", "redo" }
}
};
// Deep-merges with the default configuration
private Dictionary<string, object> editorMergeConfig = new()
{
["menuBar"] = new Dictionary<string, object> { ["isVisible"] = true }
};
// UI text overrides for specific languages
private EditorTranslations editorTranslations = new()
{
["en"] = new Dictionary<string, string> { ["Bold"] = "Strong" }
};
// ARIA / data-* attributes for the main editable element
private EditorRootAttributes editorRootAttributes = new()
{
["aria-label"] = "Article body",
["data-testid"] = "editor-root"
};
private async Task HandleChange(CKE5EditorChangeEventArgs args)
{
Console.WriteLine($"Content changed: {args.Value}");
await Task.CompletedTask;
}
private async Task HandleReady(IJSObjectReference editor)
{
Console.WriteLine("Editor ready");
await Task.CompletedTask;
}
private async Task HandleFocus(IJSObjectReference editor)
{
Console.WriteLine("Editor focused");
await Task.CompletedTask;
}
private async Task HandleBlur(IJSObjectReference editor)
{
Console.WriteLine("Editor blurred");
await Task.CompletedTask;
}
private async Task<string?> HandleImageUpload(CKE5ImageUploadEventArgs args)
{
// Upload args.Data (Base64) to your server/storage here
// Return the public image URL, or 'null' to reject the upload.
return $"https://cdn.example.com/images/{args.FileName}";
}
}
You rarely need every parameter at once. Most editors only need @bind-Value and optionally EditorType, EditableHeight, and Preset. All other parameters have sensible defaults.
Static rendering with Interactive=true ๐งฑ
By default, components initialize through Blazor .NET interop callbacks. If your page is rendered in a non-interactive/static mode, those callbacks are not available.
Set Interactive="true" to let CKEditor web components bootstrap directly in the browser without .NET interop initialization:
@using CKEditor.Blazor.Model
<CKE5Editor
EditorType="EditorType.Classic"
Interactive="true"
Value="@("<p>Static page content</p>")" />
This is useful when rendering static pages where you still want the editor UI to initialize on the client.
Configuration โ๏ธ
You can configure editor presets in AddCKEditor(...). The default preset is default. Presets are reusable configuration objects that can be applied to any editor instance. You can also define custom presets and override the default one.
Override default preset configuration ๐งโ๐ป
You can pass initial content and merge additional configuration. In scenario below, the MergeConfig will extend the default preset configuration to make the menu bar visible. It's only shallow merge, so nested arrays will be replaced, not merged.
<CKE5Editor
Value="<p>This is the initial content of the editor.</p>"
MergeConfig="@(new Dictionary<string, object>
{
["menuBar"] = new Dictionary<string, object>
{
["isVisible"] = true
}
})" />
Alternatively, you can extend the default configuration directly in Program.cs when registering services:
builder.Services.AddCKEditor(options =>
options.ExtendDefaultPreset(preset => preset
.WithMergedConfig(new Dictionary<string, object>
{
["menuBar"] = new Dictionary<string, object>
{
["isVisible"] = true
}
})));
Define your configuration directly in the view ๐ป
Override the default configuration with custom plugins and toolbar items. In this example, the editor will only have Essentials, Paragraph, Bold, Italic, Link, and Undo plugins, and the toolbar will contain only bold, italic, link, undo, and redo buttons. The editor locale is set to Polish (pl), and a custom translation for the "Bold" label is provided.
<CKE5Editor
Language="@("pl")"
Config="@(new Dictionary<string, object>
{
["plugins"] = new[] { "Essentials", "Paragraph", "Bold", "Italic", "Link", "Undo" },
["toolbar"] = new Dictionary<string, object>
{
["items"] = new[] { "bold", "italic", "link", "|", "undo", "redo" }
}
})" />
In order to specify the UI and Content language separately, use the Language object:
@using CKEditor.Blazor.Model
<CKE5Editor Language="@(new Language { UI = "pl", Content = "en" })" />
Preset DSL ๐ ๏ธ
If you prefer strongly typed, fluent configuration over raw dictionaries, build presets with PresetConfig and register them through CKEditorOptions:
using CKEditor.Blazor.Model;
using CKEditor.Blazor.Services;
builder.Services.AddCKEditor(options => options
.SetLicenseKey("GPL")
.AddDefaultPreset(preset => preset
.WithEditorType(EditorType.Classic)
.WithPlugins("Essentials", "Paragraph", "Bold", "Italic", "Undo")
.WithToolbar("bold", "italic", Toolbar.Separator, "undo", "redo")
.WithLanguage("pl")
.WithCustomTranslations("pl", new Dictionary<string, string>
{
["Bold"] = "Pogrubienie"
})));
Define reusable configuration presets ๐งฉ
In order to override the default preset or add custom presets, use the fluent AddPreset helper:
using CKEditor.Blazor.Model;
using CKEditor.Blazor.Services;
builder.Services.AddCKEditor(options => options
.AddPreset("minimal", preset => preset
.WithEditorType(EditorType.Classic)
.WithPlugins("Essentials", "Paragraph", "Bold", "Italic", "Undo")
.WithToolbar("bold", "italic", Toolbar.Separator, "undo", "redo")));
Use it in Razor:
<CKE5Editor Preset="@("minimal")" Value="<p>Simple editor</p>" />
Dynamic presets ๐ฏ
You can also create dynamic presets that can be modified at runtime. This is useful if you want to change the editor configuration based on user input or other conditions.
@using CKEditor.Blazor.Model
@using CKEditor.Blazor.Services
<CKE5Editor Preset="@dynamicPreset" Value="<p>Runtime preset</p>" />
@code {
private readonly PresetConfig dynamicPreset = ConfigManager.CreateDefaultPreset()
.WithToolbar("bold", "italic", "link", Toolbar.Separator, "undo", "redo");
}
Element references using $element ๐ฏ
Similarly to translation references, configuration objects may reference DOM elements by CSS selector. Use PresetConfig.ElementSelector anywhere in your editor configuration where CKEditor expects an HTMLElement, and the package will resolve it to the matching DOM element during initialization (serializes to { "$element": "selector" }).
This is useful, for example, when pointing a plugin to an external container element:
using CKEditor.Blazor.Model;
using CKEditor.Blazor.Services;
builder.Services.AddCKEditor(options => options
.ExtendDefaultPreset(preset => preset
.WithConfigEntry("myPlugin", new Dictionary<string, object>
{
["container"] = PresetConfig.ElementSelector("#my-container")
})));
If no element matching the selector is found in the DOM, a warning is printed and null is used instead.
Providing the License Key ๐๏ธ
CKEditor 5 requires a license key for official CDN and premium features.
Environment variable (recommended for production):
export CKEditor__DefaultLicenseKey="your-license-key-here"Programmatic config in
Program.cs:builder.Services.AddCKEditor(options => options .SetLicenseKey("your-license-key-here"));
If you use CKEditor 5 under GPL, use GPL as your key value.
Localization ๐
Support multiple languages in the editor UI and content. Configure translation loading, custom dictionaries, and reuse translation keys or DOM element references across your configuration.
Translation Loading ๐
For self-hosted setups, translation assets are handled by your bundler automatically. For cloud setups, translations are loaded through the configured CDN bundle. In both cases, set the UI language per editor or context:
<CKE5Editor
Language="@("pl")"
Value="<p>Treลฤ z polskim UI</p>" />
@* or *@
<CKE5Editor
Language="@(new Language { UI = "pl", Content = "en" })"
Value="<p>Polish UI, English content</p>" />
Global Translation Config ๐ ๏ธ
Set default language and translated labels in your preset configuration:
builder.Services.AddCKEditor(options => options
.ExtendDefaultPreset(preset => preset
.WithLanguage("pl")
.WithCustomTranslations("pl", new Dictionary<string, string>
{
["Bold"] = "Pogrubienie",
["Italic"] = "Kursywa",
["Link"] = "Link",
["Undo"] = "Cofnij",
["Redo"] = "Ponรณw"
})));
Custom translations ๐
You can override translations per editor instance via CustomTranslations:
@using CKEditor.Blazor.Model
<CKE5Editor
Value="<p>Custom labels</p>"
CustomTranslations="@(new EditorTranslations
{
["pl"] = new Dictionary<string, string>
{
["Bold"] = "Pogrubienie (custom)"
}
})" />
Translation references using $translation โจ
In addition to supplying full translation maps, configuration objects may contain reference helpers that point to existing translation keys. This is particularly handy when you want to reuse an existing label or avoid repeating the same string in multiple places. Use PresetConfig.TranslationReference in any part of your editor or context configuration, and the package will automatically replace it with the correct localized string during initialization (serializes to { "$translation": "key" }).
using CKEditor.Blazor.Model;
using CKEditor.Blazor.Services;
builder.Services.AddCKEditor(options => options
.ExtendDefaultPreset(preset => preset
.WithCustomTranslations("pl", new Dictionary<string, string>
{
["Bold"] = "Pogrubienie"
})
.WithConfigEntry("myPlugin", new Dictionary<string, object>
{
["buttonLabel"] = PresetConfig.TranslationReference("Bold")
})));
When the editor or context is created, the helper will be resolved against the loaded translations (including any custom translations you provided). If the key is not found, a warning is printed and null will be used instead.
Editor Types ๐๏ธ
CKEditor 5 for Blazor supports four distinct editor types, each designed for specific use cases. Choose the one that best fits your application's layout and functionality requirements.
Classic editor ๐
Traditional WYSIWYG editor with a fixed toolbar above the editing area. Best for standard content editing scenarios like blog posts, articles, or forms.
@using CKEditor.Blazor.Model
<CKE5Editor
EditorType="EditorType.Classic"
Value="@("<p>This is the initial content of the editor.</p>")"
EditableHeight="300" />
Inline editor ๐
Minimalist editor that appears directly within content when clicked. Ideal for in-place editing scenarios where the editing interface should be invisible until needed.
@using CKEditor.Blazor.Model
<CKE5Editor
EditorType="EditorType.Inline"
Value="@("<p>Inline editor content</p>")"
Class="border border-gray-300" />
Inline editors don't work with <textarea> elements and may not be suitable for traditional form scenarios.
Balloon editor ๐
Contextual editor that shows a floating toolbar near the selected text. Great for comment editing, annotations, or any scenario where a non-intrusive editing experience is desired.
@using CKEditor.Blazor.Model
<CKE5Editor
EditorType="EditorType.Balloon"
Value="<p>Balloon editor content</p>"
Class="border border-gray-300" />
Decoupled editor ๐
Flexible editor where toolbar and editing area are completely separated. Provides maximum layout control for custom interfaces and complex applications.
@using CKEditor.Blazor.Model
<CKE5Editor EditorType="EditorType.Decoupled">
<CKE5UIPart Name="toolbar" Class="mb-4" />
<CKE5Editable
RootName="main"
Value="<p>This is the initial content of the decoupled editor editable.</p>"
InnerClass="p-4" />
</CKE5Editor>
EditorId is passed down automatically to CKE5Editable and CKE5UIPart components via cascading parameters if they are placed inside the <CKE5Editor>. If placed outside, you must manually set EditorId on each of them to link them to the specific editor. Otherwise, all editables will bind to the first editor instance found in the DOM.
Multiroot editor ๐ณ
Advanced editor supporting multiple separate editing areas (roots) with a shared toolbar. Perfect for complex documents with multiple editable sections like headers, sidebars, and main content.
You can set the content for all roots at once via the <CKE5Editor> component using a dictionary. Both Value and @bind-Value are supported (see Blazor Data Binding).
@using CKEditor.Blazor.Model
<CKE5Editor
EditorType="EditorType.Multiroot"
Value="@(new Dictionary<string, string>
{
["header"] = "<p>Header content</p>",
["content"] = "<p>Main content</p>",
["footer"] = "<p>Footer content</p>"
})">
<CKE5UIPart Name="toolbar" Class="mb-4" />
<CKE5Editable RootName="header" />
<CKE5Editable RootName="content" />
<CKE5Editable RootName="footer" />
</CKE5Editor>
Alternatively, you can provide the initial Value or use two-way binding (@bind-Value) directly on the individual editable components (see Multiroot Editables binding):
@using CKEditor.Blazor.Model
<CKE5Editor EditorType="EditorType.Multiroot">
<CKE5UIPart Name="toolbar" Class="mb-4" />
<CKE5Editable RootName="header" Value="<p>Header content</p>" />
<CKE5Editable RootName="content" Value="<p>Main content</p>" />
<CKE5Editable RootName="footer" Value="<p>Footer content</p>" />
</CKE5Editor>
EditorId is passed down automatically to CKE5Editable and CKE5UIPart components via cascading parameters if they are placed inside the <CKE5Editor>. If placed outside, you must manually set EditorId on each of them to link them to the specific editor. Otherwise, all editables will bind to the first editor instance found in the DOM.
Advanced configuration โ๏ธ
Blazor Data Binding ๐
Use native Blazor binding and callbacks for full client โ server synchronization.
Two way binding using @bind-Value โ๏ธ
Bind editor content to your component state. After each typed character, the editor updates the bound value with the current content. The SaveDebounceMs parameter allows you to control the debounce delay for content updates, preventing excessive updates on every keystroke. Keep in mind that SaveDebounceMs only affects the frequency of updates when editor is focused and typing is happening. If you programmatically update the bound value from .NET (e.g. loading a template), the changes will be pushed to the editor immediately without any debounce.
<CKE5Editor
@bind-Value="content"
SaveDebounceMs="500" />
@code {
private EditorValue content = "<p>Initial content</p>";
}
Multiroot Editables ๐ณโ๏ธ
For multiroot/decoupled layouts, you can bind each editable separately by placing @bind-Value on the individual CKE5Editable components.
<CKE5Editable RootName="header" @bind-Value="header" />
<CKE5Editable RootName="content" @bind-Value="content" />
@code {
private string header = "<p>Header</p>";
private string content = "<p>Main</p>";
}
EditorId is passed down automatically to CKE5Editable and CKE5UIPart components via cascading parameters if they are placed inside the <CKE5Editor>. If placed outside, you must manually set EditorId on each of them to link them to the specific editor. Otherwise, all editables will bind to the first editor instance found in the DOM.
Bidirectional Communication using Events ๐
Editor โ .NET: Content Change Event ๐ค
Observe content updates without replacing your binding logic:
<CKE5Editor
@bind-Value="value"
OnChange="OnEditorChange" />
@code {
private EditorValue value = "<p>Hello</p>";
private void OnEditorChange(CKE5EditorChangeEventArgs args)
{
Console.WriteLine(args.Value.Data["main"]);
}
}
.NET โ Editor: Set Content ๐ฅ
Update bound value from C# and editor content is pushed automatically:
<button @onclick="LoadTemplate">Load template</button>
<CKE5Editor @bind-Value="value" />
@code {
private EditorValue value = "<p>Initial</p>";
private void LoadTemplate()
=> value = "<h2>Work Report</h2><p>This is a template loaded from .NET.</p>";
}
Editor Ready Event โ
An event is fired when the editor has finished initializing and is fully ready. This can be useful for triggering UI updates, focusing related components, or performing any logic that must wait until the editor is available.
@using Microsoft.JSInterop
<CKE5Editor OnReady="OnReady" />
@code {
private void OnReady(IJSObjectReference editor)
{
Console.WriteLine("Editor is ready");
}
}
Focus Tracking ๐๏ธ
You can track editor focus state using OnFocus and OnBlur events. This is useful for UI adjustments or validation logic based on whether the editor is active.
<CKE5Editor
OnFocus="() => isFocused = true"
OnBlur="() => isFocused = false" />
@code {
private bool isFocused;
}
Root Attributes ๐ท๏ธ
RootAttributes lets you attach custom key-value pairs to the editor root model element. This is useful for storing metadata in roots, which can be accessed by custom plugins or used for testing and debugging purposes. For more details on root attributes in CKEditor 5, see the official engine documentation. Long story short - they are not DOM / HTML attributes, but rather a dictionary of arbitrary data attached to the root element in the editor model.
Example of setting root attributes on a single-root editor:
@using CKEditor.Blazor.Model
<CKE5Editor
Value="@("<p>Hello</p>")"
RootAttributes="@(new EditorRootAttributes
{
["custom-model-attribute"] = "my-value"
})" />
In multiroot/decoupled layouts each CKE5Editable can carry its own set of root attributes, allowing you to annotate every root independently:
@using CKEditor.Blazor.Model
<CKE5Editor EditorType="EditorType.Multiroot">
<CKE5UIPart Name="toolbar" Class="mb-4" />
<CKE5Editable
RootName="header"
Value="<p>Header</p>"
RootAttributes="@(new EditorRootAttributes
{
["custom-model-attribute"] = "header-root"
})" />
<CKE5Editable
RootName="content"
Value="<p>Main content</p>"
RootAttributes="@(new EditorRootAttributes
{
["custom-model-attribute"] = "content-root",
["other-attribute"] = "another-value"
})" />
<CKE5Editable
RootName="footer"
Value="<p>Footer</p>"
RootAttributes="@(new EditorRootAttributes
{
["custom-model-attribute"] = "footer-root",
["other-attribute"] = "another-value"
})" />
</CKE5Editor>
Attributes are reactive - updating RootAttributes from .NET after initialization will push the changes to the live editor root without a full re-render.
RootAttributes serializes to JSON and is passed to the CKEditor 5 root via the data-cke-root-attributes HTML attribute. Empty dictionaries are treated as absent โ no attribute is emitted.
Image Upload ๐ผ๏ธ
The editor supports image uploads triggered by drag-and-drop, clipboard paste, or the toolbar image button.
Behavior depends on whether the OnImageUpload callback is set:
- With
OnImageUpload- the file is encoded as Base64 and passed to your .NET handler. Your handler stores it wherever you like (disk, cloud, database) and returns the public URL to embed in the document. - Without
OnImageUpload- the editor falls back to embedding the image as a Base64data:URI directly in the content. This is fine for quick prototyping but not recommended for production because it significantly inflates document size.
@using CKEditor.Blazor.Components
@using CKEditor.Blazor.Model
@using CKEditor.Blazor.Model.Events
<CKE5Editor
@bind-Value="content"
OnImageUpload="HandleImageUpload" />
@code {
private EditorValue content = "<p>Drop an image here.</p>";
private async Task<string?> HandleImageUpload(CKE5ImageUploadEventArgs args)
{
// args.FileName โ original file name, e.g. "photo.jpg"
// args.MimeType โ MIME type, e.g. "image/jpeg"
// args.Payload โ Base64-encoded file content
var bytes = Convert.FromBase64String(args.Payload);
// Save to your storage and return the public URL:
var url = await MyStorageService.SaveAsync(args.FileName, args.MimeType, bytes);
return url; // CKEditor 5 embeds this URL in the document
}
}
The OnImageUpload callback must be set on a server-interactive component (@rendermode InteractiveServer or InteractiveWebAssembly). It will not be invoked in static rendering mode.
Watchdog ๐ถ
By default, the editor is wrapped in a watchdog that automatically tries to recover from crashes by reinitializing the editor instance. This ensures a more resilient user experience, especially in cases where custom plugins or configurations might cause instability.
Configuring the watchdog โ๏ธ
You can customize the watchdog's behaviorโsuch as the maximum number of restarts before it stops trying to recoverโby passing a dictionary to the WithWatchdogConfig method in your preset configuration.
Here is an example of how to configure the watchdog to limit the number of restarts to 2, alongside loading custom plugins and adjusting self-hosted assets:
builder.Services.AddCKEditor(options => options
.ExtendDefaultPreset(p => p
.WithWatchdogConfig(new Dictionary<string, object>
{
["crashNumberLimit"] = 2
})));
Disabling the watchdog ๐ซ
In some scenarios, such as when using a highly customized editor setup or when you want to handle errors manually, you might want to disable the watchdog. You can do this by setting the Watchdog parameter to false on the CKE5Editor component:
<CKE5Editor Watchdog="false" />
Splitting Assets: Global Import Map with Per-page Styles ๐บ๏ธ
The typical setup places <CKE5Assets Distribution="..." /> in the shared <head> of your layout. This works perfectly when most routes use the editor.
If your app only uses the editor on specific pages, loading the import map globally is fine, but stylesheets add unnecessary overhead on pages without the editor. You can solve this by splitting the assets.
Place <CKE5Importmap Distribution="..." /> in your shared layout <head>, as the import map must appear before any scripts that use it. Then, place <CKE5Assets Distribution="..." EmitImportMap="false" /> only on pages that actually render the editor. This ensures stylesheets and preload hints are loaded only when needed.
Self-hosted variant ๐
App.razor (shared layout <head>):
@using CKEditor.Blazor.Components.Assets
@using CKEditor.Blazor.Model.License
<HeadContent>
@* Declared once globally. No stylesheets, no preload hints. *@
<CKE5Importmap Distribution="DistributionChannel.SH" />
</HeadContent>
Page that uses the editor:
@using CKEditor.Blazor.Components
@using CKEditor.Blazor.Components.Assets
@using CKEditor.Blazor.Model.License
@* Load stylesheets only on this page. *@
<CKE5Assets Distribution="DistributionChannel.SH" EmitImportMap="false" />
<CKE5Editor Value="@("<p>Hello!</p>")" />
CDN variant ๐ก
App.razor (shared layout <head>):
@using CKEditor.Blazor.Components.Assets
@using CKEditor.Blazor.Model.License
<HeadContent>
@* Declared once globally. No stylesheets, no preload hints. *@
<CKE5Importmap Distribution="DistributionChannel.Cloud" />
</HeadContent>
Page that uses the editor:
@using CKEditor.Blazor.Components
@using CKEditor.Blazor.Components.Assets
@using CKEditor.Blazor.Model.License
@* Load stylesheets only on this page. *@
<CKE5Assets Distribution="DistributionChannel.Cloud" EmitImportMap="false" />
<CKE5Editor Value="@("<p>Hello!</p>")" />
Both CKE5Importmap and CKE5Assets accept the same Preset, Nonce, and CustomImportMap parameters.
Disabling module preload hints โณ
By default the per-page component still emits <link rel="modulepreload"> hints, which tell the browser to fetch ESM chunks early. If you want to opt out of that too (e.g. to reduce <head> size on low-traffic pages), add EmitModulePreload="false":
<CKE5Assets Distribution="DistributionChannel.Cloud" EmitImportMap="false" EmitModulePreload="false" />
@* or *@
<CKE5Assets Distribution="DistributionChannel.SH" EmitImportMap="false" EmitModulePreload="false" />
Context ๐ค
The context feature is designed to group multiple editor instances together, allowing them to share a common context. This is particularly useful in collaborative editing scenarios, where users can work together in real time. By sharing a context, editors can synchronize features such as comments, track changes, and presence indicators across different editor instances. This enables seamless collaboration and advanced workflows in your Phoenix application.
Basic usage ๐ง
<CKE5Context Id="shared-context">
@* ContextId is inferred automatically for nested editors *@
<CKE5Editor Value="<p>Editor 1 content</p>" />
<CKE5Editor Value="<p>Editor 2 content</p>" />
</CKE5Context>
@* Editors outside the context can reference it by Id to share the same context *@
<CKE5Editor ContextId="shared-context" Value="<p>Editor 3 content</p>" />
Custom context config ๐
Pass a context config object directly using ContextPreset:
@using CKEditor.Blazor.Model
<CKE5Context
ContextPreset="@(new ContextConfig
{
Plugins = new List<string> { "Essentials", "Paragraph" },
Config = new Dictionary<string, object>
{
["language"] = "pl"
}
})">
<CKE5Editor Value="@("<p>Shared context</p>")" />
</CKE5Context>
Context config DSL ๐ ๏ธ
ContextConfig exposes the same fluent builder API as PresetConfig, so you can compose context configuration without working directly with raw collections:
using CKEditor.Blazor.Model;
using CKEditor.Blazor.Services;
builder.Services.AddCKEditor(options => options
.ExtendDefaultContext(context => context
.AddPlugins(Plugin.Import("MyCustomPlugin", "./my-custom-plugin.js")))
.AddContext("shared", context => context
.WithPlugins("Essentials", "Paragraph", "Bold")
.WithLanguage("pl")
.WithConfigEntry("toolbar", new[] { "bold" })));
You can also build a context inline in Razor using the same API:
@using CKEditor.Blazor.Model
<CKE5Context
Id="shared-context"
ContextPreset="@(new ContextConfig()
.WithPlugins("Essentials", "Paragraph")
.WithLanguage("pl"))">
<CKE5Editor ContextId="shared-context" Value="@("<p>Shared context</p>")" />
</CKE5Context>
Custom plugins ๐งฉ
There are two ways to register a custom plugin, depending on whether you have a JavaScript bundle in your app.
Import from a JS module ๐ฆ
If you don't have a custom JavaScript bundle, point the editor directly at your plugin file using Plugin.Import in Blazor. No extra JavaScript setup is needed โ the editor will load the module on demand.
using CKEditor.Blazor.Model;
using CKEditor.Blazor.Services;
builder.Services.AddCKEditor(options => options
.ExtendDefaultPreset(preset => preset
.AddPlugins(Plugin.Import("MyCustomPlugin", "./my-custom-plugin.js"))));
The module at ./my-custom-plugin.js must export the plugin class as its default export:
// my-custom-plugin.js
import { Plugin } from 'ckeditor5';
export default class MyCustomPlugin extends Plugin {
static get pluginName() {
return 'MyCustomPlugin';
}
init() {
console.log('MyCustomPlugin initialized');
}
}
Register in a JS bundle ๐๏ธ
If your app already has a JavaScript bundle that runs before the editor, you can register plugins there using CustomEditorPluginsRegistry. The plugin must be registered before the editor initializes.
import { CustomEditorPluginsRegistry as Registry } from 'ckeditor5-blazor';
const unregister = Registry.the.register('MyCustomPlugin', async () => {
const { Plugin } = await import('ckeditor5');
return class extends Plugin {
static get pluginName() {
return 'MyCustomPlugin';
}
init() {
console.log('MyCustomPlugin initialized');
}
};
});
Then reference the plugin by name in your Blazor config:
using CKEditor.Blazor.Services;
builder.Services.AddCKEditor(options => options
.ExtendDefaultPreset(preset => preset
.AddPlugins("MyCustomPlugin")));
Editors and Contexts registry ๐
The package provides two registries: EditorsRegistry and ContextsRegistry. They allow you to watch for changes in registered editors and contexts, get instances directly, or execute logic when a specific editor or context appears.
mountEffect(id, callback)โ executes logic whenever the editor is initialized or restarted. This is the recommended way to implement integrations that must be re-initialized throughout the editor's lifecycle.import { EditorsRegistry } from 'ckeditor5-blazor'; EditorsRegistry.the.mountEffect('editor1', (editor) => { const watcher = () => { console.info('Changed data:', editor.getData()); }; editor.model.document.on('change:data', watcher); // Cleanup: This will be executed when the editor is unmounted. return () => { editor.model.document.off('change:data', watcher); }; });watch(callback)โ react whenever registry state changes.import { EditorsRegistry } from 'ckeditor5-blazor'; const unregisterWatcher = EditorsRegistry.the.watch((editors) => { console.log('Registered editors changed:', editors); }); // Later, you can unregister the watcher unregisterWatcher();waitFor(id)โ get the instance directly. If it is already registered, the promise resolves immediately.import { EditorsRegistry } from 'ckeditor5-blazor'; EditorsRegistry.the.waitFor('editor1').then((editor) => { console.log('Editor "editor1" is registered:', editor); }); // ... init editor somewhere laterexecute(id, callback)โ run logic immediately if the instance already exists, or later when it appears.import { EditorsRegistry } from 'ckeditor5-blazor'; EditorsRegistry.the.execute('editor1', (editor) => { console.log('Current data:', editor.getData()); });The same methods are available on
ContextsRegistryfor shared contexts:import { ContextsRegistry } from 'ckeditor5-blazor'; ContextsRegistry.the.waitFor('shared-context').then((watchdog) => { console.log('Context is ready:', watchdog.context); }); ContextsRegistry.the.execute('shared-context', (watchdog) => { console.log('Context state:', watchdog.state); });
Development โ๏ธ
To start the development environment, run:
pnpm install
pnpm run dotnet:install-local-package # It'll fetch CKEditor 5 package
pnpm run dev
The playground app will be available at http://localhost:5175.
Running Tests ๐งช
Run JavaScript package tests:
pnpm run npm_package:test
Run .NET tests with coverage report:
pnpm run dotnet:test
Running E2E tests ๐งช
Make sure the development environment is running (pnpm run dev), then execute:
pnpm run dotnet:e2e:headed
Psst... ๐
If you're looking for similar stuff, check these out:
ckeditor5-phoenix Seamless CKEditor 5 integration for Phoenix Framework. Plug & play support for LiveView forms with dynamic content, localization, and custom builds.
ckeditor5-rails Smooth CKEditor 5 integration for Ruby on Rails. Works with standard forms, Turbo, and Hotwire. Easy setup, custom builds, and localization support.
ckeditor5-symfony Native CKEditor 5 integration for Symfony. Works with Symfony 6.x+, standard forms and Twig. Supports custom builds, multiple editor configurations, asset management, and localization. Designed to be simple, predictable, and framework-native.
ckeditor5-livewire CKEditor 5 integration for Laravel Livewire. Real-time syncing, custom builds, localization, and easy setup.
Trademarks ๐
CKEditorยฎ is a trademark of CKSource Holding sp. z o.o. All rights reserved. For more information about the license of CKEditorยฎ please visit CKEditor's licensing page.
This package is not owned by CKSource and does not use the CKEditorยฎ trademark for commercial purposes. It should not be associated with or considered an official CKSource product.
License ๐
This project is licensed under the terms of the MIT LICENSE.
This project injects CKEditor 5 which is licensed under the terms of GNU General Public License Version 2 or later. For more information about CKEditor 5 licensing, please see their official documentation.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net8.0
- Microsoft.AspNetCore.Components.Web (>= 8.0.23)
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
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.11.1 | 102 | 5/18/2026 |
| 1.11.0 | 92 | 5/17/2026 |
| 1.10.3 | 129 | 4/24/2026 |
| 1.10.2 | 96 | 4/23/2026 |
| 1.10.1 | 110 | 4/21/2026 |
| 1.10.0 | 86 | 4/21/2026 |
| 1.9.1 | 143 | 3/17/2026 |
| 1.9.0 | 120 | 3/17/2026 |
| 1.8.0 | 126 | 3/15/2026 |
| 1.7.1 | 136 | 3/13/2026 |
| 1.7.0 | 131 | 3/13/2026 |
| 1.6.0 | 135 | 3/13/2026 |
| 1.5.0 | 171 | 3/11/2026 |
| 1.4.0 | 215 | 3/8/2026 |
| 1.3.0 | 215 | 3/8/2026 |
| 1.2.1 | 251 | 3/6/2026 |
| 1.2.0 | 532 | 3/2/2026 |
| 1.1.0 | 603 | 3/1/2026 |
| 1.0.4 | 596 | 3/1/2026 |
| 1.0.3 | 593 | 3/1/2026 |