Rich-text / WYSIWYG editor for React, built on Plate / Slate. It ships a ready-made WysiwygEditor component, a toolbar, and a set of UXF-styled plugins (headings, marks, lists, links, images, videos, buttons, blockquote, highlight).
Reach for @uxf/wysiwyg when you need a structured rich-text editor whose value is a JSON document (a Slate/Plate node tree), not an HTML string. The editor is uncontrolled: it is seeded once from initialValue and reports every edit through onChange as a WysiwygContent array.
It is a client component only and depends on @uxf/ui for its visual primitives. For a higher-level CMS content-builder wrapper, see @uxf/cms; use this package directly when you want the standalone editor.
yarn add @uxf/wysiwyg
All Plate/Slate packages and the UXF packages are peer dependencies with pinned versions — install them at the versions declared in package.json:
@uxf/core, @uxf/core-react, @uxf/ui20.7.2): @udecode/plate-basic-marks, @udecode/plate-block-quote, @udecode/plate-break, @udecode/plate-code-block, @udecode/plate-common, @udecode/plate-core, @udecode/plate-floating, @udecode/plate-link, @udecode/plate-list, @udecode/plate-media, @udecode/plate-node-id, @udecode/plate-paragraph, @udecode/plate-reset-node, @udecode/plate-select, @udecode/plate-trailing-block@udecode/slate (19.8.0), @udecode/slate-react (19.7.1), @udecode/slate-utils (19.7.1)slate (0.90.0), slate-history (0.86.0), slate-hyperscript (0.77.0), slate-react (0.91.11)react / react-dom (>=18.2.0)Import the published stylesheet once (e.g. in your global CSS):
@import url("@uxf/wysiwyg/styles/styles.css");
This stylesheet uses Tailwind's theme(), @apply (including uxf-typo-* utilities from @uxf/ui) and CSS nesting, so it must be processed by your app's Tailwind/PostCSS pipeline alongside the @uxf/ui Tailwind config.
The editor renders its toolbar/labels through useUxfTranslation (@uxf/core-react/translations). Register this package's translations, or you will see raw translation keys instead of text. Merge the default export of @uxf/wysiwyg/translations/translations (locales: cs, en, sk, de) into the translation function you pass to your app's provider:
import wysiwygTranslations from "@uxf/wysiwyg/translations/translations";
import uiTranslations from "@uxf/ui/translations/translations";
import { createDevT } from "@uxf/core-react/translations/create-dev-t";
export const tFunction = createDevT("cs", {
...uiTranslations,
...wysiwygTranslations,
});
Wire tFunction through the standard UXF setup (translationFn on UiContextProvider from @uxf/ui/context, or TranslationsProvider from @uxf/core-react/translations).
Build the plugin set once, then render WysiwygEditor. createAllPluginsWithUi enables every plugin; its only required option is image (image insertion needs uploadImage + getImageUrl).
"use client";
import { createAllPluginsWithUi, WysiwygContent, WysiwygEditor } from "@uxf/wysiwyg";
import { useState } from "react";
const plugins = createAllPluginsWithUi({
image: {
uploadImage: async (file) => uploadToS3(file), // returns FileResponse
getImageUrl: (file) => resolveUrl(file),
},
});
const INITIAL_VALUE: WysiwygContent = [{ type: "paragraph", id: "p1", children: [{ text: "Hello" }] }];
export function MyEditor() {
const [value, setValue] = useState<WysiwygContent>(INITIAL_VALUE);
return <WysiwygEditor id="my-editor" initialValue={value} onChange={setValue} plugins={plugins} />;
}
To enable only some plugins, compose them yourself with createPluginsWithUi:
import { createBoldPluginWithUi, createHeadingsPluginWithUi, createPluginsWithUi } from "@uxf/wysiwyg";
const plugins = createPluginsWithUi([
createHeadingsPluginWithUi({ disabledLevels: [1] }),
createBoldPluginWithUi(),
]);
WysiwygContent is a WysiwygRootBlock[] — a Plate/Slate document, not HTML. Each block carries a type, an optional id, and children:
type WysiwygRootBlock =
| UxfParagraphElement // type: "paragraph"
| UxfHeadingElement // type: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
| UxfBlockQuoteElement // type: "blockquote"
| UxfUnorderedListElement // type: "ul" (li > lic)
| UxfOrderedListElement // type: "ol" (li > lic)
| UxfLinkElement // type: "link" (inline)
| UxfImageElement // type: "image" (void)
| UxfVideoElement // type: "video" (void)
| UxfButtonElement; // type: "button" (void)
Inline text (RichText) carries the mark flags bold, italic, underline, code, highlight. onChange always receives the full, current document.
Everything below is exported from the package root (@uxf/wysiwyg) unless a deep path is shown. There is no exports map, so deep imports resolve by filesystem.
WysiwygEditorimport { WysiwygEditor } from "@uxf/wysiwyg";
| Prop | Type | Required | Description |
|---|---|---|---|
id |
string |
yes | Unique Plate instance id. Must be unique per editor on the page. |
initialValue |
WysiwygContent | undefined |
yes | Document the editor is seeded with on mount. |
onChange |
(value: WysiwygContent) => void |
yes | Called on every edit with the full document. |
plugins |
UxfPlatePlugin[] |
yes | Build with createAllPluginsWithUi / createPluginsWithUi. |
className |
string |
no | Class on the editor root wrapper. |
editableProps |
TEditableProps<WysiwygContent> |
no | Passed to Plate's editable area (placeholder, readOnly, autoFocus, spellCheck, className, …). Defaults: autoFocus: false, readOnly: false, spellCheck: false, localized placeholder. |
customPluginsToolbarButtons |
ReactNode |
no | Extra toolbar buttons appended to the built-in ones. |
toolbarLeftElement |
ReactNode |
no | Node rendered at the toolbar's left edge. |
toolbarRightElement |
ReactNode |
no | Node rendered at the toolbar's right edge. |
createPluginsWithUi / createAllPluginsWithUi return the UxfPlatePlugin[] you pass to WysiwygEditor. Both inject the base plugins automatically (paragraph, exit-break, reset-node, soft-break, node-id, select-on-backspace, trailing-block), so pass only the feature builders below — not raw Plate plugins.
| Export | Signature | Notes |
|---|---|---|
createPluginsWithUi |
(plugins: Array<WysiwygPlugin | WysiwygRecursivePlugin<string>>, options?: { overrideByKey?: MyOverrideByKey }) => UxfPlatePlugin[] |
Compose a custom subset. |
createAllPluginsWithUi |
(options: CreateAllPluginsOptions) => UxfPlatePlugin[] |
Enable every plugin. |
createHeadingsPluginWithUi |
(options?: HeadingsPluginOptions) => WysiwygRecursivePlugin |
disabledLevels?: (1..6)[]. |
createBoldPluginWithUi |
() => WysiwygPlugin |
|
createItalicPluginWithUi |
() => WysiwygPlugin |
|
createUnderlinePluginWithUi |
() => WysiwygPlugin |
|
createCodePluginWithUi |
() => WysiwygPlugin |
|
createHighlightPluginWithUi |
(color?: CSSProperties["color"]) => WysiwygPlugin |
Defaults to twColors.yellow[300]. |
createBlockquotePluginWithUi |
() => WysiwygPlugin |
|
createListPluginWithUi |
() => WysiwygRecursivePlugin |
Renders ul / ol / li. |
createLinkPluginWithUi |
() => WysiwygPlugin |
|
createImagePluginWithUi |
(options: UxfImagePluginOptions) => WysiwygPlugin |
See options below. |
createVideoPluginWithUi |
() => WysiwygPlugin |
|
createButtonPluginWithUi |
() => WysiwygPlugin |
CreateAllPluginsOptions:
| Key | Type | Required | Description |
|---|---|---|---|
image |
UxfImagePluginOptions |
yes | Image plugin options (pass {} to enable without upload handlers). |
headings |
HeadingsPluginOptions |
no | e.g. { disabledLevels: [1] }. |
highlightColor |
CSSProperties["color"] |
no | Highlight mark color. |
UxfImagePluginOptions (extends Plate's MediaPlugin):
| Key | Type | Description |
|---|---|---|
uploadImage |
(file: File) => Promise<FileResponse> |
Upload handler; returns a FileResponse (@uxf/core/types). |
getImageUrl |
(file: FileResponse) => string |
Resolves the display URL for an uploaded file. |
disableUploadOnPasteImageUrl |
boolean |
Disable auto-upload when an image URL is pasted. |
disableUploadOnPasteImage |
boolean |
Disable auto-upload when an image blob is pasted. |
import { ... } from "@uxf/wysiwyg"; — thin, typed wrappers over Plate's editor hooks, bound to WysiwygContent / UxfEditor:
useUxfPlateEditorRef, useUxfEditorRef, useUxfEditorState, useUxfPlateEditorState, useUxfPlateSelectors, useUxfPlateActions, useUxfPlateStates, and a re-export of useSelected (from slate-react).
Editor helpers for building custom plugins/toolbar buttons: getUxfEditor, getPluginOptions, getPluginType, someNode, toggleNodeType, focusEditor, isRangeInSingleText, isMarkActive, toggleMark, isPluginEnabled, isSomeOfPluginsEnabled, getSelectedNode, getActiveElement, removeElement, insertVoid, removeSelectedNode.
Serialize a document to plain text (deep import, not in the root barrel):
import { serializeToPlaintext } from "@uxf/wysiwyg/serializers/serialize-to-plaintext";
serializeToPlaintext(value); // keepIndentation defaults to true (joins blocks with "\n")
The full node model and editor types are re-exported, including: WysiwygContent, WysiwygRootBlock, RichText, the element interfaces (UxfParagraphElement, UxfHeadingElement, UxfBlockQuoteElement, UxfLinkElement, UxfImageElement, UxfVideoElement, UxfButtonElement, UxfUnorderedListElement, UxfOrderedListElement, LiElement, LicElement), plus UxfEditor, UxfPlatePlugin, WysiwygPlugin, WysiwygRecursivePlugin, and the render prop/component types (RenderElementProps, ElementUiComponent, RenderLeafProps, LeafUiComponent, RenderAfterEditable, MyOverrideByKey, UiComponents).
WysiwygEditor is marked "use client" and renders a Loader until it has mounted on the client; it cannot render on the server.initialValue seeds the document once on mount. Changing initialValue later does not reset the editor — remount it (e.g. via a changed React key) to load a different document. Read the live value from onChange.createPluginsWithUi / createAllPluginsWithUi to plugins; they inject the required base plugins. Do not hand-assemble raw Plate plugin arrays.@uxf/wysiwyg/styles/styles.css the editor is unstyled; the file relies on your Tailwind/PostCSS setup and @uxf/ui's uxf-typo-* utilities.@uxf/wysiwyg/translations/translations, otherwise labels/tooltips render as raw keys.@uxf/* peer versions are pinned exactly (see package.json); mismatched versions will not work.