| menu-title | SharePoint (SPFx) |
|---|---|
| meta-title | Using the CKEditor 5 WYSIWYG editor with SharePoint SPFx | CKEditor 5 Documentation |
| meta-description | Integrate the CKEditor 5 rich-text editor with SharePoint SPFx using npm. Follow step-by-step instructions for fast installation and setup. |
| category | self-hosted |
| order | 120 |
| modified_at | 2026-01-28 |
You can integrate the CKEditor 5 rich-text editor into SharePoint Framework (SPFx) solutions by installing the CKEditor 5 npm packages and using the editor inside an SPFx web part.
Before integrating CKEditor 5 rich-text editor with SharePoint, make sure you have the following installed:
- Node.js (LTS version)
- Yeoman generator and SharePoint generator plugin:
npm install -g yo @microsoft/generator-sharepoint- Gulp (for building the project):
npm install -g gulpFirst, create a new folder and navigate into it:
mkdir my-spfx-app
cd my-spfx-appGenerate a new SPFx web part project:
yo @microsoft/sharepointWhen prompted for:
- Which type of client-side component to create? – choose WebPart
- Which template would you like to use? – choose React
You will be also prompted for names for the application and the web part, for example:
{@img assets/img/sharepoint-integration_01.png Screenshot of prompts from SharePoint app generator.}
After the project is created, go to config/serve.json file and update the initialPage property with your SharePoint tenant URL:
"initialPage": "https://your-tenant.sharepoint.com/_layouts/workbench.aspx"At this point you should be able to run the project by executing:
gulp serveA new browser tab with SPFx app should open automatically. You might be prompted to log in to your Microsoft account at this point.
If the page does not open automatically, navigate to the following URL:
https://your-tenant.sharepoint.com/_layouts/workbench.aspx
If everything went fine, you should see a screen similar to this one: {@img assets/img/sharepoint-integration_02.png Screenshot of SharePoint application.}
Now it is time to add CKEditor 5 to the application. First, install CKEditor 5 and React integration packages:
npm install --save @ckeditor/ckeditor5-react ckeditor5 ckeditor5-premium-featuresIn tsconfig.json file add:
...
"allowSyntheticDefaultImports": true
...Assuming you named your web part "RichTextEditor", open the src/webparts/richTextEditor/components/RichTextEditor.tsx file and modify it to include CKEditor 5:
import * as React from 'react';
import type { IRichTextEditorProps } from './IRichTextEditorProps';
import { CKEditor } from '@ckeditor/ckeditor5-react';
import {
ClassicEditor,
Bold,
Essentials,
Italic,
Paragraph,
Font,
Heading,
Table,
List,
TableCellProperties,
TableProperties,
TableToolbar,
Autoformat
} from 'ckeditor5';
import { FormatPainter } from 'ckeditor5-premium-features';
import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';
export default class RichTextEditor extends React.Component<IRichTextEditorProps> {
public render(): React.ReactElement<IRichTextEditorProps> {
return (
<CKEditor
editor={ ClassicEditor }
config={ {
licenseKey: '<YOUR_LICENSE_KEY>',
plugins: [
Bold, Essentials, Italic, Paragraph, Font, Heading,
Table, TableCellProperties, TableProperties, TableToolbar, List,
Autoformat, FormatPainter
],
toolbar: [
'undo', 'redo', '|', 'bold', 'italic', '|',
'fontFamily', 'fontSize', 'fontColor', 'fontBackgroundColor', '|',
'formatPainter', 'insertTable', 'bulletedList', 'numberedList'
],
root: {
initialData: '<p>Hello from CKEditor 5 in SPFX React app!</p>'
},
table: {
contentToolbar: [
'tableColumn', 'tableRow', 'mergeTableCells',
'tableProperties', 'tableCellProperties'
]
}
} }
/>
);
}
}Restart the server and then refresh the page.
gulp serveYou should be now able to add CKEditor 5 HTML editor to your SharePoint application by clicking the plus icon on the main page:
{@img assets/img/sharepoint-integration_03.png Screenshot of SharePoint application.}
Then choose CKEditor 5 component:
{@img assets/img/sharepoint-integration_04.png Screenshot of SharePoint application.}
You should now be able to use the rich-text editor with SharePoint:
{@img assets/img/sharepoint-integration_05.png Screenshot of SharePoint application.}
- When using Rooster JS React in SharePoint with the SPFx Framework, after inserting a link, the Enter key may stop working. Adding
data-sp-a11y-skipkeys='all'in the div wrapping the editor should fix the problem. (See issue). - When using CKEditor 5 inside a Fluent UI
DialogorModalcomponent, image resizing handles might not work because theLayercomponent captures mouse events. To fix this, enable event bubbling in themodalProps:
<Dialog
modalProps={ {
layerProps: {
// Enable event bubbling to allow CKEditor 5 resize handles to work.
eventBubblingEnabled: true
}
} }
>
{/* ... */}
</Dialog>- SharePoint overrides the table border styles. This is a problem since SharePoint uses the following style:
.ck-content .table > table {
border: unset !important;
}The !important rule overrides any styles generated by the editor, so setting them through table properties has no effect. To fix it, you need to make sure CKEditor 5's internal styles take precedence before SharePoint's global styles. This can be achieved by introducing the following workaround in the form of a custom plugin:
/**
* CKEditor 5 Plugin defined as a simple function.
* Forces !important on table border styles to override SharePoint CSS.
*/
const ForceTableBorderImportant = ( editor: Editor ) => {
const { view } = editor.editing;
const modifiedElements = new Set<ViewElement>();
const BORDER_ATTRIBUTES = [
'borderColor', 'borderWidth', 'borderStyle',
'tableBorderColor', 'tableBorderWidth', 'tableBorderStyle'
];
/**
* Resolves the actual DOM element that needs styling.
*/
const resolveTargetDomElement = ( viewElement: ViewElement, domConverter: ViewDomConverter ): HTMLElement | null | undefined => {
const domElement = domConverter.mapViewToDom( viewElement );
return domElement?.matches( 'table' ) ? domElement : domElement?.querySelector( 'table' );
};
/**
* Extracts default border properties from the editor configuration.
*/
const getDefaultBorderConfig = () => {
const config = editor.config.get( 'table.tableProperties.defaultProperties' );
return {
width: config?.borderWidth || '1px',
style: config?.borderStyle || 'double',
color: config?.borderColor || '#b3b3b3'
};
};
/**
* Applies default border values if missing and enforces !important on all border styles.
*/
const enforceBorderStyles = ( domElement: HTMLElement, defaults: { width: string, style: string, color: string } ) => {
// Skip layout tables (used for positioning, not data)
if (
domElement.closest('figure')?.classList.contains( 'layout-table' ) ||
domElement.classList.contains( 'layout-table' )
) {
return;
}
const style = domElement.style;
// 1. Apply default values only if specific properties are missing.
if ( !style.getPropertyValue( 'border-width' ) ) style.setProperty( 'border-width', defaults.width, 'important' );
if ( !style.getPropertyValue( 'border-style' ) ) style.setProperty( 'border-style', defaults.style, 'important' );
if ( !style.getPropertyValue( 'border-color' ) ) style.setProperty( 'border-color', defaults.color, 'important' );
// 2. Iterate through ALL active border-related properties and enforce !important.
Array.from( style )
.filter( propName => propName.includes( 'border' ) )
.forEach( propName => {
if ( style.getPropertyPriority( propName ) !== 'important' ) {
style.setProperty( propName, style.getPropertyValue( propName ), 'important' );
}
} );
};
// 1. Collection Phase (Downcast)
editor.conversion.for( 'editingDowncast' ).add( ( dispatcher: any ) => {
const addToCollection = ( evt: unknown, data: any, { mapper }: any ) => {
const viewElement = mapper.toViewElement( data.item );
if ( viewElement ) {
modifiedElements.add( viewElement );
}
};
BORDER_ATTRIBUTES.forEach( attr =>
dispatcher.on( `attribute:${ attr }`, addToCollection, { priority: 'lowest' } )
);
} );
// 2. Execution Phase (Render)
view.on( 'render', () => {
if ( !modifiedElements.size ) return;
const defaultBorder = getDefaultBorderConfig();
modifiedElements.forEach( viewElement => {
const domElement = resolveTargetDomElement( viewElement, view.domConverter );
if ( domElement ) {
enforceBorderStyles( domElement, defaultBorder );
}
} );
modifiedElements.clear();
} );
};The provided workaround, therefore, performs low-level DOM manipulation during the render cycle instead of relying solely on `StylesMap`. By applying these styles directly to the DOM elements after they are mapped, we ensure the priority flag remains intact and is correctly interpreted by the browser.
- SharePoint Framework Documentation
- {@link getting-started/integrations/react-default-npm React integration documentation}
- See how to manipulate the editor's data in the {@link getting-started/setup/getting-and-setting-data Getting and setting data} guide.
- Refer to further guides in the {@link getting-started/setup/configuration setup section} to see how to customize your editor further.
- Check the {@link features/index features category} to learn more about individual features.