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

@shashimadushan/docx-editor-editor

v0.6.1

Published

A clean, extensible, framework-agnostic DOCX editor built on TipTap. Load .docx, edit, save .docx — and add your own extensions. Word/Google-Docs-style paged layout.

Readme

@shashimadushan/docx-editor-editor

A clean, extensible, framework-agnostic DOCX editor. Word/Google-Docs-style paged layout, .docx load/save, 14 built-in extensions, and a public extension API for adding your own.

Install

npm install @shashimadushan/docx-editor-editor react react-dom

react and react-dom are peer dependencies (only needed for the React bindings).

Quick start (React)

import * as React from 'react';
import { ReactDocxEditor, type DocxEditor } from '@shashimadushan/docx-editor-editor/react';
import '@shashimadushan/docx-editor-editor/style.css';

export function App() {
  const [editor, setEditor] = React.useState<DocxEditor | null>(null);

  return (
    <div style={{ height: '100vh' }}>
      <ReactDocxEditor
        content="<h1>Hello, world!</h1><p>Start editing…</p>"
        onReady={setEditor}
        onUpdate={(json, html) => console.log('Document changed:', html)}
      />
    </div>
  );
}

⚠️ Important: The host element must have a defined height (e.g. height: 100vh or flex: 1 in a flex column). The editor uses a paged layout where only the page surface scrolls — if the host has no height, the editor won't be visible.

Quick start (vanilla JS)

import { DocxEditor } from '@shashimadushan/docx-editor-editor';
import '@shashimadushan/docx-editor-editor/style.css';

const editor = new DocxEditor({
  element: document.getElementById('host')!,
  content: '<p>Hello</p>',
  onUpdate: ({ json, html }) => console.log(html),
});

// Later:
editor.destroy();

<ReactDocxEditor /> props

| Prop | Type | Default | Description | |---|---|---|---| | content | string | '<p></p>' | Initial HTML content | | theme | DocxEditorTheme | Word-like defaults | Theme tokens (see below) | | editable | boolean | true | Whether the editor is editable | | filename | string | 'Untitled.docx' | Filename shown in the title bar | | onFilenameChange | (name: string) => void | — | Called when the user edits the filename | | placeholder | string | 'Start writing, or type "/" for commands…' | Empty-document placeholder | | disableBuiltins | string[] \| '*' | [] | Builtin extension IDs to disable, or '*' to disable all | | extensions | DocxEditorExtension[] | [] | Custom extensions to register | | tiptapExtensions | Extension[] | defaultTipTapExtensions() | Override the TipTap schema entirely | | onUpdate | (json, html) => void | — | Called on every content change | | onReady | (editor: DocxEditor) => void | — | Called once when the editor mounts | | renderToolbar | (editor) => ReactNode | default toolbar | Render-prop for a custom toolbar | | renderTitleBar | (editor) => ReactNode | default title bar | Render-prop for a custom title bar | | showTitleBar | boolean | true | Show/hide the default title bar | | showStatusBar | boolean | true | Show/hide the bottom status bar (word count) | | titleBarActions | ReactNode | — | Extra content appended to the title bar | | className | string | — | CSS class on the host wrapper | | style | CSSProperties | — | Inline style on the host wrapper |


DocxEditor class (framework-agnostic core)

Lifecycle

| Method | Description | |---|---| | new DocxEditor(opts) | Create the editor. Pass element to mount immediately. | | mount(el) | Mount into a DOM element (if not passed in constructor). | | destroy() | Destroy the editor and free all resources. |

.docx I/O

| Method | Returns | Description | |---|---|---| | loadDocx(buffer: ArrayBuffer) | Promise<void> | Load a .docx file into the editor. | | saveDocx(options?) | Promise<Blob> | Save the document as a .docx Blob. | | downloadDocx(filename?, options?) | Promise<void> | Trigger a browser download of the .docx. |

Content accessors

| Method | Returns | Description | |---|---|---| | getJSON() | any | Document as TipTap JSON | | setJSON(json) | void | Replace the document | | getHTML() | string | Document as HTML | | setHTML(html) | void | Replace the document | | getText() | string | Plain text (no formatting) | | setEditable(b) | void | Toggle editability | | getEditable() | boolean | Is the editor editable? |

Extension API

| Method | Description | |---|---| | registerExtension(ext) | Register an extension after creation | | getToolbarButtons() | Get all toolbar buttons (for custom UIs) | | getSlashCommands() | Get all slash commands | | getExtensionContext() | Get the context object passed to extension callbacks | | registry | The ExtensionRegistry instance |

DOM access

| Method | Returns | Description | |---|---|---| | getHost() | HTMLElement | The outermost host element | | getSurface() | HTMLElement | The gray scroll container | | getPage() | HTMLElement | The white "paper" element | | editor | TipTap Editor | The underlying TipTap Editor instance |

Loading overlay

| Method | Description | |---|---| | setLoading(loading, message?) | Show/hide a loading overlay on the editor surface |


Theme tokens

Override any of these via the theme prop or by setting CSS custom properties on a parent element:

<ReactDocxEditor
  theme={{
    background: '#f1f5f9',    // desk color (around the page)
    text: '#1f2328',           // default text color
    accent: '#2563eb',         // selection, active toolbar button
    border: '#d0d7de',         // table/border color
    fontFamily: 'Inter, sans-serif',
    fontSize: '11pt',
    pageWidth: '816px',        // 8.5in @ 96dpi
    pageHeight: '1056px',      // 11in @ 96dpi
    pageMarginX: '96px',       // 1in
    pageMarginY: '96px',
    pageGap: '24px',
    lineHeight: 1.5,
  }}
/>

Or via CSS:

.my-editor-host {
  --de-page-width: 21cm;
  --de-page-height: 29.7cm;
  --de-page-margin-x: 2cm;
  --de-page-margin-y: 2cm;
  --de-accent: #f59e0b;
  --de-font-family: "Inter", sans-serif;
}

Common page sizes

| Format | pageWidth | pageHeight | |---|---|---| | US Letter (default) | 816px (8.5in) | 1056px (11in) | | A4 | 794px (21cm) | 1123px (29.7cm) | | Legal | 816px (8.5in) | 1344px (14in) | | A5 | 559px (14.8cm) | 794px (21cm) |


Built-in extensions (14)

These are registered by default. Disable any with disableBuiltins:

| ID | Buttons / commands | |---|---| | bold | Bold (Ctrl+B) | | italic | Italic (Ctrl+I) | | underline | Underline (Ctrl+U) | | strike | Strikethrough | | highlight | Highlight toggle | | heading | H1, H2, H3 buttons + slash commands | | list | Bullet list, numbered list | | alignment | Left, center, right, justify | | codeBlock | /code slash command | | blockquote | /quote slash command | | horizontalRule | /hr slash command | | table | /table slash command | | image | /image slash command | | link | Link button (Ctrl+K) |

// Disable specific builtins:
<ReactDocxEditor disableBuiltins={['highlight', 'blockquote']} />

// Disable ALL builtins (use your own only):
<ReactDocxEditor disableBuiltins="*" extensions={[myCustomExtension]} />

Custom extensions

import type { DocxEditorExtension } from '@shashimadushan/docx-editor-editor';

const wordCountExtension: DocxEditorExtension = {
  id: 'word-count',
  name: 'Word count',
  toolbar: [{
    id: 'word-count',
    label: '📝 Words',
    onClick: (ctx) => {
      const text = ctx.editor.getText();
      const count = text.trim().split(/\s+/).filter(Boolean).length;
      alert(`${count} words`);
    },
  }],
};

<ReactDocxEditor extensions={[wordCountExtension]} />

DocxEditorExtension interface

interface DocxEditorExtension {
  id: string;                    // unique id
  name?: string;                 // display name
  tiptapExtensions?: Extension | Extension[];  // TipTap nodes/marks to register
  toolbar?: ToolbarButton[];     // toolbar buttons
  slashCommands?: SlashCommand[];// "/" menu items
  commands?: Record<string, (args: any) => Command>;  // custom editor commands
  onInit?: (ctx: ExtensionContext) => void;
  onDestroy?: (ctx: ExtensionContext) => void;
}

interface ToolbarButton {
  id: string;
  label: string;
  icon?: string;
  tooltip?: string;
  isActive?: (ctx: ExtensionContext) => boolean;
  isDisabled?: (ctx: ExtensionContext) => boolean;
  onClick: (ctx: ExtensionContext) => void;
}

interface ExtensionContext {
  editor: TipTapEditor;
  insertHTML(html: string): void;
  insertText(text: string): void;
  focus(): void;
  getJSON(): any;
  getHTML(): string;
  setJSON(json: any): void;
  setHTML(html: string): void;
}

Custom toolbar

Replace the entire default toolbar with renderToolbar:

<ReactDocxEditor
  renderToolbar={(editor) => (
    <div style={{ display: 'flex', gap: 4, padding: 8 }}>
      <button onClick={() => editor.editor.chain().focus().toggleBold().run()}>
        B
      </button>
      <button onClick={() => editor.editor.chain().focus().toggleItalic().run()}>
        I
      </button>
      <button onClick={() => editor.downloadDocx('my-doc.docx')}>
        Save
      </button>
    </div>
  )}
/>

.docx load/save

Load from file input

const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (!file || !editor) return;
  const buffer = await file.arrayBuffer();
  await editor.loadDocx(buffer);
};

<input type="file" accept=".docx" onChange={handleFile} />

Save to server

const saveToServer = async () => {
  const blob = await editor.saveDocx({ title: 'My Document', creator: 'Me' });
  const formData = new FormData();
  formData.append('file', blob, 'document.docx');
  await fetch('/api/upload', { method: 'POST', body: formData });
};

Save options

interface SaveDocxOptions {
  title?: string;    // document title (core properties)
  creator?: string;  // author name (core properties)
}

What's preserved when loading .docx

The mammoth style map maps these Word styles to semantic HTML:

| Word style | HTML element | |---|---| | Heading 1–9 | <h1>–<h6> | | Title | <h1 class="docx-title"> | | Subtitle | <h2 class="docx-subtitle"> | | Quote / Intense Quote | <blockquote> | | Code / Source Code | <pre> | | List Bullet / Number | <ul> / <ol> | | Bold / Italic / Underline / Strikethrough | <b> / <i> / <u> / <s> | | Superscript / Subscript | <sup> / <sub> | | Tables | <table> with <th> / <td> | | Images | <img src="data:..."> | | Hyperlinks | <a href="..."> | | Caption | <p class="docx-caption"> |

Not preserved (out of scope): headers/footers, footnotes/endnotes, page breaks, watermarks, content controls, floating images, embedded fonts.


CSS

The editor's CSS is bundled separately. Import it once in your app entry:

import '@shashimadushan/docx-editor-editor/style.css';
// or
import '@shashimadushan/docx-editor-editor/styles';

All classes are prefixed with docx-editor- to avoid collisions. All design tokens are CSS custom properties prefixed with --de-.


Package structure (for contributors)

src/react/ follows a one component per file convention. A file that used to export several related components (e.g. ToolbarPrimitives.tsx, MenuBar.tsx, ui-widgets.tsx, document-structure.tsx, context-menu/menuPrimitives.tsx, context-menu/TableContextMenu.tsx) is now a same-named folder containing one file per component plus an index.ts barrel that re-exports everything the original file exported — so external imports (from './MenuBar', from './document-structure', etc.) are unchanged. New multi-component additions should follow the same pattern: create ComponentGroup/Thing.tsx per component and re-export from ComponentGroup/index.ts.

Exception: src/react/icons.tsx stays a single file — it's a set of ~30 trivial one-line SVG icon components, and icon sets are conventionally bundled together (same as lucide-react/heroicons) rather than split one file per icon.

src/styles/ mirrors that structure for CSS: editor.css is a manifest of @import statements pulling in focused partials under base/ (reset, design tokens, outer layout), components/ (title bar, menu bar, toolbar, suggestions, table picker, watermark/header/footer, agent panel, status bar), editor-surface/ (paged paper, imported-.docx layout preservation, placeholder/page-break/pageless/image content blocks), tables/ (all in-document table styling), and theme/ (dark mode). print.css covers @media print. Add a new concern as a new partial + @import line rather than growing an existing file.

The build (tsup.config.ts's onSuccess) copies the whole src/styles/ tree to dist/styles/ so those relative @imports keep resolving, and publishes dist/style.css as a one-line shim (@import './styles/editor.css';) at the same path consumers already import (@shashimadushan/docx-editor-editor/style.css).

The public API surface is exactly what src/react.ts, src/index.ts, and src/headless.ts export — internal file moves must never change those barrels' exports.


License

MIT