Skip to main content

Rich Text Editor

The Ever Works Template includes a fully integrated rich text editor powered by TipTap, a headless editor framework built on top of ProseMirror. The editor supports content formatting, image uploads, task lists, and bidirectional synchronization with form data.

Architecture Overview

The editor system is organized into a modular structure under lib/editor/:

Directory / FilePurpose
providers/editor-provider.tsxReact context provider that initializes the TipTap editor with all extensions
hooks/use-tiptap-editor.tsHook for accessing the editor instance from context or direct prop
hooks/use-editor.tsSimplified context consumer hook
hooks/use-editor-sync.tsBidirectional sync between editor and form state
contents/editor-content.tsxWrapper component for rendering the editor content area
contents/use-editor-toolbar.tsHook for managing toolbar state (mobile/desktop, views)

TipTap Extensions

The editor is configured with a comprehensive set of extensions through the EditorContextProvider:

// lib/editor/providers/editor-provider.tsx
const extensions = useMemo(() => [
StarterKit?.configure({
horizontalRule: false,
link: { openOnClick: false, enableClickSelection: true }
}),
HorizontalRule,
TextAlign?.configure({ types: ['heading', 'paragraph'] }),
ImageUploadNode?.configure({
accept: 'image/*',
maxSize: MAX_FILE_SIZE,
limit: 3,
upload: handleImageUpload,
onError: (error) => console.error('Upload failed:', error)
}),
TaskList,
TaskItem?.configure({ nested: true }),
Highlight?.configure({ multicolor: true }),
Image,
Typography,
Superscript,
Subscript,
Selection
], []);

Extension Reference

ExtensionDescription
StarterKitCore formatting: bold, italic, headings, lists, blockquotes, code blocks, links
HorizontalRuleCustom horizontal rule insertion
TextAlignText alignment (left, center, right, justify) for headings and paragraphs
ImageUploadNodeDrag-and-drop image upload with size limits and file count restrictions
TaskList / TaskItemInteractive task/checkbox lists with nested support
HighlightText highlighting with multi-color support
ImageStandard image embedding via @tiptap/extension-image
TypographyAutomatic typographic replacements (smart quotes, dashes)
Superscript / SubscriptSuperscript and subscript text formatting
SelectionEnhanced selection handling

Editor Context Provider

The editor is initialized through a React context provider. Wrap your component tree with EditorContextProvider to make the editor available:

import { EditorContextProvider } from '@/lib/editor/providers';

function MyPage() {
return (
<EditorContextProvider>
<MyEditorComponent />
</EditorContextProvider>
);
}

The provider creates the editor with the following configuration:

  • immediatelyRender: false -- Prevents SSR hydration mismatches
  • shouldRerenderOnTransaction: false -- Performance optimization to reduce unnecessary re-renders
  • Accessibility attributes -- Autocomplete, autocorrect, and ARIA labels are configured
  • Minimum height -- min-h-96 ensures a usable editing area

Accessing the Editor Instance

Using useTiptapEditor

The primary hook for accessing the editor supports both direct injection and context fallback:

import { useTiptapEditor } from '@/lib/editor/hooks/use-tiptap-editor';

function MyToolbar({ editor: externalEditor }) {
const { editor, editorState, canCommand } = useTiptapEditor(externalEditor);

if (!editor) return null;

return (
<div>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!canCommand?.().toggleBold()}
>
Bold
</button>
</div>
);
}

Using useEditor

A simpler hook that strictly requires being within the EditorContextProvider:

import { useEditor } from '@/lib/editor/hooks/use-editor';

function EditorStatus() {
const editor = useEditor();
// Throws if not inside EditorContextProvider
return <span>{editor?.isFocused ? 'Editing' : 'Idle'}</span>;
}

Content Synchronization

The useEditorSync hook handles bidirectional synchronization between the TipTap editor and form state. This is essential for integrating the editor into forms managed by React state or form libraries.

Basic Sync

import { useEditorSync } from '@/lib/editor/hooks/use-editor-sync';

function DescriptionEditor({ editor }) {
const [content, setContent] = useState('');

useEditorSync({
editor,
content,
onContentChange: setContent,
fieldName: 'description',
enableLogging: false
});

return <EditorContent editor={editor} />;
}

Form Field Sync

For forms with multiple fields, useEditorFieldSync provides a shorthand:

import { useEditorFieldSync } from '@/lib/editor/hooks/use-editor-sync';

function ItemForm({ editor }) {
const [formData, setFormData] = useState({
name: '',
description: '',
notes: ''
});

// Synchronizes formData.description with the editor
useEditorFieldSync(editor, formData, 'description', setFormData);

return <EditorContent editor={editor} />;
}

Sync Behavior

DirectionTriggerCondition
Form to Editorcontent prop changesOnly when editor is empty or content differs significantly
Editor to Formupdate and blur eventsAlways propagates the current HTML to the form callback

The hook avoids infinite update loops by checking whether the editor content is empty or substantially different before overwriting.

Editor Content Component

The EditorContent wrapper handles word-wrapping and ProseMirror styling:

import { EditorContent } from '@/lib/editor/contents/editor-content';

function MyEditor({ editor }) {
return (
<EditorContent
editor={editor}
toolbar={<MyToolbar editor={editor} />}
className="prose dark:prose-invert"
onPaste={handlePaste}
onDrop={handleDrop}
/>
);
}

The component applies CSS rules for proper text wrapping:

  • break-words on the ProseMirror container
  • whitespace-pre-wrap for preserving whitespace
  • overflow-wrap-anywhere for preventing horizontal overflow

Toolbar Management

The useEditorToolbar hook manages toolbar state including mobile responsiveness:

import { useEditorToolbar } from '@/lib/editor/contents/use-editor-toolbar';

function Toolbar({ editor }) {
const { rect, toolbarRef, isMobile, mobileView, setMobileView } = useEditorToolbar(editor);

return (
<div ref={toolbarRef}>
{isMobile ? (
<MobileToolbar view={mobileView} onViewChange={setMobileView} />
) : (
<DesktopToolbar />
)}
</div>
);
}

The toolbar supports three mobile view modes: "main", "highlighter", and "link".

Image Upload

The editor supports image uploads through the ImageUploadNode extension:

SettingValue
Accepted typesimage/*
Max file sizeDefined by MAX_FILE_SIZE constant
Max images per upload3
Upload handlerhandleImageUpload utility function

Images can be uploaded via drag-and-drop or the toolbar upload button.

Key Files

FilePath
Editor Providerlib/editor/providers/editor-provider.tsx
TipTap Editor Hooklib/editor/hooks/use-tiptap-editor.ts
Editor Sync Hooklib/editor/hooks/use-editor-sync.ts
Editor Contentlib/editor/contents/editor-content.tsx
Toolbar Hooklib/editor/contents/use-editor-toolbar.ts
Editor Context Hooklib/editor/hooks/use-editor.ts