-
Notifications
You must be signed in to change notification settings - Fork 2
Toolbar Setup
The package supports two toolbar integration modes:
- Use the built-in toolbar through
NativeRichTextEditor(recommended). - Render
EditorToolbaryourself for custom layout or placement.
This is the recommended starting point. The toolbar is hosted natively as a keyboard accessory (iOS) or above-keyboard view (Android).
<NativeRichTextEditor
showToolbar
toolbarItems={DEFAULT_EDITOR_TOOLBAR_ITEMS}
/>;This gives you the default button set, platform-specific hosting, and minimal setup.
If the editor lives inside a larger React Native screen that scrolls, keep app-level keyboard avoidance in place. The built-in keyboard toolbar handles native hosting, but it does not replace KeyboardAvoidingView or another parent-level keyboard inset strategy for the rest of your layout.
Control where the toolbar renders with toolbarPlacement:
// Native keyboard accessory (default)
<NativeRichTextEditor showToolbar toolbarPlacement="keyboard" />
// Inline React view above the editor
<NativeRichTextEditor showToolbar toolbarPlacement="inline" />| Value | Behavior |
|---|---|
keyboard |
Attached as a native keyboard accessory (iOS) or above-keyboard view (Android). Default. |
inline |
Rendered in React above the editor. Visible even when the keyboard is closed. |
Notes:
-
keyboardis the preferred mode when you want platform-native toolbar behavior. - On Android, the editor accounts for the built-in above-keyboard toolbar inside the editor viewport.
- You should still make the surrounding screen keyboard-aware if the editor sits inside a parent
ScrollViewor a longer form. - If you use
KeyboardAvoidingViewon Android, choose akeyboardVerticalOffsetthat matches your screen layout. The right value depends on your header, safe area handling, and any extra space you keep above the keyboard.
Example screen wrapper fragment:
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={androidKeyboardOffset}
>
<ScrollView keyboardShouldPersistTaps="always">
<NativeRichTextEditor
showToolbar
toolbarPlacement="keyboard"
/>
</ScrollView>
</KeyboardAvoidingView>androidKeyboardOffset is app-specific. Derive it from the layout you already use on that screen.
These examples are component fragments. Import the hooks and types they use in your screen or component.
Use toolbarItems to define your own button list while still using the built-in host:
import { useState } from 'react';
import {
NativeRichTextEditor,
type LinkRequestContext,
} from '@apollohg/react-native-prose-editor';
export function Composer() {
const [pendingLink, setPendingLink] = useState<LinkRequestContext | null>(null);
const [linkDraft, setLinkDraft] = useState('');
const toolbarItems = [
{ type: 'mark', mark: 'bold', label: 'Bold', icon: { type: 'default', id: 'bold' } },
{ type: 'mark', mark: 'italic', label: 'Italic', icon: { type: 'default', id: 'italic' } },
{ type: 'link', label: 'Link', icon: { type: 'default', id: 'link' } },
{ type: 'separator' },
{ type: 'node', nodeType: 'hardBreak', label: 'Line Break', icon: { type: 'default', id: 'lineBreak' } },
{ type: 'node', nodeType: 'horizontalRule', label: 'Horizontal Rule', icon: { type: 'default', id: 'horizontalRule' } },
] as const;
return (
<>
<NativeRichTextEditor
showToolbar
toolbarItems={toolbarItems}
onRequestLink={(context) => {
setPendingLink(context);
setLinkDraft(context.href ?? 'https://');
}}
/>
<LinkEditorModal
visible={pendingLink != null}
value={linkDraft}
onChangeText={setLinkDraft}
onCancel={() => setPendingLink(null)}
onSubmit={() => {
if (!pendingLink) return;
const nextHref = linkDraft.trim();
if (nextHref === '') {
pendingLink.unsetLink();
} else {
pendingLink.setLink(nextHref);
}
setPendingLink(null);
}}
/>
</>
);
}LinkEditorModal is your app's UI. The package only tells you when a link should be created, edited, or removed.
Image buttons are host-driven too. Handle your picker or upload flow in onRequestImage, then call insertImage(...).
Use an action item plus onToolbarAction to add buttons with custom behavior:
const toolbarItems = [
{ type: 'mark', mark: 'bold', label: 'Bold', icon: { type: 'default', id: 'bold' } },
{
type: 'action',
key: 'insertMention',
label: 'Mention',
icon: {
type: 'platform',
ios: { type: 'sfSymbol', name: 'at' },
android: { type: 'material', name: 'alternate-email' },
fallbackText: '@',
},
},
] as const;
<NativeRichTextEditor
showToolbar
toolbarItems={toolbarItems}
onToolbarAction={(key) => {
if (key === 'insertMention') {
// your app logic here
}
}}
/>;Toolbar items address schema names directly, so custom marks and nodes work by name:
const toolbarItems = [
{
type: 'mark',
mark: 'highlight',
label: 'Highlight',
icon: {
type: 'platform',
ios: { type: 'sfSymbol', name: 'highlighter' },
android: { type: 'material', name: 'ink-highlighter' },
fallbackText: 'H',
},
},
] as const;This only works if your schema includes the highlight mark.
Heading toolbar items target schema node names indirectly by level, so the built-in schemas can toggle between paragraph and h1 through h6:
const toolbarItems = [
{ type: 'heading', level: 1, label: 'Heading 1', icon: { type: 'default', id: 'h1' } },
{ type: 'heading', level: 2, label: 'Heading 2', icon: { type: 'default', id: 'h2' } },
] as const;
<NativeRichTextEditor
showToolbar
toolbarItems={toolbarItems}
/>;If you need an imperative path, the editor ref also exposes toggleHeading(level).
Use a group item when several buttons should collapse into one slot. This is useful for heading levels, insert menus, or app-specific button clusters.
const toolbarItems = [
{
type: 'group',
key: 'headings',
label: 'Headings',
icon: { type: 'glyph', text: 'H' },
presentation: 'menu',
items: [
{ type: 'heading', level: 1, label: 'Heading 1', icon: { type: 'default', id: 'h1' } },
{ type: 'heading', level: 2, label: 'Heading 2', icon: { type: 'default', id: 'h2' } },
{ type: 'heading', level: 3, label: 'Heading 3', icon: { type: 'default', id: 'h3' } },
],
},
] as const;
<NativeRichTextEditor
showToolbar
toolbarItems={toolbarItems}
/>;Set presentation: 'expand' when you want the child buttons inserted inline after tapping the group button instead of opening a menu.
Groups can only contain actionable items. They do not support nested groups or separators.
Hyperlinks are slightly different from ordinary mark buttons because they need a URL. Use a link toolbar item and handle onRequestLink:
const toolbarItems = [
{ type: 'mark', mark: 'bold', label: 'Bold', icon: { type: 'default', id: 'bold' } },
{ type: 'link', label: 'Link', icon: { type: 'default', id: 'link' } },
] as const;
<NativeRichTextEditor
showToolbar
toolbarItems={toolbarItems}
onRequestLink={({ href, isActive, setLink, unsetLink }) => {
if (isActive && href) {
// show your edit/remove UI
}
setLink('https://example.com');
}}
/>;If you need an imperative path, the editor ref also exposes setLink(href) and unsetLink().
Image toolbar items call back into your app. Choose the image however you want, then finish by calling insertImage(...):
const toolbarItems = [
{ type: 'mark', mark: 'bold', label: 'Bold', icon: { type: 'default', id: 'bold' } },
{ type: 'image', label: 'Image', icon: { type: 'default', id: 'image' } },
] as const;
<NativeRichTextEditor
showToolbar
toolbarItems={toolbarItems}
onRequestImage={({ allowBase64, insertImage }) => {
const source = allowBase64
? 'data:image/png;base64,AAAA'
: 'https://cdn.example.com/cat.png';
insertImage(source, { alt: 'Cat', width: 320, height: 180 });
}}
/>;If you need imperative insertion, the editor ref also exposes insertImage(src, attrs?). Base64 data URIs require allowBase64Images={true}. Optional width and height attrs seed the initial size and are updated when the user resizes the image natively.
Set allowImageResizing={false} if inserted images should stay fixed.
Use EditorToolbar directly when you need full layout control. This requires wiring up the editor ref and state callbacks manually.
const editorRef = useRef<NativeRichTextEditorRef>(null);
const [activeState, setActiveState] = useState<ActiveState>({
marks: {},
markAttrs: {},
nodes: {},
commands: {},
allowedMarks: [],
insertableNodes: [],
});
const [historyState, setHistoryState] = useState<HistoryState>({
canUndo: false,
canRedo: false,
});
<>
<NativeRichTextEditor
ref={editorRef}
showToolbar={false}
onActiveStateChange={setActiveState}
onHistoryStateChange={setHistoryState}
/>
<EditorToolbar
activeState={activeState}
historyState={historyState}
onToggleMark={(mark) => editorRef.current?.toggleMark(mark)}
onToggleHeading={(level) => editorRef.current?.toggleHeading(level)}
onToggleListType={(listType) => editorRef.current?.toggleList(listType)}
onInsertNodeType={(nodeType) => editorRef.current?.insertNode(nodeType)}
onRequestImage={() => editorRef.current?.insertImage('https://cdn.example.com/cat.png')}
onRunCommand={(command) => {
if (command === 'indentList') editorRef.current?.indentListItem();
if (command === 'outdentList') editorRef.current?.outdentListItem();
}}
onUndo={() => editorRef.current?.undo()}
onRedo={() => editorRef.current?.redo()}
onToggleBold={() => editorRef.current?.toggleMark('bold')}
onToggleItalic={() => editorRef.current?.toggleMark('italic')}
onToggleUnderline={() => editorRef.current?.toggleMark('underline')}
onToggleStrike={() => editorRef.current?.toggleMark('strike')}
/>
</>;When using the generic handlers (onToggleMark, onToggleListType, onInsertNodeType, onRunCommand), they take priority over the specific built-in handlers. The specific handlers (onToggleBold, etc.) serve as fallbacks for built-in items when generic handlers are not provided.
Standalone toolbars preserve editor focus by default. EditorToolbar reports its screen frame to the focused editor, so tapping a standalone toolbar button is treated like toolbar interaction instead of an outside tap that blurs the editor. It also marks active press interactions; if native focus still drops during a toolbar press, the editor suppresses the blur callback and refocuses itself. Pass preserveEditorFocus={false} only when the toolbar is not associated with a NativeRichTextEditor or when you intentionally manage focus yourself.
If the standalone toolbar sits inside a parent ScrollView, FlatList, section list, or keyboard-aware scroll view, set that parent to keep taps while the keyboard is open. Prefer keyboardShouldPersistTaps="always" for composer-style screens. Otherwise React Native may dismiss the keyboard before the toolbar press reaches the editor, which appears as keyboard flicker.
| Need | Recommended Path |
|---|---|
| Simplest integration | Built-in toolbar via showToolbar
|
| Custom button list | Built-in toolbar with toolbarItems
|
| Toolbar visible without keyboard | toolbarPlacement="inline" |
| Custom layout or placement | Standalone EditorToolbar
|
| App-defined buttons | Either path, using action items |
- Styling Guide for toolbar styling tokens
- EditorToolbar Reference for exact toolbar types
- NativeRichTextEditor Reference for all component props
Copyright © 2026 Apollo Health Group Pty. Ltd.