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

@rtif-sdk/react

v2.14.0

Published

React bindings for RTIF — zero-config to full-control

Readme

@rtif-sdk/react

React bindings for RTIF with zero-config to full-control DX.

import { Editor } from '@rtif-sdk/react';
import '@rtif-sdk/react/styles/all.css';

function App() {
  return (
    <Editor
      preset="standard"
      placeholder="Start writing..."
      onChange={({ html, isEmpty }) => save(html)}
      formats={['html']}
    />
  );
}

Install

npm install @rtif-sdk/react @rtif-sdk/core @rtif-sdk/engine @rtif-sdk/web

# Optional format plugins (for onChange serialization, defaultHTML, etc.)
npm install @rtif-sdk/format-html @rtif-sdk/format-markdown

Requires React 18 or 19 as a peer dependency.

Styles

Import the aggregated stylesheet once at your app's entry point:

import '@rtif-sdk/react/styles/all.css';

This re-exports @rtif-sdk/web/styles/all.css which includes all component styles (tokens, marks, blocks, toolbar, pickers, features). All rules use :where() zero-specificity, so your CSS always wins.

For dark mode, add the optional dark theme after:

import '@rtif-sdk/react/styles/all.css';
import '@rtif-sdk/react/styles/dark.css';

Dark mode activates automatically via prefers-color-scheme: dark, the rtif-dark class, or the dark class (Tailwind/shadcn convention).

All CSS source lives in @rtif-sdk/web/styles/. The React package re-exports for convenience. For selective imports and the full CSS reference, see docs/styling.md in the @rtif-sdk/web package.

Usage Modes

Zero config

Renders a default toolbar and content area. No plugins needed for basic text editing.

<Editor />

Preset

Named preset bundles (plaintext, basic, standard, full) provide curated plugin sets:

<Editor preset="standard" placeholder="Type here..." />

| Preset | Plugins | |--------|---------| | plaintext | None | | basic | bold, italic, underline, link | | standard | basic + strikethrough, code, heading, list, blockquote, codeBlock, hr | | full | standard + image, callout, embed, alignment, indent, textColor, backgroundColor, fontSize, fontFamily |

Explicit plugins

For full control, pass an array of plugin factories:

import { Editor, bold, italic, heading, link, preset } from '@rtif-sdk/react';

// Individual plugins
<Editor plugins={[bold(), italic(), heading(), link()]} />

// Preset with surgical overrides
<Editor plugins={preset('full', {
  overrides: { 'block-image': { upload: myUploader } },
  remove: ['block-embed'],
})} />

Compound mode

When <Editor.Content> is a child, auto-layout is disabled and you control the layout:

<Editor plugins={preset('standard')}>
  <Editor.Toolbar />
  <Editor.BubbleToolbar />
  <Editor.Content className="min-h-[300px] p-4" />
</Editor>

Headless mode

Use useEditor + EditorProvider for maximum flexibility:

import { useEditor, EditorProvider, EditorContent, preset } from '@rtif-sdk/react';

function MyEditor() {
  const editor = useEditor({ plugins: preset('standard') });

  return (
    <EditorProvider editor={editor}>
      <MyCustomToolbar />
      <EditorContent />
    </EditorProvider>
  );
}

Compound Sub-Components

| Component | Description | |-----------|-------------| | Editor.Content | The contenteditable area | | Editor.Toolbar | Default formatting toolbar — supports append children and render-prop customization | | Editor.BubbleToolbar | Floating toolbar that appears on text selection | | Editor.MentionMenu | Render-prop for mention dropdown UI | | Editor.SlashMenu | Render-prop for slash command menu UI |

Smart onChange

The onChange callback is debounced and serializes only the formats you request:

<Editor
  preset="standard"
  onChange={({ doc, html, text, markdown, isEmpty, state }) => {
    console.log(html);    // only present if 'html' is in formats
    console.log(isEmpty); // true when document is empty
  }}
  formats={['html', 'text']}
  debounceMs={500}         // default: 300
  transientAttrs={['focused']} // strip before serialization
/>

Requires the corresponding format plugin to be installed (@rtif-sdk/format-html, @rtif-sdk/format-markdown). Serializers are loaded lazily on first use.

Initial Content

// From an RTIF document
<Editor defaultDoc={savedDoc} />

// From HTML (requires @rtif-sdk/format-html)
<Editor defaultHTML="<p>Hello <strong>world</strong></p>" />

Imperative Ref

import { useRef } from 'react';
import type { EditorRef } from '@rtif-sdk/react';

function MyEditor() {
  const ref = useRef<EditorRef>(null);

  return (
    <>
      <button onClick={() => ref.current?.focus()}>Focus</button>
      <button onClick={() => console.log(ref.current?.getHTML())}>Log HTML</button>
      <button onClick={() => ref.current?.clear()}>Clear</button>
      <Editor ref={ref} preset="standard" />
    </>
  );
}

EditorRef methods

| Method | Returns | Description | |--------|---------|-------------| | focus() | void | Focus the editor | | blur() | void | Blur the editor | | isFocused() | boolean | Whether the editor is focused | | getDoc() | Document | Current RTIF document | | getSelection() | Selection | Current selection | | getHTML() | string | HTML serialization (requires format-html) | | getText() | string | Plain text serialization (requires format-plaintext) | | isEmpty() | boolean | Whether the document is empty | | exec(cmd, payload?) | void | Execute a named command | | isActive(cmd, payload?) | boolean | Whether a command's effect is active | | canExecute(cmd, payload?) | boolean | Whether a command can execute | | clear() | void | Clear all content |

Subscription Hooks

Reactive hooks for building custom UI. All use useSyncExternalStore for tear-free reads.

import {
  useIsActive,
  useCanExecute,
  useMarksAtCursor,
  useBlockAtCursor,
  useEditorState,
  useEditorFocused,
  useFocusedBlock,
} from '@rtif-sdk/react';

function MyToolbar() {
  const editor = useEditorContext();
  const isBold = useIsActive(editor, 'toggleMark:bold');
  const canUndo = useCanExecute(editor, 'undo');
  const marks = useMarksAtCursor(editor);
  const block = useBlockAtCursor(editor);
  const focused = useEditorFocused(editor);

  return (
    <button
      aria-pressed={isBold}
      onMouseDown={(e) => { e.preventDefault(); editor.exec('toggleMark:bold'); }}
    >
      Bold
    </button>
  );
}

Toolbar Components

Pre-built toolbar primitives that auto-reflect active/disabled state:

import {
  Toolbar,
  ToolbarButton,
  ToolbarSeparator,
  ToolbarGroup,
  BoldButton,
  ItalicButton,
  HeadingSelect,
  DefaultToolbar,
  BubbleToolbar,
  ColorPicker,
  FontSizePicker,
  FontFamilyPicker,
} from '@rtif-sdk/react';

// Use pre-built buttons
<Toolbar>
  <BoldButton />
  <ItalicButton />
  <ToolbarSeparator />
  <ToolbarButton command="toggleMark:underline" label="Underline">U</ToolbarButton>
</Toolbar>

// Or the all-in-one default toolbar
<DefaultToolbar />

// Floating selection toolbar
<BubbleToolbar />

// Block type dropdown
<HeadingSelect />

// Pickers
<ColorPicker />
<FontSizePicker />
<FontFamilyPicker />

Customizing the Default Toolbar

<DefaultToolbar> (also available as <Editor.Toolbar>) supports three modes:

// 1. Default — standard layout, no customization
<Editor.Toolbar />

// 2. Append — add custom elements after the defaults
<Editor.Toolbar>
  <ColorPicker />
  <FontSizePicker />
</Editor.Toolbar>

// 3. Render prop — full layout control via sections object
<Editor.Toolbar>
  {(sections) => (
    <>
      {sections.undoRedo}
      <ToolbarSeparator />
      <MyCustomHeadingSelect />
      <ToolbarSeparator />
      {sections.formatMarks}
    </>
  )}
</Editor.Toolbar>

The render prop exposes pre-built groups (undoRedo, headingSelect, formatMarks, link) and individual buttons (undo, redo, bold, italic, underline, strikethrough, code).

Two helpers simplify common patterns:

// sections.group() — render sections with auto-separators
{sections.group('undoRedo', 'formatMarks', 'link')}

// sections.without() — default layout minus excluded sections
{sections.without('headingSelect')}

Mention Support

<Editor
  preset="standard"
  mention={{
    triggerChar: '@',
    onKeyDown: (session, event) => {
      if (event.key === 'Enter') {
        session.accept({ id: '1', displayName: 'Alice' });
        return true;
      }
      return false;
    },
  }}
>
  <Editor.Content />
  <Editor.MentionMenu>
    {({ isActive, query, triggerRect, accept, dismiss }) =>
      isActive && (
        <MentionDropdown
          query={query}
          position={triggerRect}
          onSelect={(user) => accept({ id: user.id, displayName: user.name })}
          onClose={dismiss}
        />
      )
    }
  </Editor.MentionMenu>
</Editor>

Slash Commands

import { filterSlashItems } from '@rtif-sdk/react';

<Editor
  preset="standard"
  slashMenu={{
    triggerChar: '/',
    items: DEFAULT_SLASH_ITEMS, // or custom items
  }}
>
  <Editor.Content />
  <Editor.SlashMenu>
    {({ isActive, query, items, triggerRect, accept, dismiss }) =>
      isActive && (
        <SlashCommandMenu
          items={filterSlashItems(items, query)}
          position={triggerRect}
          onSelect={(item) => accept(item)}
          onClose={dismiss}
        />
      )
    }
  </Editor.SlashMenu>
</Editor>

Read-Only Document Rendering

For feeds, previews, and static content display without editor overhead:

import { DocumentView, DocumentViewProvider } from '@rtif-sdk/react';

// Single document
<DocumentView doc={post.doc} />

// Shared config across multiple views
<DocumentViewProvider marks={{ mention: mentionRenderer }}>
  {posts.map(p => <DocumentView key={p.id} doc={p.doc} />)}
</DocumentViewProvider>

Multi-Block Selection

All mark and block type commands automatically handle multi-block selections. When the user selects across multiple blocks:

  • Format buttons (bold, italic, color, etc.) apply to all selected blocks
  • Block type buttons (heading, blockquote, list, etc.) change all selected blocks
  • Undo reverts the entire multi-block change in one step

Configure how the toolbar reports active state for mixed-type selections via blockTypeActiveStrategy on the engine:

const editor = useEditor({
  plugins: preset('standard'),
  blockTypeActiveStrategy: 'first', // or 'all' (default)
});

| Strategy | Behavior | Use case | |----------|----------|----------| | 'all' | Active only when every selected block matches | Conservative default | | 'first' | Active when the first selected block matches | Google Docs/Notion behavior |

Plugin System

Built-in plugin factories

Every plugin is a factory function returning a unified EditorPlugin:

import { bold, image, link } from '@rtif-sdk/react';

bold()                // EditorPlugin<BoldConfig>
image({ upload: fn }) // EditorPlugin<ImageConfig>
link()                // EditorPlugin<LinkConfig>

Surgical overrides via extend()

const customImage = image({ upload: myUploader }).extend({ maxFileSize: 10_000_000 });

Preset customization

const plugins = preset('full', {
  overrides: {
    'block-image': { upload: myUploader },
  },
  remove: ['block-embed'],
  add: [myCustomPlugin()],
});

Creating custom plugins

Use createEditorPlugin to reduce boilerplate:

import { createEditorPlugin } from '@rtif-sdk/react';

interface MyConfig {
  color?: string;
}

export const myPlugin = createEditorPlugin<MyConfig>('my-plugin', (config) => ({
  engine: createMyEnginePlugin(config),
  marks: { myMark: myRenderer },
}));

// Usage
const plugins = [myPlugin({ color: 'red' })];

EditorFeatures (UI plugins)

UI features like block gutter, context menu, and link popover are tree-shakeable opt-ins:

import { blockGutter, contextMenu, linkPopover, defaultFeatures } from '@rtif-sdk/react';

<Editor
  plugins={preset('standard')}
  features={[blockGutter(), contextMenu(), linkPopover()]}
/>

// Or use the convenience helper
<Editor plugins={preset('standard')} features={defaultFeatures()} />

blockDrag() provides drag-to-reorder with keyboard support and a theme-aware preview. Use it as an alternative to blockGutter — do not use both at the same time.

import { Editor, blockDrag } from '@rtif-sdk/react';

// Basic
<Editor features={[blockDrag()]} />

// With custom preview config
<Editor features={[blockDrag({ preview: { maxWidth: 400 }, keyboard: true })]} />

Alt+ArrowUp / Alt+ArrowDown reorder the focused block without dragging.

CSS Custom Properties

All visual tokens are overridable via CSS custom properties. The toolbar-specific tokens defined by this package:

:where(.rtif-toolbar) {
  --rtif-toolbar-bg: var(--rtif-surface, #fff);
  --rtif-toolbar-border: var(--rtif-border, #ccc);
  --rtif-toolbar-gap: 2px;
  --rtif-toolbar-padding: 4px 8px;
  --rtif-toolbar-button-size: 32px;
  --rtif-toolbar-button-radius: 4px;
  --rtif-toolbar-button-hover: var(--rtif-hover-bg, #e8e8e8);
  --rtif-toolbar-button-active: var(--rtif-active-bg, #d0e0ff);
  --rtif-content-padding: 16px;
  --rtif-content-font-size: 16px;
  --rtif-content-line-height: 1.6;
}

Base design tokens (--rtif-accent-color, --rtif-surface, --rtif-border, --rtif-shadow, --rtif-focus-ring), interactive state tokens (--rtif-text-color, --rtif-hover-bg, --rtif-active-bg, --rtif-danger-color, etc.), and content tokens (--rtif-code-bg, --rtif-code-block-bg, --rtif-blockquote-text, --rtif-placeholder-color, etc.) are defined by @rtif-sdk/web and inherited — not redeclared.

Programmatic Theming

The theme prop sets CSS custom properties as inline styles without writing CSS:

<Editor
  theme={{ surface: '#1a1a1a', textColor: '#e0e0e0', accentColor: '#4a9eff' }}
  preset="standard"
/>

The EditorTheme interface supports all 15 tokens. See the Styling Guide for the full key-to-property mapping.

Testing

A separate entry point provides test utilities:

import { createMockEditorHandle, createDefaultDoc, isDocEmpty } from '@rtif-sdk/react/testing';

const editor = createMockEditorHandle();
// Use with <EditorContext.Provider value={editor}> in tests

API Reference

Components

| Export | Description | |--------|-------------| | Editor | Compound component with auto-layout | | EditorContent | Contenteditable container | | EditorProvider | Headless context provider | | DocumentView | Read-only document renderer | | DocumentViewProvider | Shared config for multiple DocumentViews | | Toolbar | Toolbar container (role="toolbar") | | ToolbarButton | Command button with active/disabled state | | ToolbarSeparator | Visual divider | | ToolbarGroup | Button grouping | | DefaultToolbar | Pre-composed toolbar with common buttons (supports append and render-prop children) | | HeadingSelect | Block type dropdown | | BubbleToolbar | Floating selection toolbar | | ColorPicker | Text color picker | | FontSizePicker | Font size picker | | FontFamilyPicker | Font family picker | | BoldButton ... RedoButton | Pre-built format buttons |

Hooks

| Export | Description | |--------|-------------| | useEditor | Core lifecycle hook — creates engine + handle | | useEditorContext | Access EditorHandle from context | | useIsActive | Whether a command effect is active | | useCanExecute | Whether a command can execute | | useMarksAtCursor | Marks at the current cursor offset | | useBlockAtCursor | Block at the current cursor offset | | useEditorState | Full editor state (re-renders on every change) | | useEditorFocused | Whether the editor is focused | | useFocusedBlock | Currently focused block info | | useSmartOnChange | Debounced, serialized onChange subscription | | useMentionPlugin | Headless mention trigger hook | | useSlashCommandPlugin | Headless slash command hook | | useTrigger | Generic trigger hook | | useSavedSelection | Saved selection range and text, with save/clear actions |

Plugin Factories

| Export | ID | Type | |--------|----|------| | bold() | mark-bold | Mark | | italic() | mark-italic | Mark | | underline() | mark-underline | Mark | | strikethrough() | mark-strikethrough | Mark | | code() | mark-code | Mark | | textColor() | mark-textColor | Mark | | fontSize() | mark-fontSize | Mark | | fontFamily() | mark-fontFamily | Mark | | link() | mark-link | Mark | | heading() | block-heading | Block | | list() | block-list | Block | | blockquote() | block-blockquote | Block | | codeBlock() | block-codeBlock | Block | | hr() | block-hr | Block | | image() | block-image | Block | | callout() | block-callout | Block | | embed() | block-embed | Block | | alignment() | attr-alignment | Attr | | indent() | attr-indent | Attr |

Utilities

| Export | Description | |--------|-------------| | preset(name, options?) | Resolve a preset name to plugins | | createEditorPlugin(id, build) | Factory helper for custom plugins | | createDefaultDoc() | Create an empty RTIF document | | isDocEmpty(doc) | Check if a document is empty | | filterSlashItems(items, query) | Filter slash command items by query | | getAvailableItems(items, ctx) | Filter items by availability | | themeToStyle(theme) | Convert EditorTheme to CSS custom property style object | | blockGutter(config?) | EditorFeature: hover-revealed gutter with drag handle and add-block button | | contextMenu(config?) | EditorFeature: right-click context menu | | linkPopover(config?) | EditorFeature: inline link editing popover | | blockDrag(config?) | EditorFeature: drag-to-reorder blocks with keyboard support and theme-aware preview | | defaultFeatures(config?) | Convenience helper returning [blockGutter(), contextMenu(), linkPopover()] |

License

MIT