RoadieRoadie
  • Home
  • Brand
  • Foundations
  • Content
  • Tokens
  • Components
  • Charts
  • Widgets
  • Appearance
HomeBrandFoundations
Appearance

Components

  • Navigation

    • Breadcrumb
    • LinkBeta
    • NavigatorAlpha
    • Steps
    • Tabs
  • Layout

    • Accordion
    • CollapsibleBeta
    • PaneAlpha
    • ScrollArea
    • Separator
  • Actions

    • Button
    • IconButton
    • ToggleBeta
    • Toggle groupBeta
    • ToolbarBeta
  • Fields

    • EditorAlpha
    • Field
    • Fieldset
    • Input
    • Label
    • Number fieldBeta
    • OTP fieldBeta
    • Query fieldAlpha
    • Textarea
  • Choices

    • Autocomplete
    • CheckboxBeta
    • Combobox
    • Radio group
    • Select
    • SliderBeta
    • SwitchBeta
  • Date and time

    • CalendarBeta
    • CalendarTile
    • Countdown
    • Dashboard periodBeta
    • Date fieldBeta
    • Date pickerBeta
    • Date range pickerBeta
    • DateTime
    • Duration
    • Time fieldBeta
  • Collections

    • Card
    • Carousel
    • DataTableBeta
    • ListAlpha
    • Marquee
    • RecordGridAlpha
    • RecordTableAlpha
    • SortableAlpha
    • TableBeta
  • Overlays

    • Dialog
    • Drawer
    • MenuBeta
    • Popover
    • ToastBeta
    • Tooltip
  • Status

    • Badge
    • CalloutBeta
    • DeltaBeta
    • Empty state
    • MeterBeta
    • ProgressBeta
    • SkeletonBeta
    • SparklineBeta
    • StatTileBeta
  • Media & brand

    • AvatarBeta
    • Icon Tile
    • Image
    • Logo
    • PatternBeta
    • QR codeBeta
    • SpotIllustration
  • Text

    • Code
    • Highlight
    • KbdBeta
    • Mark
    • Prose

Editor

Status
Alpha
Added
Editor 0.1.020 Oct 2026
Docs updated
20 Oct 2026

A rich-text field that saves its content as an EditorValue.

Import

import { Editor } from '@oztix/roadie-editor/editor'

Add the editor's CSS after the components' CSS. It places the toolbar and styles the placeholder and selection:

@import '@oztix/roadie-core/css';
@import '@oztix/roadie-components/css';
@import '@oztix/roadie-editor/css';

Examples

Default

The toolbar at the top of the editor holds the preset's controls, and each control's tooltip shows its shortcut.

<Editor
aria-label='Event description'
placeholder='Tell people about the night'
/>

Toolbar

The toolbar is a bar at the top of the editor, a size smaller than the text. It scrolls away with a long description, and looks the same whether you're editing or not, so the text never moves. On a phone or touch screen its controls scroll sideways on one row when they don't fit.

<Editor
aria-label='Event description'
preset='rich'
defaultValue={{
version: 1,
preset: 'rich',

With content

defaultValue takes an EditorValue from @oztix/roadie-editor/schema. The basic preset holds paragraphs, bulleted and numbered lists, bold, italic, underline, and links.

<Editor
aria-label='Event description'
defaultValue={{
version: 1,
preset: 'basic',
doc: {

Links

Select text and press Mod+K, or use the Link control, to link it. Type a web address such as oztix.com.au and the editor adds https://. With nothing selected, saving inserts the address as linked text. Typing or pasting a web address links it as you go.

The Link, Embed, Edit link, and Alt text popovers are one row: an input, a check button that saves, and Close. Enter saves and Escape closes. Editing a link adds Remove link between the two.

<Editor
aria-label='Event description'
defaultValue={{
version: 1,
preset: 'basic',
doc: {

A link that could run script, such as a javascript: address, never reaches the saved value. The link editor refuses it, pasted HTML keeps only its text, and migrate drops it from a stored value.

Rich preset

preset='rich' adds headings 2 to 4, quotes, and dividers to the basic preset, for an event page. The Block type menu leads the toolbar and names the block at the caret, then come the basic controls, from Bold, Italic, and Underline to the lists. Markdown works as you type: ## , ### , and #### start a heading, > a quote, and --- a divider.

<Editor
aria-label='Event description'
preset='rich'
defaultValue={{
version: 1,
preset: 'rich',

The page owns its h1, so pasted h1 becomes a heading 2, and h5 and h6 become a heading 4.

Images

The rich preset holds images. Pass onUploadImage to add the Image control: it uploads a file the person picks, pastes, or drops, and resolves to where the image is served. A placeholder shows while it uploads, and a rejection's message shows in Field.ErrorText. Without onUploadImage there's no Image control, and image files are ignored.

function uploadImage(file) {
// Send the file to your storage and resolve to where it's served.
return new Promise((resolve) =>
setTimeout(() => resolve({ src: '/brand/photography/04.webp' }), 1000)
)
}

An image saves only its src and alt. The src must be https or a path on the same site, such as /images/poster.webp; any other address is refused when it's inserted, pasted, or loaded.

Click an image, or move onto it with the arrow keys, to edit it in place. A toolbar by the image has Alt text, Replace image (with onUploadImage, which uploads the new file the same way and keeps the alt text), and Remove image. An image with no alt text shows a No alt text badge until it has some; Enter on a selected image opens Alt text too.

Embeds

The rich preset embeds videos and tracks from YouTube, Vimeo, Spotify, and TikTok. Paste a share link alone on an empty line, or use the Embed control. The editor shows each player at its own shape, under a cover, so a click selects it rather than playing it, and Backspace or Delete removes it. A player with a ratio is no taller than 36rem, so a portrait one, such as TikTok's, stays narrow at the start of the line, in the editor and in /render.

<Editor
aria-label='Event description'
preset='rich'
defaultValue={{
version: 1,
preset: 'rich',

An embed saves only its provider, its id, and the params its provider reads, never a URL, so /render builds the player's address from defaultEmbedProviders and a provider can move its player without a migration. Players run in a sandboxed iframe, and a look-alike host such as youtube.com.evil.test is never embedded.

  • embedProviders sets the providers the editor reads links from, starting from defaultEmbedProviders. Pass the same list to /render, or it drops the embeds it doesn't know.
  • resolveUrl turns a link no provider reads, such as a TikTok short link, into one it can, when it's entered in the Embed control. Resolve it on your server.

Click an embed, or move onto it with the arrow keys, to edit it in place. A toolbar by the embed has Edit link and Remove embed. Edit link starts from the embed's share link, from its provider's optional shareUrl, and replaces the embed with the new link's player. It refuses a link no provider reads, as the Embed control does.

Tables

The rich preset holds tables. The Table control inserts one of three rows and three columns, with a header row. Tab moves to the next cell, adding a row after the last, and Shift+Tab moves back, leaving the editor from the first cell. While the caret is in a table, a toolbar at its start edge, above it, or below it when a table comes first, has its actions: a Row menu with Insert above, Insert below, and Delete row, a Column menu with Insert before, Insert after, and Delete column, the Header row toggle, and Delete table.

<Editor
aria-label='Event description'
preset='rich'
defaultValue={{
version: 1,
preset: 'rich',

A cell holds paragraphs, with bold, italic, underline, and links, but no headings, lists, images, embeds, or other tables, so a list pasted into a cell, as from Word, keeps each item as a paragraph. A table saves no widths or styles, only each cell's colspan and rowspan, and a wide one scrolls sideways in the editor and in /render, so it never widens the page on a phone.

Emphasis

emphasis sets the frame. The toolbar takes its own emphasis, so it's the same in each.

EmphasisFrameUse it for
normal (default)A field's border, fill, and focus ringA form, beside other fields
subtleA tint with no border, and a focus ringA form on a busy surface, where a border adds noise
subtlerNone: the content sits on the pageContent edited in place, such as an event page's sections
<Editor aria-label='Normal emphasis' emphasis='normal' placeholder='Normal emphasis' />
<Editor aria-label='Subtle emphasis' emphasis='subtle' placeholder='Subtle emphasis' />

Subtler

emphasis='subtler' drops the border, fill, and padding, so the text lines up with the page around it and editing looks like reading. The caret shows where focus is. Its skeleton is lines of text, not a box.

<h2 id='about-the-event' className='text-display-ui-4 text-strong'>
About the event
</h2>
<Editor
aria-labelledby='about-the-event'
emphasis='subtler'

An invalid subtler editor has no border to turn red, so it draws a danger edge on its start side, just outside the text, and Field.ErrorText says what's wrong. Give it room at the start: a container that clips its overflow hides the edge. A disabled one dims as a disabled field does.

Toolbar emphasis

Editor.Toolbar takes Toolbar's emphasis for its bar, the same on every screen and whether you're editing or not. It's independent of the editor's own emphasis. To change it, pass the parts yourself:

EmphasisBarUse it for
normal (default)A visible border on the normal surfaceMost editors, in a form or on the page
subtleA tint with no borderA busy surface, where a border adds noise
subtlerNo surface, just the controlsA quiet bar, such as over a subtler editor
raisedA translucent raised surfaceAn editor on a tinted surface
floatingA translucent surface with a deep shadowA bar that should read as above the content
<Editor aria-label='Subtler toolbar' placeholder='Subtler toolbar'>
<Editor.Toolbar emphasis='subtler' />
<Editor.Content />
</Editor>
<Editor aria-label='Raised toolbar' placeholder='Raised toolbar'>
<Editor.Toolbar emphasis='raised' />

Sizes

size sets the body and heading sizes to match Prose, so editing looks like the published page.

<Editor aria-label='Small' size='sm' placeholder='Small' />
<Editor aria-label='Medium' size='md' placeholder='Medium (default)' />
<Editor aria-label='Large' size='lg' placeholder='Large' />

States

{/* Default */}
<Editor aria-label='Event description' placeholder='Click to focus' />
{/* Read-only */}
<Editor
aria-label='Event description'
readOnly

Controlled

onValueChange gets the whole EditorValue on each change. Save it as JSON, never as HTML, and pass a stored value through migrate before it goes back in. The editor takes its preset from the value. A preset prop that differs reads the value into that preset, keeping the text of what it can't hold.

function DescriptionEditor() {
const [value, setValue] = useState()
return (
<Editor
aria-label='Event description'
placeholder='Tell people about the night'

Composition

Field

In a Field, the editor is named by Field.Label and described by Field.HelperText, or by Field.ErrorText while the field is invalid. It takes invalid, required, and disabled from the field.

{/* Default */}
<Field>
<Field.Label>Event description</Field.Label>
<Editor placeholder='Tell people about the night' />
<Field.HelperText>Shown on the event page, under the lineup.</Field.HelperText>
</Field>

Forms

name adds a hidden input that holds the EditorValue as JSON, so a native form submits it. A disabled editor leaves it out, as a disabled input does.

<form action='/events/ochre-kite-weekender' method='post'>
<Field>
<Field.Label>Event description</Field.Label>
<Editor name='description' defaultValue={savedDescription} />
</Field>
<Button type='submit'>Save</Button>

Custom controls

With no children, Editor renders Editor.Toolbar then Editor.Content. Pass them yourself to add controls: Editor.Toolbar renders the preset's controls, then its children. A Toolbar.Button child joins the toolbar's arrow order, and useEditorContext() gives it the editor to run commands on.

function DoorsTimeControl() {
const { editor } = useEditorContext()
return (
<Toolbar.Button
onClick={() =>
editor?.chain().focus().insertContent('Doors open 7pm. ').run()

Rendering saved content

@oztix/roadie-editor/render turns a saved EditorValue into the page's content, with none of the editor's code. It works in a server component and on the client.

import { renderEditorHtml, renderEditorValue } from '@oztix/roadie-editor/render'
  • renderEditorValue(value, { size, className }) returns React elements in Prose at size, so pass the editor's size. Links render through Link, so internal hrefs route through the Link given to RoadieProvider.
  • renderEditorHtml(value, { size, className }) returns the same content as an HTML string in a .prose element, for a consumer that isn't React. External links get target="_blank" and rel="noopener noreferrer", as Link gives them.
  • Both render images with loading="lazy", and embeds as a lazy, sandboxed iframe whose address they rebuild from the provider. Pass embedProviders if the editor has its own, or embeds from any other provider drop.
  • Both render tables in a .prose-scroll, so a wide table scrolls on a phone rather than widening the page, with a first row of header cells in a thead, unless one spans down, as a thead would clip it.

Both run migrate first, so a value from a newer package, or with a preset this one doesn't know, throws. They render only the nodes, marks, and attributes the value's preset defines, and keep only the text of the rest. A link to an unsafe href, such as javascript:, keeps its text and loses its link. Neither renders raw HTML, so the saved JSON stays the source of truth.

The same value in the editor, then rendered:

const description = {
version: 1,
preset: 'basic',
doc: {
type: 'doc',
content: [

Guidelines

  • The editor's code loads after the page, so Editor shows a skeleton the height of the empty editor until then, and always on the server. Call preloadEditor() on the hover or focus that leads to an editor to start loading it sooner.
  • Put the editor in a Field with a Field.Label. Outside one, label it with aria-label or aria-labelledby; a <label for> can't name editable content.
  • Add a custom control only for something the preset can't do. A preset's controls come with its schema, so each one works on what the editor can save.

Accessibility

  • The editable area is a textbox with aria-multiline. In a Field it's named by Field.Label through aria-labelledby, unless Editor has its own aria-label or aria-labelledby, and described by the helper or error text through aria-describedby.
  • An invalid or required field sets aria-invalid or aria-required. A read-only editor sets aria-readonly, and a disabled one aria-disabled.
  • A subtler editor has no focus ring: the caret shows focus. While invalid it shows a danger edge at its start, beside aria-invalid and the error text.
  • Tab moves into and out of the editor. In a list, Tab nests an item under the one before it and Shift+Tab lifts it back out.
  • The toolbar is a toolbar named Formatting, with aria-controls set to the editable area. It takes one Tab stop, and the arrow keys move between its controls, scrolling the one with focus into view on a phone. Bold, italic, underline, link, and the lists are toggles that report aria-pressed for the text at the caret. Undo and Redo are disabled when there's nothing to undo or redo, and stay focusable.
  • A selected image or embed has its own toolbar, named Image actions or Embed actions, with the same keys as Formatting. It shows only while the node is selected and the editor has focus, and never while read-only or disabled. Each action is one step to undo.
  • An image's alt text is edited in a popover with a named input, opened by Enter on a selected image or its Alt text action. Escape closes it and returns focus to the image. An upload in progress is a status, and an upload error shows in Field.ErrorText, or under the editor outside a Field.
  • An embed's player is titled with its provider and id, and takes no Tab stop in the editor. Arrow keys select an embed or an image, and Backspace or Delete removes it. The Embed control's popover has a named input, and a link it can't embed shows its error under the row.
  • While the caret is in a table, a toolbar named Table actions shows above it, or below it when above would cover Formatting, with the same keys as Formatting, and never while read-only or disabled. Row and Column open menus whose items are menuitems, and a menu returns focus to the cell when it closes. Header row is a toggle that reports aria-pressed, and Delete row and Delete column are disabled when they'd delete the last one. Alt+F10 in a table moves focus to its first action, and Escape back to the cell. In a table, Tab and Shift+Tab move between cells instead of nesting. Tab in the last cell adds a row, and Shift+Tab in the first leaves the editor, as it does from any text.
  • In the rich preset, the Block type menu's trigger is named for the block at the caret, such as "Block type: Heading 2". Its items are radio items, with the current type checked, and choosing one puts focus back in the text.
  • Each control carries its shortcut in aria-keyshortcuts.
  • The link editor is a popover by the selection, with an input named Link. It takes focus when it opens, Enter or the Save link button saves, and Escape or the Close button closes it and puts the caret back where it was. A refused address shows its error under the row, and focus stays in the input.
  • Each popover's icon buttons are named for what they do: Save link, Embed link, Save alt text, Remove link, and Close. A check with nothing to save is disabled but keeps its Tab stop.
KeysAction
Mod+BBold
Mod+IItalic
Mod+UUnderline
Mod+KAdd, change, or remove a link
Mod+Shift+8Bulleted list
Mod+Shift+7Numbered list
Mod+Alt+0Paragraph, in the rich preset
Mod+Alt+2 to Mod+Alt+4Heading 2 to 4, in the rich preset
Mod+Shift+BQuote, in the rich preset
EnterOn a selected image, edit its alt text
TabIn a table, move to the next cell, adding a row after the last
Shift+TabIn a table, move to the cell before, or leave the editor from the first
Mod+ZUndo
Mod+Shift+ZRedo
Shift+EnterLine break
Alt+F10Move focus to the toolbar's first enabled control, a selected image's or embed's first action, or in a table its first table action
EscapeIn a toolbar, move focus back to the text, where the caret was, or to the selected image or embed

Mod is ⌘ on Apple devices and Ctrl elsewhere, and Alt is ⌥ on Apple devices.

Hooks

useEditorContext

function useEditorContext(): { editor: Editor | null }

Returns the Tiptap Editor of the nearest Editor, for a custom control in Editor.Toolbar or elsewhere among its children. editor is null for a moment after the editor's code loads, until it mounts. It throws outside an Editor.

API reference

Editor

preset"basic" | "rich"

The nodes and marks the document may hold. A value in another preset is read into this one, keeping the text of what it can't hold.

Defaults to the value's preset, or 'basic'.

valueEditorValue

The document, for a controlled editor.

defaultValueEditorValue

The starting document, for an uncontrolled editor.

onValueChange((value: EditorValue) => void)

Called with the whole document on each change.

placeholderstring

Shown while the document is empty.

readOnlyboolean
disabledboolean

Takes no input and dims, as does a disabled `Field` around it.

namestring

Submits the value as JSON with a native form, under this name.

size"sm" | "md" | "lg"

Body size, matching `Prose`'s sizes.

Defaults to 'md'.

emphasis"normal" | "subtle" | "subtler"

The frame. `normal` and `subtle` match `Textarea`'s; `subtler` has none, for content edited in place on the page.

Defaults to 'normal'.

classNamestring
onUploadImage((file: File) => Promise<{ src: string; alt?: string; }>)

Uploads an image the person picks, pastes, or drops, and resolves to where it's served: https, or a path on this site. Without it the editor has no image control and ignores image files. A rejection's message shows in `Field.ErrorText`.

embedProvidersreadonly EmbedProvider[]

The providers the editor embeds from, matching what renders the value.

Defaults to defaultEmbedProviders.

resolveUrl((url: string) => Promise<string>)

Resolves a link no provider reads, such as a short link, to one it can. Called when one is entered in the Embed control; run it on your server.

aria-labelstring
aria-labelledbystring
childrenReactNode

Replaces the default parts, `<Editor.Toolbar />` then `<Editor.Content />`.

Editor.Content

The editable area: a multiline textbox styled as `.prose` at the editor's size.

No additional props. It forwards all standard HTML attributes to the underlying element.

Editor.Toolbar

The preset's formatting controls, then `children`, at the editor's size. Alt+F10 in the content moves focus here, and Escape moves it back.

classNamestring

Classes for the toolbar's surface, its outermost element.

emphasis"normal" | "subtle" | "subtler" | "raised" | "floating"

The surface behind the toolbar. `subtler` has none, for a toolbar inside another surface. The buttons stay at `subtler` on every one.

Defaults to 'normal'.

Inherited from ToolbarRootProps

disabledboolean
orientation"horizontal" | "vertical"

The orientation of the toolbar.

Defaults to 'horizontal'.

loopFocusboolean

If `true`, using keyboard navigation will wrap focus to the other end of the toolbar once the end is reached.

Defaults to true.

Previous page← ToolbarNext pageField →