• CMSnpm version

    • Overview
    • ContentBuilder
    • InviteUserForm
    • LoginForm
    • RenewPasswordForm
    • WysiwygInput
  • UInpm version

    • Overview
    • Accordion
    • AlertBubble
    • AnnouncementBar
    • Avatar
    • AvatarFileInput
    • Badge
    • Button
    • ButtonGroup
    • ButtonList
    • Calendar
    • Checkbox
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • Chip
    • ColorRadio
    • ColorRadioGroup
    • Combobox
    • DatePicker
    • DatePickerInput
    • DateRangePicker
    • DateRangePickerInput
    • DatetimePicker
    • DatetimePickerInput
    • Dialog
    • Dropdown
    • Dropzone
    • ErrorMessage
    • FileInput
    • FlashMessages
    • FormComponent
    • Icon
    • IconButton
    • ImageGallery
    • InfoBox
    • Input
    • Label
    • Layout
    • Lightbox
    • ListItem
    • Loader
    • Lozenge
    • Menu
    • Message
    • Modal
    • ✅ ModalDialog
    • ✅ ModalHeader
    • MultiCombobox
    • MultiSelect
    • Pagination
    • Paper
    • Popover
    • Radio
    • RadioGroup
    • RasterImage
    • Select
    • Switch
    • ✅ Tabs
    • TextInput
    • TextLink
    • Textarea
    • TimePicker
    • TimePickerInput
    • Toggle
    • Tooltip
    • Typography
  • Formnpm version

    • Overview
    • AvatarFileInput
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • ColorRadioGroup
    • Combobox
    • DatePickerInput
    • DateRangePickerInput
    • DatetimePickerInput
    • Dropzone
    • FileInput
    • Form
    • FormRenderer
    • GpsInput
    • MoneyInput
    • MultiCombobox
    • MultiSelect
    • NumberInput
    • PasswordInput
    • RadioGroup
    • Select
    • TextInput
    • Textarea
    • TimePickerInput
    • Toggle
  • DataGridnpm version

    • Overview
    • DataGrid
    • DataGridCustomExample
    • ExportButton
    • FilterList
    • Filters
    • FiltersButton
    • FulltextInput
    • HiddenColumns
    • HiddenColumnsButton
    • Pagination
    • RowCounts
    • RowsPerPageSelect
    • SelectedRowsToolbar
    • TableV2
    • ToolbarControl
    • ToolbarCustoms
    • ToolbarTabs
  • Wysiwygnpm version

    • Overview
  • Resizernpm version

    • Overview
  • Routernpm version

    • Overview
  • Corenpm version

    • Overview
  • Core-Reactnpm version

    • Overview
  • DnDnpm version

    • Overview
  • Stylesnpm version

    • Overview
  • Localizenpm version

    • Overview
  • Analyticsnpm version

    • Overview
  • Datepickernpm version

    • Overview
  • Icons-generatornpm version

    • Overview
  • Smart-addressnpm version

    • Overview
  • E2Enpm version

    • Overview
  • E2E-Playwrightnpm version

    • Overview

@uxf/wysiwyg

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).

When to use

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.

Installation

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: @uxf/core, @uxf/core-react, @uxf/ui
  • Plate: platejs (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().

Required CSS

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.

Required translations

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).

Quick start

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()]);

Content model

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.

API

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.

WysiwygEditor

import { WysiwygEditor } from "@uxf/wysiwyg";

PropTypeRequiredDescription
idstringyesUnique Plate instance id. Must be unique per editor on the page.
initialValueWysiwygContent | undefinedyesDocument the editor is seeded with on mount.
onChange(value: WysiwygContent) => voidyesCalled on every edit with the full document.
pluginsUxfPlatePlugin[]yesBuild with createAllPluginsWithUi / createPluginsWithUi.
classNamestringnoClass on the editor root wrapper.
editablePropsTEditableProps<WysiwygContent>noPassed to Plate's editable area (placeholder, readOnly, autoFocus, spellCheck, className, …). Defaults: autoFocus: false, readOnly: false, spellCheck: false, localized placeholder.
editorRefForwardedRef<WysiwygEditorHandle>noImperative handle exposing focus(), which selects the end of the document and then focuses the editable area.
customPluginsToolbarButtonsReactNodenoExtra toolbar buttons appended to the built-in ones.
toolbarLeftElementReactNodenoNode rendered at the toolbar's left edge.
toolbarRightElementReactNodenoNode rendered at the toolbar's right edge.

Plugin builders

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.

ExportSignatureNotes
createPluginsWithUi(plugins: Array<WysiwygPlugin | WysiwygRecursivePlugin<string>>, options?: { overrideByKey?: MyOverrideByKey }) => UxfPlatePlugin[]Compose a custom subset.
createAllPluginsWithUi(options: CreateAllPluginsOptions) => UxfPlatePlugin[]Enable every plugin.
createHeadingsPluginWithUi(options?: HeadingsPluginOptions) => WysiwygRecursivePlugindisabledLevels?: (1..6)[].
createBoldPluginWithUi() => WysiwygPlugin
createItalicPluginWithUi() => WysiwygPlugin
createUnderlinePluginWithUi() => WysiwygPlugin
createCodePluginWithUi() => WysiwygPlugin
createHighlightPluginWithUi(color?: CSSProperties["color"]) => WysiwygPluginDefaults to twColors.yellow[300].
createBlockquotePluginWithUi() => WysiwygPlugin
createListPluginWithUi() => WysiwygRecursivePluginRenders ul / ol / li.
createLinkPluginWithUi() => WysiwygPlugin
createImagePluginWithUi(options: UxfImagePluginOptions) => WysiwygPluginSee options below.
createVideoPluginWithUi() => WysiwygPlugin
createButtonPluginWithUi() => WysiwygPlugin

CreateAllPluginsOptions:

KeyTypeRequiredDescription
imageUxfImagePluginOptionsyesImage plugin options (pass {} to enable without upload handlers).
headingsHeadingsPluginOptionsnoe.g. { disabledLevels: [1] }.
highlightColorCSSProperties["color"]noHighlight mark color.

UxfImagePluginOptions (extends Plate's MediaPlugin):

KeyTypeDescription
uploadImage(file: File) => Promise<FileResponse>Upload handler; returns a FileResponse (@uxf/core/types).
getImageUrl(file: FileResponse) => stringResolves the display URL for an uploaded file.
disableUploadOnPasteImageUrlbooleanDisable auto-upload when an image URL is pasted.
disableUploadOnPasteImagebooleanDisable auto-upload when an image blob is pasted.

Hooks

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.

Utilities

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")

Types

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).

Gotchas

  • Client component only. WysiwygEditor is marked "use client" and renders a Loader until it has mounted on the client; it cannot render on the server.
  • Uncontrolled value. 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.
  • Build plugins with the provided factories. Pass only the results of createPluginsWithUi / createAllPluginsWithUi to plugins; they inject the required base plugins. Do not hand-assemble raw Plate plugin arrays.
  • CSS is required and Tailwind-dependent. Without @uxf/wysiwyg/styles/styles.css the editor is unstyled; the file relies on your Tailwind/PostCSS setup and @uxf/ui's uxf-typo-* utilities.
  • Translations are required. Register @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.
  • Pinned peers. Plate/Slate and @uxf/* peer versions are pinned exactly (see package.json); mismatched versions will not work.

Links

  • Plate documentation — the underlying editor framework.
  • Slate documentation — the underlying document model.
  • Package homepage: gitlab.com/uxf-npm/wysiwyg
Open in new tab
Open in new tab