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/wysiwygAll 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/uiplatejs (53.3.9), @platejs/basic-nodes (53.0.0), @platejs/floating (53.0.0), @platejs/link (53.3.5), @platejs/list-classic (53.0.0), @platejs/media (53.1.4)react / react-dom (>=18.2.0)slate and slate-react are deliberately not peer dependencies. Plate owns the whole Slate stack, and declaring them here made npm resolve a second physical copy of slate-react — two copies mean two React contexts and a silently broken useSelected().
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. |
editorRef | ForwardedRef<WysiwygEditorHandle> | no | Imperative handle exposing focus(), which selects the end of the document and then focuses the editable area. |
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, node-id, 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, and re-exports of useSelected and usePlateStore.
useUxfPlateSelectors, useUxfPlateActions and useUxfPlateStates still exist but return unknown: Plate 53 replaced the split selectors/actions/states store with a single store that these three cannot meaningfully wrap. Use the re-exported usePlateStore instead.
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")
WysiwygEditorHandle — the shape behind editorRef:
import { WysiwygEditor } from "@uxf/wysiwyg";
// deep import: this type is not re-exported from the package index
import { WysiwygEditorHandle } from "@uxf/wysiwyg/wysiwyg-editor";
import { useRef } from "react";
const editorRef = useRef<WysiwygEditorHandle>(null);
<WysiwygEditor editorRef={editorRef} id="body" initialValue={value} onChange={setValue} plugins={plugins} />;
editorRef.current?.focus(); // caret goes to the end of the document
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.WysiwygEditorHandle is not on the package index. index.ts exports only the WysiwygEditor component, so the ref type has to come from the deep import @uxf/wysiwyg/wysiwyg-editor — which is what @uxf/cms's own WysiwygInput does.@uxf/* peer versions are pinned exactly (see package.json); mismatched versions will not work.