✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Tutorials5 min read

How to change the WYSIWYG in Strapi

October 23, 2019Updated on May 22, 2026
How to change the WYSIWYG in Strapi

In this tutorial we will see how you can create a new field for the admin panel.

For this example, we will replace the default WYSIWYG with CKEditor in the Content Manager by creating a new plugin that will add a new field in your application.

Setting up the plugin

  • Create an application and prevent the server from starting automatically with the following command:
yarn create strapi-app my-app --quickstart --no-run
# or
npx create-strapi-app@latest my-app --quickstart --no-run

The --no-run flag was added as we will run additional commands to create a plugin right after the project generation.

  • Generate a plugin:
cd my-app
yarn strapi generate

Choose "plugin" from the list, press Enter and name the plugin wysiwyg.

// path: ./config/plugins.js

module.exports = {
  // ...
  wysiwyg: {
    enabled: true,
    resolve: "./src/plugins/wysiwyg", // path to plugin folder
  },
  // ...
};
  • Install the required dependencies:
cd src/plugins/wysiwyg
yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
  • Start the application with the front-end development mode:
# Go back to the application root folder
cd ../../..
yarn develop --watch-admin

Note: Launching the Strapi server in watch mode without creating a user account first will open localhost:1337 with a JSON format error. Creating a user on localhost:8081 prevents this alert.

We now need to create our new WYSIWYG, which will replace the default one in the Content Manager.

Creating the WYSIWYG

In this part we will create 3 components:

  • a MediaLib component used to insert media in the editor
  • an Editor component that uses CKEditor as the WYSIWYG editor
  • a Wysiwyg component to wrap the CKEditor

The following code examples can be used to implement the logic for the 3 components:

Example of a MediaLib component used to insert media in the editor:

// path: ./src/plugins/wysiwyg/admin/src/components/MediaLib/index.js

import React from "react";
import { prefixFileUrlWithBackendUrl, useLibrary } from "@strapi/helper-plugin";
import PropTypes from "prop-types";

const MediaLib = ({ isOpen, onChange, onToggle }) => {
  const { components } = useLibrary();
  const MediaLibraryDialog = components["media-library"];

  const handleSelectAssets = (files) => {
    const formattedFiles = files.map((f) => ({
      alt: f.alternativeText || f.name,
      url: prefixFileUrlWithBackendUrl(f.url),
      mime: f.mime,
    }));

    onChange(formattedFiles);
  };

  if (!isOpen) {
    return null;
  }

  return (
    <MediaLibraryDialog
      onClose={onToggle}
      onSelectAssets={handleSelectAssets}
    />
  );
};

MediaLib.defaultProps = {
  isOpen: false,
  onChange: () => {},
  onToggle: () => {},
};

MediaLib.propTypes = {
  isOpen: PropTypes.bool,
  onChange: PropTypes.func,
  onToggle: PropTypes.func,
};

export default MediaLib;

Example of an Editor component using CKEditor as the WYSIWYG editor:

// path: ./src/plugins/wysiwyg/admin/src/components/Editor/index.js

import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import { Box } from "@strapi/design-system/Box";

const Wrapper = styled(Box)`
  .ck-editor__main {
    min-height: ${200 / 16}em;
    > div {
      min-height: ${200 / 16}em;
    }
    // Since Strapi resets css styles, it can be configured here (h2, h3, strong, i, ...)
  }
`;

const configuration = {
  toolbar: [
    "heading",
    "|",
    "bold",
    "italic",
    "link",
    "bulletedList",
    "numberedList",
    "|",
    "indent",
    "outdent",
    "|",
    "blockQuote",
    "insertTable",
    "mediaEmbed",
    "undo",
    "redo",
  ],
};

const Editor = ({ onChange, name, value, disabled }) => {
  return (
    <Wrapper>
      <CKEditor
        editor={ClassicEditor}
        disabled={disabled}
        config={configuration}
        data={value || ""}
        onReady={(editor) => editor.setData(value || "")}
        onChange={(event, editor) => {
          const data = editor.getData();
          onChange({ target: { name, value: data } });
        }}
      />
    </Wrapper>
  );
};

Editor.defaultProps = {
  value: "",
  disabled: false,
};

Editor.propTypes = {
  onChange: PropTypes.func.isRequired,
  name: PropTypes.string.isRequired,
  value: PropTypes.string,
  disabled: PropTypes.bool,
};

export default Editor;

Example of a Wysiwyg component wrapping CKEditor:

// path: ./src/plugins/wysiwyg/admin/src/components/Wysiwyg/index.js

import React, { useState } from "react";
import PropTypes from "prop-types";
import { Stack } from "@strapi/design-system/Stack";
import { Box } from "@strapi/design-system/Box";
import { Button } from "@strapi/design-system/Button";
import { Typography } from "@strapi/design-system/Typography";
import Landscape from "@strapi/icons/Landscape";
import MediaLib from "../MediaLib";
import Editor from "../Editor";
import { useIntl } from "react-intl";

const Wysiwyg = ({
  name,
  onChange,
  value,
  intlLabel,
  disabled,
  error,
  description,
  required,
}) => {
  const { formatMessage } = useIntl();
  const [mediaLibVisible, setMediaLibVisible] = useState(false);

  const handleToggleMediaLib = () => setMediaLibVisible((prev) => !prev);

  const handleChangeAssets = (assets) => {
    let newValue = value ? value : "";

    assets.map((asset) => {
      if (asset.mime.includes("image")) {
        const imgTag = `<p><img src="${asset.url}" alt="${asset.alt}"></img></p>`;

        newValue = `${newValue}${imgTag}`;
      }

      // Handle videos and other type of files by adding some code
    });

    onChange({ target: { name, value: newValue } });
    handleToggleMediaLib();
  };

  return (
    <>
      <Stack size={1}>
        <Box>
          <Typography variant="pi" fontWeight="bold">
            {formatMessage(intlLabel)}
          </Typography>
          {required && (
            <Typography variant="pi" fontWeight="bold" textColor="danger600">
              *
            </Typography>
          )}
        </Box>
        <Button
          startIcon={<Landscape />}
          variant="secondary"
          fullWidth
          onClick={handleToggleMediaLib}
        >
          Media library
        </Button>
        <Editor
          disabled={disabled}
          name={name}
          onChange={onChange}
          value={value}
        />
        {error && (
          <Typography variant="pi" textColor="danger600">
            {formatMessage({ id: error, defaultMessage: error })}
          </Typography>
        )}
        {description && (
          <Typography variant="pi">{formatMessage(description)}</Typography>
        )}
      </Stack>
      <MediaLib
        isOpen={mediaLibVisible}
        onChange={handleChangeAssets}
        onToggle={handleToggleMediaLib}
      />
    </>
  );
};

Wysiwyg.defaultProps = {
  description: "",
  disabled: false,
  error: undefined,
  intlLabel: "",
  required: false,
  value: "",
};

Wysiwyg.propTypes = {
  description: PropTypes.shape({
    id: PropTypes.string,
    defaultMessage: PropTypes.string,
  }),
  disabled: PropTypes.bool,
  error: PropTypes.string,
  intlLabel: PropTypes.shape({
    id: PropTypes.string,
    defaultMessage: PropTypes.string,
  }),
  name: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  required: PropTypes.bool,
  value: PropTypes.string,
};

export default Wysiwyg;

Registering the field

The last step is to register the wysiwyg field with the Wysiwyg component using addFields(). Replace the content of the admin/src/index.js field of the plugin with the following code:

// path: ./src/plugins/wysiwyg/admin/src/index.js

import pluginPkg from "../../package.json";
import Wysiwyg from "./components/Wysiwyg";
import pluginId from "./pluginId";

const name = pluginPkg.strapi.name;

export default {
  register(app) {
    app.addFields({ type: "wysiwyg", Component: Wysiwyg });

    app.registerPlugin({
      id: pluginId,
      isReady: true,
      name,
    });
  },
  bootstrap() {},
};

And voilà, if you create a new collection type or single type with a rich text field you will see the implementation of CKEditor instead of the default WYSIWYG:

Screenshot of Content Manager using CKEditor for rich text fields

Paul BratslavskyDeveloper Advocate

Join our Newsletter

Get all the latest news and events.

Newsletter signup is not configured yet.

Building Docs for the AI Era, Part 1
Tutorials·10 min read

Building Docs for the AI Era, Part 1: Self-Healing Docs

📚 Series: Building Docs for the AI Era: This post is the first of many in this series. You can find the outline for...

·June 11, 2026
How to Migrate from Contentful to Strapi Using a Claude Code Skill
Tutorials·19 min read

How to Migrate from Contentful to Strapi Using a Claude Code Skill

TL;DR By the end, you will have moved a complete site from Contentful into Strapi: a blog with a landing page, posts,...

·June 4, 2026
Building High-Performance Strapi Applications
Tutorials·8 min read

Building High-Performance Strapi Apps: Common Pitfalls and Best Practices

Strapi is designed to scale and perform well in production environments. Teams running welldesigned Strapi applications...

·April 16, 2026