slate-rich-editor
v1.0.1
Published
A production-ready rich text editor built with Slate.js, React, and TypeScript
Maintainers
Readme
Slate Rich Editor
A production-ready, extensible rich text editor built with Slate.js, React, and TypeScript. This package provides a clean API for building rich text editing experiences with full control over customization and extension.
Features
- ✅ Core Formatting: Bold, Italic, Underline
- ✅ Headings: H1, H2, H3
- ✅ Lists: Bulleted and numbered lists
- ✅ Links: Insert and edit links
- ✅ Images: Insert images with alt text
- ✅ Undo/Redo: Full history support via
slate-history - ✅ Keyboard Shortcuts: Cmd/Ctrl+B, I, U, Shift+1/2/3, etc.
- ✅ Custom Highlighting: Non-destructive text highlighting with decorations
- ✅ Read-only Mode: Display content without editing capabilities
- ✅ Serialization: Convert to/from HTML (safe, escaped)
- ✅ TypeScript: Full type safety
- ✅ Extensible: Plugin-based architecture
Installation
npm install slate-rich-editor slate slate-react slate-historyNote: slate, slate-react, and slate-history are peer dependencies. Make sure they're installed in your project.
Quick Start
import React, { useState } from 'react';
import { SlateRichEditor, EditorValue } from 'slate-rich-editor';
function App() {
const [value, setValue] = useState<EditorValue>([
{
type: 'paragraph',
children: [{ text: 'Hello, world!' }],
},
]);
return (
<SlateRichEditor
value={value}
onChange={setValue}
placeholder="Start typing..."
/>
);
}Basic Usage
Editor with Toolbar
import { SlateRichEditor, Toolbar } from 'slate-rich-editor';
function Editor() {
const [value, setValue] = useState<EditorValue>(initialValue);
return (
<div>
<SlateRichEditor value={value} onChange={setValue} />
<Toolbar />
</div>
);
}Read-only Renderer
import { ReadOnlyRenderer } from 'slate-rich-editor';
function Preview({ content }: { content: EditorValue }) {
return <ReadOnlyRenderer value={content} />;
}Search Highlighting
<SlateRichEditor
value={value}
onChange={setValue}
searchTerm="highlight"
highlightColor="#ffeb3b"
/>API Reference
<SlateRichEditor />
Main editor component.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | EditorValue | required | Current editor value (Slate nodes) |
| onChange | (value: EditorValue) => void | required | Callback when value changes |
| readOnly | boolean | false | Disable editing |
| placeholder | string | "Start typing..." | Placeholder text |
| searchTerm | string | - | Search term for programmatic highlighting |
| highlightColor | string | "#ffeb3b" | Color for search highlights |
| onHighlight | (editor: CustomEditor) => void | - | Callback to customize editor instance |
| className | string | - | Custom CSS class |
| style | React.CSSProperties | - | Custom styles |
<ReadOnlyRenderer />
Read-only component for displaying editor content.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | EditorValue | required | Editor value to render |
| className | string | - | Custom CSS class |
| style | React.CSSProperties | - | Custom styles |
<Toolbar />
Toolbar component with formatting buttons.
No props required. Uses useSlate() hook internally.
Plugins & Utilities
Highlight Plugin
The highlight plugin supports two approaches:
1. Manual Highlighting (Marks)
import { toggleHighlight, isHighlightActive } from 'slate-rich-editor';
import { useSlate } from 'slate-react';
function HighlightButton() {
const editor = useSlate();
return (
<button
onClick={() => toggleHighlight(editor, '#ffeb3b')}
disabled={isHighlightActive(editor)}
>
Highlight
</button>
);
}2. Programmatic Highlighting (Decorations)
// Using searchTerm prop (recommended)
<SlateRichEditor
value={value}
onChange={setValue}
searchTerm="search"
highlightColor="#ffeb3b"
/>
// Or using withHighlightSearch plugin
import { withHighlightSearch } from 'slate-rich-editor';
const editor = useMemo(() => {
let baseEditor = createEditor();
return withHighlightSearch(baseEditor, 'search', '#ffeb3b');
}, []);Key Difference:
- Marks: Modify text nodes directly (persistent, saved in value)
- Decorations: Non-destructive overlays (temporary, not saved in value)
Link Plugin
import { wrapLink, isLinkActive, unwrapLink } from 'slate-rich-editor';
import { useSlate } from 'slate-react';
function LinkButton() {
const editor = useSlate();
const handleClick = () => {
if (isLinkActive(editor)) {
unwrapLink(editor);
} else {
const url = window.prompt('Enter URL:');
if (url) wrapLink(editor, url);
}
};
return <button onClick={handleClick}>Link</button>;
}Image Plugin
import { insertImage } from 'slate-rich-editor';
import { useSlate } from 'slate-react';
function ImageButton() {
const editor = useSlate();
const handleClick = () => {
const url = window.prompt('Enter image URL:');
if (url) insertImage(editor, url, 'Alt text');
};
return <button onClick={handleClick}>Image</button>;
}Serialization
import { serialize, deserialize } from 'slate-rich-editor';
// Convert Slate nodes to HTML
const html = serialize(editorValue);
// Convert HTML to Slate nodes
const nodes = deserialize(htmlString);Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| Cmd/Ctrl + B | Toggle bold |
| Cmd/Ctrl + I | Toggle italic |
| Cmd/Ctrl + U | Toggle underline |
| Cmd/Ctrl + Shift + 1 | Heading 1 |
| Cmd/Ctrl + Shift + 2 | Heading 2 |
| Cmd/Ctrl + Shift + 3 | Heading 3 |
| Cmd/Ctrl + Shift + 7 | Numbered list |
| Cmd/Ctrl + Shift + 8 | Bulleted list |
| Cmd/Ctrl + Z | Undo |
| Cmd/Ctrl + Shift + Z | Redo |
Extending the Editor
Adding Custom Elements
- Update Types (
src/types.ts):
export type CustomElement =
| { type: 'paragraph'; children: CustomText[] }
| { type: 'quote'; children: CustomText[] } // New element
// ... other types- Update ElementRenderer (
src/renderers/ElementRenderer.tsx):
case 'quote':
return (
<blockquote {...attributes} style={{ borderLeft: '4px solid #ccc', paddingLeft: '1em' }}>
{children}
</blockquote>
);- Add Insert Function:
export function insertQuote(editor: CustomEditor): void {
const quote: CustomElement = {
type: 'quote',
children: [{ text: '' }],
};
Transforms.insertNodes(editor, quote);
}Adding Custom Marks
- Update Types (
src/types.ts):
export type CustomText = {
text: string;
bold?: boolean;
strikethrough?: boolean; // New mark
// ... other marks
};- Update LeafRenderer (
src/renderers/LeafRenderer.tsx):
if (customLeaf.strikethrough) {
element = <del>{element}</del>;
}- Add Toggle Function:
export function toggleStrikethrough(editor: CustomEditor): void {
const isActive = Editor.marks(editor)?.strikethrough === true;
if (isActive) {
Editor.removeMark(editor, 'strikethrough');
} else {
Editor.addMark(editor, 'strikethrough', true);
}
}Creating Custom Plugins
Plugins are functions that take an editor and return an enhanced editor:
import { CustomEditor } from 'slate-rich-editor';
export function withCustomPlugin(editor: CustomEditor): CustomEditor {
const { isInline } = editor;
editor.isInline = (element) => {
return element.type === 'custom-inline' || isInline(element);
};
return editor;
}
// Usage
const editor = useMemo(() => {
let baseEditor = createEditor();
return withCustomPlugin(baseEditor);
}, []);Styling
The editor uses inline styles by default, but you can override them:
<SlateRichEditor
value={value}
onChange={setValue}
style={{
minHeight: '400px',
padding: '2em',
fontSize: '16px',
}}
className="my-custom-editor"
/>For more control, you can customize the renderers:
// Create custom renderer
function CustomElementRenderer(props: RenderElementProps) {
// Your custom rendering logic
}
// Use in editor
<Editable
renderElement={CustomElementRenderer}
renderLeaf={LeafRenderer}
/>Architecture Decisions
Why Slate Decorations for Highlighting?
We use Slate's decoration API for search-based highlighting because:
- Non-destructive: Decorations don't modify the document structure
- Toggleable: Easy to add/remove without affecting saved content
- Performance: Decorations are computed on render, not stored
- Flexibility: Can highlight multiple terms simultaneously
For manual highlighting (user selection), we use marks because they persist in the document.
Plugin Architecture
The editor uses a plugin-based architecture where each plugin is a function that enhances the editor:
editor = plugin3(plugin2(plugin1(baseEditor)))This allows:
- Composable functionality
- Easy testing of individual plugins
- Clear separation of concerns
Controlled Component Pattern
The editor follows React's controlled component pattern:
valueprop controls the editor stateonChangecallback notifies of changes- No internal state management
This ensures:
- Predictable behavior
- Easy integration with state management
- Full control over editor state
Type Safety
The package is fully typed with TypeScript. Key types:
EditorValue: Array of Slate nodesCustomEditor: Editor instance typeCustomElement: Element node typesCustomText: Text node types
Browser Support
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
Requires React 18+.
License
MIT - Free to use in any project, commercial or otherwise.
Contributing
This is an internal package, but contributions are welcome:
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
Publishing
To publish this package to npm, you need to set up authentication first.
Prerequisites
npm Account: Make sure you have an npm account. If not, create one at npmjs.com/signup
Login to npm:
npm login
Two-Factor Authentication Setup (Required)
npm requires two-factor authentication (2FA) or a granular access token with bypass 2FA enabled to publish packages.
Option 1: Enable 2FA (Recommended)
Enable 2FA on your npm account:
npm profile enable-2fa auth-and-writesThis will prompt you to set up 2FA using an authenticator app (like Google Authenticator or Authy).
Option 2: Use a Granular Access Token
If you prefer not to use 2FA, you can create a granular access token:
Go to npmjs.com → Sign in → Click your profile → Access Tokens
Click Generate New Token → Select Granular Access Token
Configure the token:
- Name: e.g., "publish-token"
- Expiration: Set as needed
- Permissions: Enable "Publish" for your package
- Bypass 2FA: Enable this option
Copy the token and use it:
npm login --auth-type=legacyWhen prompted for password, paste your token instead.
Or set it as an environment variable:
npm config set //registry.npmjs.org/:_authToken YOUR_TOKEN_HERE
Publishing Steps
Build the package:
npm run buildPublish to npm:
npm run releaseOr publish directly:
npm publishThe
prepublishOnlyhook will automatically build the package before publishing.
Note: If your package name is already taken on npm, you'll need to either:
- Change the package name in
package.json - Use a scoped package name (e.g.,
@yourusername/slate-rich-editor)
Examples
See the example/ directory for a complete working example with:
- Editor with toolbar
- Read-only renderer
- Custom actions
- Search highlighting
- HTML serialization
Run the example:
cd example
npm install
npm run devTroubleshooting
Editor not updating
Make sure you're using the value prop correctly:
// ✅ Correct
const [value, setValue] = useState<EditorValue>(initialValue);
<SlateRichEditor value={value} onChange={setValue} />
// ❌ Wrong - don't mutate value directly
value[0].children[0].text = 'new text';Decorations not showing
Ensure you're using withHighlightSearch or the searchTerm prop:
// ✅ Correct
<SlateRichEditor searchTerm="test" value={value} onChange={setValue} />
// Or
const editor = useMemo(() => {
return withHighlightSearch(createEditor(), 'test');
}, []);Type errors
Make sure you've installed TypeScript types:
npm install --save-dev @types/react @types/react-domSupport
For issues or questions, please open an issue on the repository.
