npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

slate-rich-editor

v1.0.1

Published

A production-ready rich text editor built with Slate.js, React, and TypeScript

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-history

Note: 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

  1. Update Types (src/types.ts):
export type CustomElement = 
  | { type: 'paragraph'; children: CustomText[] }
  | { type: 'quote'; children: CustomText[] }  // New element
  // ... other types
  1. Update ElementRenderer (src/renderers/ElementRenderer.tsx):
case 'quote':
  return (
    <blockquote {...attributes} style={{ borderLeft: '4px solid #ccc', paddingLeft: '1em' }}>
      {children}
    </blockquote>
  );
  1. Add Insert Function:
export function insertQuote(editor: CustomEditor): void {
  const quote: CustomElement = {
    type: 'quote',
    children: [{ text: '' }],
  };
  Transforms.insertNodes(editor, quote);
}

Adding Custom Marks

  1. Update Types (src/types.ts):
export type CustomText = {
  text: string;
  bold?: boolean;
  strikethrough?: boolean;  // New mark
  // ... other marks
};
  1. Update LeafRenderer (src/renderers/LeafRenderer.tsx):
if (customLeaf.strikethrough) {
  element = <del>{element}</del>;
}
  1. 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:

  1. Non-destructive: Decorations don't modify the document structure
  2. Toggleable: Easy to add/remove without affecting saved content
  3. Performance: Decorations are computed on render, not stored
  4. 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:

  • value prop controls the editor state
  • onChange callback 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 nodes
  • CustomEditor: Editor instance type
  • CustomElement: Element node types
  • CustomText: 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:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests if applicable
  5. Submit a pull request

Publishing

To publish this package to npm, you need to set up authentication first.

Prerequisites

  1. npm Account: Make sure you have an npm account. If not, create one at npmjs.com/signup

  2. 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-writes

This 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:

  1. Go to npmjs.com → Sign in → Click your profile → Access Tokens

  2. Click Generate New Token → Select Granular Access Token

  3. Configure the token:

    • Name: e.g., "publish-token"
    • Expiration: Set as needed
    • Permissions: Enable "Publish" for your package
    • Bypass 2FA: Enable this option
  4. Copy the token and use it:

    npm login --auth-type=legacy

    When 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

  1. Build the package:

    npm run build
  2. Publish to npm:

    npm run release

    Or publish directly:

    npm publish

    The prepublishOnly hook 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 dev

Troubleshooting

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-dom

Support

For issues or questions, please open an issue on the repository.