# Integrating CKEditor 5 rich-text editor with SharePoint SPFx from npm

You can integrate the CKEditor 5 rich-text editor into [SharePoint Framework (SPFx)](https://learn.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview) solutions by installing the CKEditor 5 npm packages and using the editor inside an SPFx web part.

<a id="prerequisites">

## Prerequisites

Before integrating CKEditor 5 rich-text editor with SharePoint, make sure you have the following installed:

1. [Node.js](https://nodejs.org/) (LTS version)
2. [Yeoman generator](https://yeoman.io/) and SharePoint generator plugin:

```bash
npm install -g yo @microsoft/generator-sharepoint
```

3. [Gulp](https://gulpjs.com/) (for building the project):

```bash
npm install -g gulp
```

<a id="creating-a-new-web-part-project">

## Creating a new web part project

First, create a new folder and navigate into it:

```bash
mkdir my-spfx-app
cd my-spfx-app
```

Generate a new SPFx web part project:

```bash
yo @microsoft/sharepoint
```

When 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:

<a id="running-the-web-part-project">

## Running the web part project

After the project is created, go to `config/serve.json` file and update the `initialPage` property with your SharePoint tenant URL:

```js
"initialPage": "https://your-tenant.sharepoint.com/_layouts/workbench.aspx"
```

At this point you should be able to run the project by executing:

```bash
gulp serve
```

A 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:

<a id="ckeditor-5-web-part-integration">

## CKEditor 5 web part integration

<a id="installing-dependencies">

### Installing dependencies

Now it is time to add CKEditor 5 to the application. First, install CKEditor 5 and React integration packages:

```bash
npm install --save @ckeditor/ckeditor5-react ckeditor5 ckeditor5-premium-features
```

In `tsconfig.json` file add:

```js
...
"allowSyntheticDefaultImports": true
...
```

<a id="creating-ckeditor-5-web-part-component">

### Creating CKEditor 5 web part component

Assuming you named your web part “RichTextEditor”, open the `src/webparts/richTextEditor/components/RichTextEditor.tsx` file and modify it to include CKEditor 5:

```typescript
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.

```bash
gulp serve
```

<a id="adding-the-editor-web-part-on-page">

### Adding the editor web part on page

You should be now able to add CKEditor 5 HTML editor to your SharePoint application by clicking the plus icon on the main page:

Then choose CKEditor 5 component:

You should now be able to use the rich-text editor with SharePoint:

<a id="known-issues">

## Known issues

* 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](https://github.com/SharePoint/sp-dev-docs/issues/9438)).
* When using CKEditor 5 inside a Fluent UI `Dialog` or `Modal` component, image resizing handles might not work because the `Layer` component captures mouse events. To fix this, enable event bubbling in the `modalProps`:

```typescript
<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:

```css
.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:

```js
/**
* 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();
	} );
};
```

> **Important**
>
> The internal `StylesMap` utility used by the editor’s conversion system does not natively support the `!important` flag during style normalization.
>
> 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.

<a id="additional-resources">

## Additional resources

* [SharePoint Framework Documentation](https://learn.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview)
* [React integration documentation](react/react-default-npm.md)

<a id="next-steps">

## Next steps

* See how to manipulate the editor’s data in the [Getting and setting data](../../setup/getting-and-setting-data.md) guide.
* Refer to further guides in the [setup section](../../setup/configuration.md) to see how to customize your editor further.
* Check the [features category](../../../features/index.md) to learn more about individual features.

---

Full index of the CKEditor 5 documentation: [llms.txt](../../../../llms.txt)
