Use the editor in your app
Let your app’s users write rich text with the same editor as the admin, save it to a richtext.Field, and restyle it.
Your app’s users can write rich text with the editor the admin uses. It edits exactly the document
a richtext.Field stores, so your app saves it through the generated client like any other value.
Install the editor
Add the package to your SvelteKit app:
npm install @riducms/plugin-richtextbun add @riducms/plugin-richtextpnpm add @riducms/plugin-richtextyarn add @riducms/plugin-richtextThe editor needs no Vite plugin, preprocessing or Sass. It ships its own styles and loads them when it renders.
Edit a field
Import RichTextEditor from @riducms/plugin-richtext/editor and bind it to the field’s value.
This page loads a post on the server and saves its body from the browser with the client from
SvelteKit:
<script lang="ts">
import { RichTextEditor } from '@riducms/plugin-richtext/editor';
import { ridu } from '$lib/ridu';
let { data } = $props();
const client = ridu.use();
// The editor reads the document when it mounts and writes each edit back.
// svelte-ignore state_referenced_locally
let body = $state.raw(data.post.body ?? null);
async function save() {
await client.update('posts', data.post.id, { body });
}
</script>
<RichTextEditor
bind:value={body}
label="Body"
features={['links', 'lists']}
toolbar="fixed"
placeholder="Write what your readers should know"
/>
<button onclick={save}>Save</button>Pass the same features as the Go field, such as richtext.FeatureLinks and
richtext.FeatureLists here. The editor then offers only what the field accepts, and every edit
it writes passes the field’s validation. See Rich text features.
The editor reads value when it mounts. To replace the document from outside, such as after
discarding changes, remount the editor with a {#key} block. You can also pass value one way and
follow onchange, which receives each accepted document.
The editor opens text, headings, quotes, links, lists, code and dividers, within the features you
pass. A document holding anything else, such as uploads, relationships or blocks, which only the
admin can edit, shows a message and an Export document JSON button instead of the editor.
Save it with a form
Give the editor a name to submit its document with a form, like a native input: it adds a hidden
field holding the document as JSON. Without JavaScript, the form submits the saved document.
<form method="POST" use:enhance>
<RichTextEditor
bind:value={body}
name="body"
label="Body"
features={['links', 'lists']}
/>
<button>Save</button>
</form>import { fail } from '@sveltejs/kit';
import { RiduError } from '@riducms/sdk';
export const actions = {
default: async ({ locals, params, request }) => {
const field = String(
(await request.formData()).get('body') ?? ''
);
let body;
try {
// An editor with no document submits an empty field.
body = field === '' ? null : JSON.parse(field);
} catch {
return fail(400, { issues: ["The body isn't JSON."] });
}
try {
// Ridu checks the document against the field.
await locals.ridu.update('posts', params.id, { body });
} catch (error) {
if (!(error instanceof RiduError)) throw error;
return fail(error.status, {
issues: error.issues.map((issue) => issue.message)
});
}
}
};Turn Markdown into a document
convertMarkdownToLexical turns Markdown, such as a language model’s answer or imported content,
into the document the editor’s Markdown shortcuts would make. It runs on the server or in the
browser, without the editor:
import { convertMarkdownToLexical } from '@riducms/plugin-richtext/markdown';
const body = convertMarkdownToLexical(answer, {
features: ['links', 'lists']
});
await locals.ridu.update('posts', id, { body });Pass the Go field’s features. Markdown for anything else, such as a code block in a field without
code, stays text, and so does a link with an unsafe URL. To show the document in an open editor,
remount the editor with a {#key} block, because it reads value when it mounts.
Choose the toolbar
toolbar sets where formatting controls appear:
"floating", the default, shows them over selected text."fixed"pins them above the text, where they stay in view as the document scrolls."both"does both.
hideGutter, hideDraggableBlockElement, hideAddBlockButton and hideInsertParagraphAtEnd
match the Go field’s layout options. readonly shows the
document without editing controls, and disabled blocks editing for now, for example while your
app saves.
Translate the editor
The editor’s buttons, menus and messages are in English, French and Arabic. Set lang to your
page’s language, and dir for right-to-left text; other languages use English. Replace any
message with messages:
<RichTextEditor
bind:value={body}
label="Note"
lang="fr"
messages={{ 'editor.placeholder': 'Écrivez une note' }}
/>The editor reads lang, dir and messages when it mounts.
Restyle the editor
The editor’s colors, fonts and main sizes are --ridu-richtext-* custom properties. Each one
follows your app’s theme when it uses the same names as shadcn, such as --foreground,
--background, --border, --popover and --primary, and otherwise falls back to a neutral
light theme. The editor’s text inherits your page’s font.
Set the properties on :root to restyle the editor, including its menus and dialogs. Write them
outside any @layer, such as Tailwind’s base: the editor’s defaults sit in its own cascade layer,
which beats layers your app declared before it.
:root {
--ridu-richtext-link: #b45309;
--ridu-richtext-radius: 8px;
--ridu-richtext-font: 'Source Serif 4', serif;
}| Property | Styles |
|---|---|
--ridu-richtext-text, -text-muted, -placeholder |
Text, secondary text such as icons, and the placeholder |
--ridu-richtext-link, -code, -danger |
Links, inline code, and error messages |
--ridu-richtext-background, -surface |
The fixed toolbar, and menus, popovers and code blocks |
--ridu-richtext-surface-hover, -surface-active |
Hovered and pressed controls |
--ridu-richtext-border, -invalid, -accent |
Lines, the invalid state, and where a dragged block will land |
--ridu-richtext-radius, -shadow |
Corners, and the shadow under menus |
--ridu-richtext-focus, -focus-offset |
The keyboard focus outline |
--ridu-richtext-font, -heading-font, -ui-font, -mono-font |
The text, headings, menus and toolbars, and code |
--ridu-richtext-font-size, -line-height |
The text’s size and spacing |
--ridu-richtext-ui-font-size, -ui-line-height |
Menus and toolbars |
--ridu-richtext-gutter |
The space beside the text that holds each block’s handle |
--ridu-richtext-target |
The size of toolbar buttons and menu options |
--ridu-richtext-sticky-offset |
How far below the top the fixed toolbar sticks |
If your app compiles Sass, configure the defaults instead. Each variable sets the property of the same name:
@use '@riducms/plugin-richtext/editor.scss' with (
$radius: 10px,
$background: #fffbf2
);Custom properties set on an element around the editor style its text and fixed toolbar, but not the floating toolbar, menus and dialogs, which render at the end of the page.
Inside the editor and its popups, your app’s resets and global rules outside a cascade layer,
such as ul { list-style: none } or [type='button'] { ... }, are rolled back, so lists keep
their bullets under a reset such as UnoCSS’s. To change one of the editor’s rules, write yours in
Ridu’s app layer, which comes after the editor’s. Declare Ridu’s layers after any stylesheet you
import, such as Tailwind’s, so they follow its reset’s layer whichever stylesheet loads first:
@import 'tailwindcss';
@layer ridu, ridu-plugins, app;
@layer app {
.post-form .ridu-richtext-editor blockquote {
border-inline-start-color: #b45309;
}
}A class you pass to the editor styles its frame from anywhere.
Render on the server
The editor is safe to render with SvelteKit’s server rendering. On the server it renders the saved
document with the same styles, and the browser swaps in the editor without moving the page. On a
narrow screen, a fixed toolbar that wraps onto a second row adds that row when the editor mounts. To
show a saved document elsewhere, render it with RichText
instead, which loads no editor.
The editor’s JavaScript is about 205 KB compressed, and it loads when the editor first renders. Keep it off pages that only display content.
Use it on phones
On a touch screen, toolbar buttons and menu options are 44 pixels, the size of a fingertip. A fixed
toolbar stays in view while the on-screen keyboard is open. Pass hideGutter on narrow screens to
give the text the full width, and set --ridu-richtext-sticky-offset to your header’s height if it
stays at the top of the page.