Customizing Liferay's Look and Feel

Using an Editor Config Contributor Client Extension

Liferay DXP 2026.Q1+

You can transform the configuration of Liferay’s text editors with an Editor Config Contributor client extension. The transformation runs in JavaScript and returns a copy rather than changing the original. To get started, consider Liferay’s sample workspace, which includes multiple examples.

Note

This example uses CKEditor 5, the default text editor in Liferay DXP 2026.Q2+. In Liferay DXP 2026.Q1, activate it with the Enhanced Rich Text Editor (LPD-11235) release feature flag. See Upgrading to CKEditor 5 for how this upgrade may impact existing content.

Prerequisites

To start developing 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 have the tools to deploy your first Editor Config Contributor client extension.

Examine and Modify the Client Extension

The sample workspace’s client-extensions/ folder contains five Editor Config Contributor samples, liferay-sample-editor-config-contributor-1 through -5. This example uses the sample in the client-extensions/liferay-sample-editor-config-contributor-2/ folder, which adds a word count readout below CKEditor 5. For the custom CKEditor 5 plugin, custom CKEditor 5 styles, and legacy CKEditor 4 and Alloy Editor samples, see Adding a Custom CKEditor 5 Plugin, Adding Custom CKEditor 5 Styles, and Configuring CKEditor 4 and Alloy Editor.

The client extension is defined in the folder’s client-extension.yaml file:

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

The assemble block specifies that everything in the build folder is included as a static resource in the built client extension .zip file. The JavaScript file in an Editor Config Contributor client extension is served as a static resource in Liferay.

The client extension declares its ID (liferay-sample-editor-config-contributor-2), its type (editorConfigContributor), and the editors it applies to (editorConfigKeys). Its url property points to index.js, the bundled JavaScript file the build produces. See the Editor Config Contributor YAML Configuration Reference for more information.

The extension’s logic is in src/index.ts and written in TypeScript. Deploying the client extension runs the sample’s build script, which bundles this file into build/index.js:

import {
	EditorConfigTransformer,
	EditorTransformer,
	WordCount,
} from '@liferay/js-api/editor';

const editorConfigTransformer: EditorConfigTransformer<any> = (config) => {
	let displayEl: HTMLElement | null = null;

	return {
		...config,
		extraPlugins: [...(config.extraPlugins ?? []), WordCount],
		wordCount: {
			onUpdate: ({
				characters,
				words,
			}: {
				characters: number;
				words: number;
			}) => {
				if (!displayEl) {
					displayEl = document.createElement('div');
					displayEl.className = 'mt-1 text-secondary';
					displayEl.dataset.testid = 'word-count-container';

					document
						.querySelector('.ck-editor__editable')
						?.closest('.ck-editor')
						?.parentElement?.appendChild(displayEl);
				}

				displayEl.textContent = `Words: ${words} | Characters: ${characters}`;
			},
		},
	};
};

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

export default editorTransformer;

The editorConfigTransformer function receives the editor’s current configuration and returns a transformed copy. Here it adds the official WordCount plugin to the configuration’s extraPlugins array and uses the plugin’s wordCount.onUpdate callback to append a div element below the editor. The element reports the text’s length as Words: N | Characters: N and updates as you type.

The sample declares a single dependency in its package.json file, "@liferay/js-api": "0.8.0". The @liferay/js-api/editor subpath provides the transformer types and re-exports the official WordCount plugin, so your code imports the plugin from one place, without pinning the CKEditor version that Liferay DXP ships. WordCount is the only official plugin the subpath re-exports. Because the plugin’s code lives in a CKEditor package that Liferay DXP serves at runtime, the sample’s build script marks that package external rather than bundling it:

esbuild src/index.ts --outdir=build --bundle --format=esm --external:@ckeditor/ckeditor5-word-count

The sample declares only one editor config key, sampleReactCKEditor5ClassicEditor. One of Liferay’s editor sample applications uses that key, and those applications aren’t part of a standard Liferay installation, so deploying the sample unchanged has no visible effect anywhere in Liferay’s UI. If you deploy the extension and see no change in any editor, check editorConfigKeys first. To see the extension in web content, add rich_text to the editorConfigKeys list in client-extension.yaml:

editorConfigKeys:
    -   rich_text
    -   sampleReactCKEditor5ClassicEditor

The rich_text key applies the client extension to rich text fields in Liferay DXP, including the Content field for web content.

Now deploy the client extension.

Deploy the Client Extension to Liferay

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 extension’s folder in the sample workspace:

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

This builds your client extension and deploys the zip to Liferay’s deploy/ folder.

Note

To deploy your client extension to Liferay SaaS, use the Liferay Cloud Command-Line Tool to run lcp deploy.

Confirm the deployment in your Liferay instance’s console:

STARTED liferaysampleeditorconfigcontributor2_...

Verify the Client Extension

Now that your client extension is deployed, check that it’s running properly.

  1. Open the Global Menu (Global Menu), go to the Applications tab, and click Client Extensions under Custom Apps.

  2. Verify the Liferay Sample Editor Config Contributor 2 client extension appears.

    Next, verify the word count readout in web content:

  3. Navigate back to the Liferay site.

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

  5. Click New and select Basic Web Content.

  6. Type in the Content field’s editor. The word count readout appears below the editor and updates as you type.

You have successfully used an Editor Config Contributor client extension in Liferay. Next, try deploying other client extension types.