Using an Editor Config Contributor Client Extension

Adding a Custom CKEditor 5 Plugin

Liferay DXP 2026.Q1+

With an Editor Config Contributor client extension, you can add your own CKEditor 5 plugin to Liferay’s editors. This example uses a sample client extension that adds a Timestamp toolbar button. Clicking the button inserts the current date and time at the cursor. See Using an Editor Config Contributor Client Extension to learn more about this client extension type.

Note

CKEditor 5 is the default text editor in Liferay DXP 2026.Q2+. In Liferay DXP 2026.Q1, activate it with the release feature flag Enhanced Rich Text Editor (LPD-11235). See Upgrading to CKEditor 5 for the upgrade’s impact on existing content.

Prerequisites

To work with Editor Config Contributor client extensions, follow these steps:

  1. Install a supported version of Java.

    Note

    Check the compatibility matrix for supported JDKs, databases, and environments. See JVM Configuration for recommended JVM settings.

  2. Download and unzip the sample workspace:

    curl -o com.liferay.sample.workspace-latest.zip https://repository.liferay.com/nexus/service/local/artifact/maven/content\?r\=liferay-public-releases\&g\=com.liferay.workspace\&a\=com.liferay.sample.workspace\&\v\=LATEST\&p\=zip
    
    unzip com.liferay.sample.workspace-latest.zip
    

Now you can examine and modify the sample Editor Config Contributor client extensions.

Examine and Modify the Client Extension

The Editor Config Contributor example is in the sample workspace’s client-extensions/liferay-sample-editor-config-contributor-3/ folder. Its client-extension.yaml file defines these details:

assemble:
    -   from: build
        into: static
liferay-sample-editor-config-contributor-3:
    editorConfigKeys:
        -   sampleReactCKEditor5ClassicEditor
    name: Liferay Sample Editor Config Contributor CKEditor 5 Timestamp
    type: editorConfigContributor
    url: index.js

The client extension declares its ID (liferay-sample-editor-config-contributor-3), its type (editorConfigContributor), and the editors it applies to (editorConfigKeys). Its url property points to index.js, the bundled JavaScript file the build produces. The assemble block packages the entire build/ folder into the client extension’s .zip file as static resources, including the index.js file. See the Editor Config Contributor YAML Configuration Reference for more information.

Important

The sample’s key (sampleReactCKEditor5ClassicEditor) uses one of Liferay’s sample editor applications, which aren’t part of a standard Liferay installation. To use the sample, update the key to the editor you want to configure.

To apply the plugin to rich text fields in Liferay DXP, including the Content field for web content, add rich_text to the key list:

editorConfigKeys:
    -   rich_text
    -   sampleReactCKEditor5ClassicEditor

Examine the Plugin Code

The plugin’s source is in src/index.ts. It has two parts: the Timestamp plugin class and an editor transformer that wires the plugin into the editor’s configuration.

import {Plugin} from '@ckeditor/ckeditor5-core/dist/index.js';
import {ButtonView} from '@ckeditor/ckeditor5-ui/dist/index.js';
import {
	EditorConfigTransformer,
	EditorTransformer,
} from '@liferay/js-api/editor';

const TIMESTAMP_ICON =
	'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10 1a9 9 0 1 0 9 9 9 9 0 0 0-9-9zm0 16a7 7 0 1 1 7-7 7 7 0 0 1-7 7z"/><path d="M10.5 5h-1v5.5l3.7 3.7.7-.7-3.4-3.4z"/></svg>';

class Timestamp extends Plugin {
	init() {
		const editor = this.editor;

		editor.ui.componentFactory.add('timestamp', () => {
			const button = new ButtonView();

			button.set({
				icon: TIMESTAMP_ICON,
				label: 'Timestamp',
				tooltip: true,
			});

			button.on('execute', () => {
				const now = new Date();

				editor.model.change((writer) => {
					editor.model.insertContent(
						writer.createText(`Current time: ${now.toString()} `)
					);
				});
			});

			return button;
		});
	}
}

The Timestamp class extends CKEditor 5’s Plugin class. In init(), it registers a component named timestamp through editor.ui.componentFactory.add(). The component is a ButtonView with an inline SVG clock icon, the label Timestamp, and tooltip: true, so the label appears as a tooltip on hover. When you click the button, its execute handler inserts the text Current time: <date> at the current selection through editor.model.change() and writer.createText().

Registering the component makes the button available, but the editor’s configuration determines whether it appears. The rest of the file handles that:

const editorConfigTransformer: EditorConfigTransformer<any> = (config) => {
	const toolbar = config.toolbar as any;
	const existingItems = Array.isArray(toolbar)
		? toolbar
		: toolbar?.items ?? [];

	return {
		...config,
		extraPlugins: [...(config.extraPlugins ?? []), Timestamp],
		toolbar: {
			items: [...existingItems, 'timestamp'],
		},
	};
};

const editorTransformer: EditorTransformer<any> = {
	editorConfigTransformer,
};

export default editorTransformer;

The transformer receives the editor’s configuration and returns a modified copy. It adds the Timestamp class to extraPlugins, which loads the plugin into the editor, and appends the 'timestamp' item to the toolbar so the button appears. Because config.toolbar can be a plain array of item names or an object with an items array, the transformer reads whichever shape it finds and normalizes the result to {items: [...existingItems, 'timestamp']}. The file’s default export is an EditorTransformer object containing the config transformer; Liferay applies it to the editors matched by the client extension’s editorConfigKeys.

Examine the Dependencies and Build Setup

A custom plugin imports CKEditor code, so its dependency and build setup matters more than in other client extension types. Open the sample’s package.json:

{
	"dependencies": {
		"@ckeditor/ckeditor5-core": "46.0.3",
		"@ckeditor/ckeditor5-ui": "46.0.3",
		"@liferay/js-api": "0.8.0"
	},
	"description": "Liferay Sample Editor Config Contributor CKEditor 5 Timestamp",
	"devDependencies": {
		"serve": "^14.2.0",
		"typescript": "^5.0.4"
	},
	"main": "./src/index.ts",
	"name": "@liferay/liferay-sample-editor-config-contributor-3",
	"scripts": {
		"build": "esbuild src/index.ts --outdir=build --bundle --format=esm --external:@ckeditor/*",
		"start": "PORT=3002 serve --cors",
		"watch": "tsc --watch"
	},
	"type": "module",
	"version": "0.0.0"
}

The @ckeditor/ckeditor5-core and @ckeditor/ckeditor5-ui packages provide the Plugin and ButtonView classes the plugin imports, and both are pinned at 46.0.3. In your project, these packages serve only TypeScript type checking at build time. At runtime, the same imports resolve to the single copy of CKEditor 5 that Liferay DXP ships. The @liferay/js-api package provides the EditorConfigTransformer and EditorTransformer types that describe the transformer contract.

The build script makes that split work. It bundles src/index.ts with esbuild into the build/ folder as an ES module, matching what the assemble block and the url: index.js property expect. The key flag is --external:@ckeditor/*: the single wildcard marks every @ckeditor package as external, so esbuild keeps those import statements in the output instead of copying CKEditor’s code into your bundle. The deployed bundle contains only your plugin code.

The wildcard matters because every plugin must run against the copy of CKEditor 5 that Liferay DXP ships. If you bundled the @ckeditor packages, your client extension would load a second copy of CKEditor into the page and break the editor. The wildcard also scales with your code: if your plugin grows to import another @ckeditor package, the exclusion already covers it.

The development dependencies support local work. The watch script runs tsc --watch to type-check the plugin as you edit, since esbuild bundles TypeScript without type checking. The start script runs serve with CORS enabled on port 3002 to serve the extension’s files locally. The sample workspace provides esbuild itself, so a standalone project outside the workspace must add it as a development dependency.

Deploy the Client Extension

Start a new Liferay DXP instance by running

docker run -it -m 8g -p 8080:8080 liferay/dxp:2026.q1.9-lts

Sign in to Liferay at http://localhost:8080 using the email address test@liferay.com and the password test. When prompted, change the password to learn.

Once Liferay starts, open a new terminal and run this command from the client-extensions/liferay-sample-editor-config-contributor-3/ folder:

../../gradlew clean deploy -Ddeploy.docker.container.id=$(docker ps -lq)

This builds the bundle and deploys the client extension .zip file to Liferay’s deploy/ folder. For deploying to Liferay SaaS, see Using an Editor Config Contributor Client Extension.

Confirm deployment is successful in your Liferay instance’s console:

STARTED liferaysampleeditorconfigcontributor3_...

Test the Timestamp Button

With the client extension deployed and rich_text in its editorConfigKeys, the plugin applies to web content’s rich text fields.

  1. Open the Site Menu (Site Menu), expand Content & Data, and click Web Content.

  2. Click New and select Basic Web Content.

  3. Click Timestamp in the Content field’s toolbar.

    This inserts the current date and time at the cursor’s location using this format, Current time: <date>. The button uses the clock icon and hovering over it displays the Timestamp tooltip.

You’ve successfully added your own CKEditor 5 plugin using an Editor Config Contributor client extension.