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

@noseberry/nbd-editor

v1.1.1

Published

Nbd Editor: a framework-agnostic rich content editor by Noseberry Private Limited.

Readme

NBD Editor

We'd love to hear from you! If you run into any bugs, have feature requests, or just want to share how you're using NBD Editor, please open an issue. Your feedback helps us make NBD Editor better for everyone.

A powerful, framework-agnostic block editor. NBD Editor gives you a clean, modern content editing experience with drag-and-drop blocks, rich text formatting, media uploads, and a slash command menu — all in a single lightweight package with zero production dependencies.

Works seamlessly with vanilla JavaScript, React, and Next.js.

Why NBD Editor?

  • Drop-in ready — one import, one line of code, and you have a full block editor
  • Framework agnostic — works with any frontend stack; optional React wrapper included
  • 20+ block types — paragraphs, headings, images, video, audio, tables, columns, code, embeds, and more
  • Rich editing — inline formatting toolbar (draggable), slash commands (/), drag-and-drop reordering, undo/redo
  • Media support — upload images, video, audio, and files with drag-and-drop or file picker
  • Simple data modelgetData() returns { content: '<p>...</p>' } — just store one HTML string
  • TypeScript support — full type definitions included for autocomplete and type safety
  • Lightweight — ~89 KB minified JS + ~25 KB CSS, zero runtime dependencies

How It Works

Install → Import → Mount → Done

1. npm install @noseberry/nbd-editor
2. import { NBDEditor } from '@noseberry/nbd-editor'
3. new NBDEditor('#editor', { onSave: (data) => save(data) })

The editor manages its own state internally. You interact with it through callbacks (onChange, onSave) or imperatively via the API (getData(), setData(), toHTML()). In React, use the ReactNBDEditor component with or without a ref.

Usage Flow

┌─────────────────────────────────────────────────────┐
│                    Your App                         │
│                                                     │
│   ┌─────────────┐        ┌──────────────────────┐  │
│   │  Initialize  │───────▶│     NBD Editor        │  │
│   │  with config │        │                      │  │
│   └─────────────┘        │  ┌──────────────────┐ │  │
│                           │  │  User writes     │ │  │
│   ┌─────────────┐        │  │  content using    │ │  │
│   │  onChange()  │◀───────│  │  blocks, toolbar, │ │  │
│   │  callback    │        │  │  slash commands   │ │  │
│   └─────────────┘        │  └──────────────────┘ │  │
│                           │                      │  │
│   ┌─────────────┐        │  ┌──────────────────┐ │  │
│   │  onSave()   │◀───────│  │  Ctrl+S or       │ │  │
│   │  callback    │        │  │  save() API call │ │  │
│   └──────┬──────┘        │  └──────────────────┘ │  │
│          │                └──────────────────────┘  │
│          ▼                                          │
│   ┌─────────────┐                                   │
│   │  Store in   │   data = { content: '<p>...</p>' }│
│   │  database   │                                   │
│   └─────────────┘                                   │
│                                                     │
│   To restore: editor.setData({ content: savedHTML })│
└─────────────────────────────────────────────────────┘

Table of Contents


Installation

npm install @noseberry/nbd-editor
# or
yarn add @noseberry/nbd-editor
# or
pnpm add @noseberry/nbd-editor

Important — always import the CSS:

import '@noseberry/nbd-editor/style.css';

Without this import, no editor styles will be applied. This is required for all setups (vanilla JS, React, Next.js).


Quick Start (Vanilla JS)

import { NBDEditor } from '@noseberry/nbd-editor';
import '@noseberry/nbd-editor/style.css';

const editor = new NBDEditor('#editor', {
  siteName: 'My Blog',
  authorName: 'Harshit',
  content: '<p>Start writing here...</p>',
  maxFileSize: 200 * 1024 * 1024,
  onChooseMediaSource: (type) => 'upload',
  onChange: (data) => console.log('Content changed:', data),
  onSave: (data) => console.log('Saved:', data),
  onPublish: (data) => console.log('Published:', data),
  uploadHandler: async (file) => {
    const form = new FormData();
    form.append('file', file);
    const res = await fetch('/api/upload', { method: 'POST', body: form });
    return res.json(); // must return { url, id? }
  },
});

You can pass either a CSS selector string ('#editor') or a DOM element directly as the first argument.


React Integration

Without Ref

The simplest approach — just render the editor and listen for changes via callbacks. No imperative access needed.

import { ReactNBDEditor } from '@noseberry/nbd-editor';
import '@noseberry/nbd-editor/style.css';

export default function EditorPage() {
  const handleChange = (data) => {
    console.log('Editor content:', data.content);
  };

  const handleSave = (data) => {
    fetch('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
  };

  return (
    <ReactNBDEditor
      content="<p>Hello World</p>"
      siteName="My App"
      authorName="Harshit"
      maxFileSize={200 * 1024 * 1024}
      onChooseMediaSource={(type) => 'upload'}
      onChange={handleChange}
      onSave={handleSave}
      uploadHandler={async (file) => {
        const form = new FormData();
        form.append('file', file);
        const res = await fetch('/api/upload', { method: 'POST', body: form });
        return res.json();
      }}
    />
  );
}

With Ref

Use a ref when you need imperative control — save programmatically, get data on demand, undo/redo, etc.

import React, { useRef, useCallback } from 'react';
import { ReactNBDEditor } from '@noseberry/nbd-editor';
import '@noseberry/nbd-editor/style.css';

export default function EditorPage() {
  const editorRef = useRef(null);

  const handleSaveClick = useCallback(() => {
    editorRef.current?.save();
  }, []);

  const handleGetData = useCallback(() => {
    const data = editorRef.current?.getData();
    console.log('Editor data:', data);
    // data => { content: '<p>...</p>' }
  }, []);

  const handleGetHTML = useCallback(() => {
    const html = editorRef.current?.toHTML();
    console.log('Raw HTML:', html);
  }, []);

  const handleUndo = useCallback(() => {
    editorRef.current?.undo();
  }, []);

  const handleRedo = useCallback(() => {
    editorRef.current?.redo();
  }, []);

  const handleSetContent = useCallback(() => {
    editorRef.current?.setData({ content: '<h2>Replaced content</h2><p>New paragraph</p>' });
  }, []);

  return (
    <div>
      <div style={{ marginBottom: 16, display: 'flex', gap: 8 }}>
        <button onClick={handleSaveClick}>Save</button>
        <button onClick={handleGetData}>Get Data</button>
        <button onClick={handleGetHTML}>Get HTML</button>
        <button onClick={handleUndo}>Undo</button>
        <button onClick={handleRedo}>Redo</button>
        <button onClick={handleSetContent}>Replace Content</button>
      </div>

      <ReactNBDEditor
        ref={editorRef}
        content="<p>Edit me...</p>"
        siteName="My App"
        authorName="Harshit"
        maxFileSize={200 * 1024 * 1024}
        onChooseMediaSource={(type) => 'upload'}
        onSave={(data) => console.log('Saved:', data)}
      />
    </div>
  );
}

Ref methods available:

| Method | Description | |---|---| | getInstance() | Returns the underlying NBDEditor instance | | getData() | Returns { content: '<p>...</p>' } | | setData(data) | Sets content — accepts { content: '...' } | | toHTML() | Returns the raw HTML string | | save() | Triggers save (fires onSave callback) | | publish() | Triggers publish (fires onPublish callback) | | undo(opts?) | Undo last action | | redo(opts?) | Redo last undone action | | destroy() | Destroys the editor instance and cleans up |

Controlled Mode (like React Quill)

NBD Editor supports the controlled component pattern — bind content to state and update it via onChange:

import React, { useState } from 'react';
import { ReactNBDEditor } from '@noseberry/nbd-editor';
import '@noseberry/nbd-editor/style.css';

export default function EditorPage() {
  const [content, setContent] = useState('<p>Hello World</p>');

  return (
    <ReactNBDEditor
      content={content}
      onChange={(data) => setContent(data.content)}
      siteName="My App"
      authorName="Harshit"
    />
  );
}

For heavy content or frequent updates, use debounceMs to limit how often onChange fires:

<ReactNBDEditor
  content={content}
  onChange={(data) => setContent(data.content)}
  debounceMs={300}  // fires onChange at most every 300ms
/>

You can also use content as a one-time initial value and use the ref for everything else:

editorRef.current?.setData({ content: '<p>New content from server</p>' });

Next.js Integration

The editor uses browser APIs (document, window, contenteditable), so it must be rendered client-side only.

App Router (Next 13+)

Create a client component wrapper:

// components/Editor.jsx
'use client';

import React, { useRef } from 'react';
import { ReactNBDEditor } from '@noseberry/nbd-editor';
import '@noseberry/nbd-editor/style.css';

export default function Editor({ initialContent }) {
  const editorRef = useRef(null);

  return (
    <ReactNBDEditor
      ref={editorRef}
      content={initialContent}
      siteName="My Next.js App"
      authorName="Harshit"
      maxFileSize={200 * 1024 * 1024}
      onChooseMediaSource={(type) => 'upload'}
      onSave={(data) => {
        console.log('Saved:', data);
      }}
      uploadHandler={async (file) => {
        const form = new FormData();
        form.append('file', file);
        const res = await fetch('/api/upload', { method: 'POST', body: form });
        return res.json();
      }}
    />
  );
}

Use it in a page:

// app/editor/page.jsx
import Editor from '@/components/Editor';

export default function EditorPage() {
  return (
    <main>
      <h1>Post Editor</h1>
      <Editor initialContent="<p>Write your post...</p>" />
    </main>
  );
}

Note: onSave, onChange, and uploadHandler are functions — they cannot be passed from a Server Component. Define them inside the 'use client' wrapper component (as shown above), or make the page itself a Client Component.

Alternative — dynamic import (skip SSR entirely):

// app/editor/page.jsx
'use client';

import dynamic from 'next/dynamic';

const Editor = dynamic(() => import('@/components/Editor'), {
  ssr: false,
  loading: () => <p>Loading editor...</p>,
});

export default function EditorPage() {
  return (
    <main>
      <h1>Post Editor</h1>
      <Editor initialContent="<p>Write your post...</p>" />
    </main>
  );
}

Pages Router

// pages/editor.jsx
import dynamic from 'next/dynamic';

const Editor = dynamic(() => import('../components/Editor'), {
  ssr: false,
  loading: () => <p>Loading editor...</p>,
});

export default function EditorPage() {
  return (
    <main>
      <h1>Post Editor</h1>
      <Editor initialContent="<p>Write your post...</p>" />
    </main>
  );
}

Tip: Using dynamic with ssr: false ensures the editor is never rendered on the server, preventing hydration mismatches.


Configuration Options

Pass these as the second argument to new NBDEditor(selector, options) or as props to <ReactNBDEditor />.

| Option | Type | Default | Description | |---|---|---|---| | siteName | string | 'My Site' | Displayed in the editor header | | authorName | string | 'Admin' | Displayed as the author name | | content | string | null | Initial HTML content to load | | placeholder | string | 'Start writing...' | Placeholder text for empty blocks | | showStatusBar | boolean | true | Show/hide the bottom status bar | | showSettingsPanel | boolean | false | Show/hide the right settings panel | | showHeaderActions | boolean | false | Show/hide header action buttons | | autoSaveInterval | number | 30000 | Auto-save interval in ms (0 to disable) | | maxFileSize | number | 10485760 | Max upload file size in bytes (default 10 MB) | | uploadHandler | async (file) => { url, id? } | null | Custom file upload function | | onChooseMediaSource | (type) => 'url' \| 'upload' | null | Controls video/audio source chooser | | onChange | (data) => void | null | Called on every content change | | onSave | (data) => void | null | Called when save is triggered | | onPublish | (data) => void | null | Called when publish is triggered |

React-only props

| Prop | Type | Description | |---|---|---| | className | string | CSS class for the wrapper div | | style | object | Inline styles for the wrapper div | | onReady | (editor) => void | Called once the editor instance is initialized | | debounceMs | number | Debounce delay (ms) for onChange — reduces re-renders in controlled mode |


Public API

These methods are available on the NBDEditor instance (vanilla JS) or via the ref (React).

// Vanilla JS
const editor = new NBDEditor('#editor', { ... });

// React — via ref
const instance = editorRef.current?.getInstance();

| Method | Returns | Description | |---|---|---| | getData() | { content: string } | Get the full editor content | | setData(data) | void | Set content. Accepts { content: '...' } or a plain string | | toHTML() | string | Export all blocks as a merged HTML string | | save() | void | Trigger save — fires onSave callback and 'save' event | | publish() | void | Trigger publish — fires onPublish callback and 'publish' event | | undo(opts?) | boolean | Undo the last action | | redo(opts?) | boolean | Redo the last undone action | | destroy() | void | Tear down the editor and clean up listeners | | getTitle() | string | Get the current title value | | setTitle(title) | void | Set the title | | selectBlock(id) | void | Select a block by its ID | | focusBlock(id) | void | Select and focus a block | | insertBlockAtSelection(type, data?) | block | Insert a new block after the selected block | | insertBlockAtEnd(type, data?) | block | Append a new block at the end | | toggleBlockHtmlMode(blockId?) | void | Toggle a block between visual and raw HTML mode | | openBlockHtmlEditor(blockId?) | void | Open the HTML editor modal for a block |

Event Emitter

The editor extends EventEmitter. Listen for events:

editor.on('change', (data) => { /* content changed */ });
editor.on('save', (data) => { /* save triggered */ });
editor.on('publish', (data) => { /* publish triggered */ });
editor.on('autosave', (data) => { /* auto-save tick */ });
editor.on('block:select', (blockId) => { /* block selected */ });
editor.on('input', (block) => { /* block input */ });

Data Contract

The editor uses a simple content-only data format:

// Getting data
const data = editor.getData();
// => { content: '<h2>Title</h2><p>Body text...</p>' }

// Setting data
editor.setData({ content: '<p>New content</p>' });

// Getting raw HTML
const html = editor.toHTML();
// => '<h2>Title</h2><p>Body text...</p>'

Store the content string in your database. Pass it back to the editor to restore.


Block Types

The editor supports these block types out of the box:

Text blocks: Paragraph, Heading 2, Heading 3, Heading 4, List (bulleted), Ordered List, Quote, Pullquote, Code, Custom HTML

Media blocks: Image, Gallery, Video, Audio, File, Embed

Design blocks: Separator, Spacer, Columns, Table, Button

Users can insert blocks via the inserter panel (+ button) or the slash command menu (type / in an empty block).


Media Handling

For video and audio, users see a modal chooser with two options: Upload file or Use URL.

Control the default behavior with onChooseMediaSource:

new NBDEditor('#editor', {
  onChooseMediaSource: (type) => {
    // type is 'video' or 'audio'
    return 'upload'; // or 'url'
  },
  maxFileSize: 500 * 1024 * 1024, // 500 MB
  uploadHandler: async (file) => {
    const form = new FormData();
    form.append('file', file);
    const res = await fetch('/api/upload', { method: 'POST', body: form });
    return res.json(); // { url: 'https://...', id: '...' }
  },
});

Files exceeding maxFileSize are blocked from uploading. Users can also drag and drop files directly onto the editor canvas.


Theming

NBD Editor exposes a set of CSS custom properties on .fe-root that you can override to match your brand. Set them on .fe-root or any ancestor element:

.fe-root {
  --fe-blue: #8b5cf6;          /* accent / primary colour */
  --fe-blue-hover: #7c3aed;
  --fe-bg: #fafafa;             /* editor shell background */
  --fe-canvas-bg: #f9fafb;     /* writing canvas background */
  --fe-text: #111827;           /* primary text colour */
  --fe-font: 'Inter', sans-serif;
  --fe-radius: 8px;             /* border radius */
  --fe-content-font-size: 16px; /* body text size inside blocks */
  --fe-content-line-height: 1.7;
}

All available tokens:

| Variable | Default | Description | |---|---|---| | --fe-blue | #3b82f6 | Accent / primary colour | | --fe-blue-hover | #2563eb | Accent hover state | | --fe-text | #1e1e1e | Primary text colour | | --fe-light-text | #6b7280 | Secondary / muted text | | --fe-bg | #f8f9fb | Editor shell background | | --fe-canvas-bg | #ffffff | Writing canvas background | | --fe-hover-bg | #f3f4f6 | Hover / subtle highlight | | --fe-border | #e2e4e9 | Border colour | | --fe-success | #10b981 | Success state colour | | --fe-danger | #ef4444 | Destructive / error colour | | --fe-radius | 6px | Border radius | | --fe-font | system-ui stack | Font family | | --fe-shadow-sm | subtle shadow | Subtle shadow — used on cards and panels | | --fe-shadow-md | medium shadow | Medium shadow — used on dropdowns and modals | | --fe-header-height | 60px | Height of the sticky header bar | | --fe-status-bar-height | 32px | Height of the bottom status bar | | --fe-content-font-size | 15px | Body text size inside blocks | | --fe-content-line-height | 1.65 | Body line height inside blocks |


Keyboard Shortcuts

| Shortcut | Action | |---|---| | Ctrl/Cmd + S | Save | | Ctrl/Cmd + Z | Undo | | Ctrl/Cmd + Shift + Z / Ctrl/Cmd + Y | Redo | | Ctrl/Cmd + B | Bold | | Ctrl/Cmd + I | Italic | | Ctrl/Cmd + U | Underline | | Ctrl/Cmd + K | Insert link | | Ctrl/Cmd + A | Select all (within current block) | | Arrow Up (at block start) | Move to previous block | | Arrow Down (at block end) | Move to next block | | Tab (in table) | Move to next cell / create new row | | Shift + Tab (in table) | Move to previous cell | | Tab (in code block) | Insert 2 spaces | | / (in empty block) | Open slash command menu |


Events

editor.on('change', (data) => { });     // Any content change
editor.on('save', (data) => { });       // Save triggered (Ctrl+S or API)
editor.on('publish', (data) => { });    // Publish triggered
editor.on('autosave', (data) => { });   // Auto-save interval tick
editor.on('block:select', (id) => { }); // Block selected
editor.on('input', (block) => { });     // Block content input

Changelog

v1.1.1

Bug fixes

  • List blocks — bullet and ordered-list blocks now initialize with the correct <ul><li> / <ol><li> structure. Previously, empty list blocks rendered a bare <br>, which caused broken list editing behavior on first interaction.
  • Focus reliability — block focus and caret positioning after clicking a block now uses double requestAnimationFrame instead of setTimeout(0), preventing occasional cases where the caret landed in the wrong position before the DOM had fully painted.

Improvements

  • Click-to-deselect — clicking anywhere on the editor canvas outside a block now deselects the active block and returns the editor to its idle state.
  • Theme tokens — two new elevation CSS variables are available: --fe-shadow-sm and --fe-shadow-md. All CSS variable defaults have been refreshed to a cleaner, more neutral palette (new accent #3b82f6, updated greys, semantic colours, and layout tokens).

Build from Source

npm install
npm run build

Output files in dist/:

  • nbd-editor.esm.js — ES module
  • nbd-editor.cjs.js — CommonJS
  • nbd-editor.umd.js — UMD (for <script> tags)
  • nbd-editor.css — Styles

License

MIT — Noseberry Private Limited