Rich text editor React component library based on Tiptap with extensive formatting controls and Mantine integration
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
React Context and hooks for accessing editor state, configuration, and building custom controls. The context system enables deep integration with the editor while maintaining consistent behavior across all components.
Primary hook for accessing the rich text editor's context, including editor instance, styling functions, and configuration options.
/**
* Hook for accessing rich text editor context
* Must be used within a RichTextEditor component tree
* @returns RichTextEditorContext object with editor state and configuration
* @throws Error if used outside of RichTextEditor provider
*/
function useRichTextEditorContext(): RichTextEditorContext;
interface RichTextEditorContext {
/** Current Tiptap editor instance */
editor: Editor | null;
/** Mantine styling API for consistent component styling */
getStyles: GetStylesApi<RichTextEditorFactory>;
/** Merged accessibility labels for all controls */
labels: RichTextEditorLabels;
/** Whether code highlighting styles are enabled */
withCodeHighlightStyles: boolean | undefined;
/** Whether typography styles are enabled */
withTypographyStyles: boolean | undefined;
/** Current component variant */
variant: string | undefined;
/** Whether default styles are disabled */
unstyled: boolean | undefined;
/** Callback for source code view toggle events */
onSourceCodeTextSwitch?: (isSourceCodeModeActive: boolean) => void;
}Usage Example:
import { useRichTextEditorContext } from "@mantine/tiptap";
import { IconCustomFormat } from "@tabler/icons-react";
function CustomControl() {
const { editor, labels, getStyles } = useRichTextEditorContext();
const isActive = editor?.isActive('bold') || false;
const canExecute = editor?.can().toggleBold() || false;
return (
<button
{...getStyles('control')}
disabled={!canExecute}
onClick={() => editor?.chain().focus().toggleBold().run()}
aria-label={labels.boldControlLabel}
data-active={isActive}
>
<IconCustomFormat size={16} />
</button>
);
}Access the current Tiptap editor instance for direct manipulation:
function EditorStateDisplay() {
const { editor } = useRichTextEditorContext();
if (!editor) {
return <div>Editor not ready</div>;
}
const wordCount = editor.storage.characterCount?.words() || 0;
const canUndo = editor.can().undo();
const canRedo = editor.can().redo();
return (
<div>
<p>Words: {wordCount}</p>
<p>Can Undo: {canUndo ? 'Yes' : 'No'}</p>
<p>Can Redo: {canRedo ? 'Yes' : 'No'}</p>
</div>
);
}Use the styling API to maintain consistency with built-in components:
function StyledCustomControl() {
const { getStyles } = useRichTextEditorContext();
return (
<div {...getStyles('controlsGroup')}>
<button {...getStyles('control')}>
Custom Control
</button>
</div>
);
}Access merged labels for consistent accessibility:
function AccessibleControl() {
const { labels, editor } = useRichTextEditorContext();
return (
<button
aria-label={labels.boldControlLabel}
title={labels.boldControlLabel}
onClick={() => editor?.chain().focus().toggleBold().run()}
>
B
</button>
);
}import { useRichTextEditorContext } from "@mantine/tiptap";
interface CustomControlProps {
command: string;
icon: React.ReactNode;
label: string;
}
function CustomControl({ command, icon, label }: CustomControlProps) {
const { editor, getStyles } = useRichTextEditorContext();
const isActive = editor?.isActive(command) || false;
const canExecute = editor?.can()[`toggle${command}`]?.() || false;
const handleClick = () => {
editor?.chain().focus()[`toggle${command}`]?.().run();
};
return (
<button
{...getStyles('control')}
onClick={handleClick}
disabled={!canExecute}
aria-label={label}
data-active={isActive}
>
{icon}
</button>
);
}
// Usage
<CustomControl
command="bold"
icon={<IconBold />}
label="Toggle Bold"
/>import { useState } from "react";
import { useRichTextEditorContext } from "@mantine/tiptap";
function FontSizeControl() {
const { editor, getStyles } = useRichTextEditorContext();
const [isOpen, setIsOpen] = useState(false);
const currentSize = editor?.getAttributes('textStyle').fontSize || '16px';
const sizes = ['12px', '14px', '16px', '18px', '20px', '24px'];
const applySize = (size: string) => {
editor?.chain().focus().setFontSize(size).run();
setIsOpen(false);
};
return (
<div style={{ position: 'relative' }}>
<button
{...getStyles('control')}
onClick={() => setIsOpen(!isOpen)}
aria-label="Font Size"
>
{currentSize}
</button>
{isOpen && (
<div style={{ position: 'absolute', top: '100%', background: 'white', border: '1px solid #ccc' }}>
{sizes.map(size => (
<button
key={size}
onClick={() => applySize(size)}
style={{ display: 'block', width: '100%', fontSize: size }}
>
{size}
</button>
))}
</div>
)}
</div>
);
}import { useRichTextEditorContext } from "@mantine/tiptap";
import { IconCustomIcon } from "@tabler/icons-react";
// Build a complex custom control with multiple features
function CustomFormatControl() {
const { editor, labels, getStyles } = useRichTextEditorContext();
const isActive = editor?.isActive('customFormat') || false;
const canExecute = editor?.can().toggleCustomFormat?.() || false;
const handleClick = () => {
editor?.chain().focus().toggleCustomFormat().run();
};
return (
<button
{...getStyles('control')}
onClick={handleClick}
disabled={!canExecute}
aria-label="Apply Custom Format"
data-active={isActive}
>
<IconCustomIcon size={16} />
</button>
);
}
// Usage
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar>
<RichTextEditor.ControlsGroup>
<CustomFormatControl />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>The context is automatically provided by the RichTextEditor component:
// Context is provided automatically
<RichTextEditor editor={editor}>
{/* All child components have access to context */}
<RichTextEditor.Toolbar>
<CustomControl /> {/* Can use useRichTextEditorContext */}
<RichTextEditor.Bold /> {/* Uses context internally */}
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>The context hook includes built-in error handling:
function SafeCustomControl() {
try {
const context = useRichTextEditorContext();
// Use context safely
return <button>Custom Control</button>;
} catch (error) {
// Handle context not found error
console.error('RichTextEditor context not found:', error);
return null;
}
}Full TypeScript support with proper type inference:
import type { Editor } from "@tiptap/react";
import type { RichTextEditorContext } from "@mantine/tiptap";
function TypedCustomControl() {
const context: RichTextEditorContext = useRichTextEditorContext();
const editor: Editor | null = context.editor;
// TypeScript knows the exact shape of context
const labels = context.labels; // RichTextEditorLabels
const getStyles = context.getStyles; // GetStylesApi<RichTextEditorFactory>
return (
<button onClick={() => editor?.chain().focus().toggleBold().run()}>
Bold
</button>
);
}import { useCallback } from "react";
function OptimizedControl() {
const { editor } = useRichTextEditorContext();
// Memoize event handlers
const handleClick = useCallback(() => {
editor?.chain().focus().toggleBold().run();
}, [editor]);
return <button onClick={handleClick}>Bold</button>;
}function ConditionalControl({ showAdvanced }: { showAdvanced: boolean }) {
const { editor } = useRichTextEditorContext();
if (!editor || !showAdvanced) {
return null;
}
return (
<button onClick={() => editor.chain().focus().toggleCode().run()}>
Code
</button>
);
}import { useEffect, useState } from "react";
function EditorStateSync() {
const { editor } = useRichTextEditorContext();
const [isBold, setIsBold] = useState(false);
useEffect(() => {
if (!editor) return;
const updateState = () => {
setIsBold(editor.isActive('bold'));
};
// Listen to editor updates
editor.on('selectionUpdate', updateState);
editor.on('transaction', updateState);
return () => {
editor.off('selectionUpdate', updateState);
editor.off('transaction', updateState);
};
}, [editor]);
return <div>Text is {isBold ? 'bold' : 'normal'}</div>;
}