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

trimedit

v0.0.1

Published

A rich-text editor built from scratch on contentEditable + the Selection/Range API — no editor libraries.

Readme

TrimEdit

A rich-text editor component for React, built directly on contentEditable and the browser's Selection/Range API — no editor library, no document.execCommand. Ships as a single component: TrimEdit.


Table of contents


Features

  • Text formatting — bold, italic, underline, inline code, font family, font size, and a font color picker (preset swatches).
  • Block formatting — paragraphs, headings H1–H6, blockquotes.
  • Alignment — left, center, right, justify.
  • Lists — bulleted and numbered, with indent/outdent (nests a list item under its previous sibling, or promotes it back out). Indent/outdent also works on non-list blocks.
  • Links — add/remove a link on the current selection.
  • Tables — insert a table at the cursor.
  • Undo/redo — its own history stack, with the usual Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, and Ctrl+Y shortcuts.
  • Source view — toggle between the WYSIWYG surface and a raw-HTML textarea.
  • Document-shell preservation — if the value you pass in is a full HTML document (<!DOCTYPE>, <head>, <style>, conditional comments, etc.), everything outside <body> is preserved as-is; editing only ever touches <body>'s contents. Safe to use for HTML email templates.
  • Clean paste — pasted content (from Word, Google Docs, other web pages, anywhere) is sanitized down to the tags this editor itself understands. No stray classes, inline styles, HTML comments, or wrapper <div>/<span> elements come along for the ride.
  • Configurable toolbar — show it above or below the editing surface, hide specific tools you don't want, or hide the toolbar entirely. See Props.
  • Accessible, icon-only toolbar — every control has a tooltip and an accessible label; active state is exposed via aria-pressed.

Installation

npm install trimedit

react and react-dom (18 or 19) are peer dependencies — install them if your project doesn't already have them. Nothing else is required; the editor bundles its own icons.

Quick start

import { useState } from 'react';
import { TrimEdit } from 'trimedit';
import 'trimedit/style.css';

function App() {
  const [html, setHtml] = useState('');

  return <TrimEdit className="editor" onChange={setHtml} />;
}

The CSS import is required — it's what styles the toolbar and editing surface. It only ever touches its own class names (.editor, .toolbar, .editor-surface, etc.), never global selectors, so it won't affect the rest of your app.

Props

| Prop | Type | Default | Description | |----------------------|--------------------------|---------|-------------| | initialValue | string | '' | Starting HTML — a bare fragment (<p>Hi</p>) or a full document. Uncontrolled: seeds the editor once on mount; change it later by remounting (e.g. a different key), same as a native <input defaultValue>. | | onChange | (html: string) => void | — | Called after every change with the current document HTML, in the same "shape" (fragment vs. full document) as initialValue. | | className | string | — | Applied to the outer wrapper. | | position | 'top' \| 'bottom' | 'top' | Where the toolbar renders relative to the editing surface. | | showToolbar | boolean | true | Set false to render just the editing surface, no toolbar at all. | | hiddenToolbarItems | ToolbarItemId[] | [] | Toolbar items to omit. Everything else still shows, in its normal left-to-right order. See the reference below for the full id list. |

Toolbar items reference

Ids for hiddenToolbarItems, grouped the way they render (also exported as TOOLBAR_ITEM_IDS):

| Group | Ids | |-------|-----| | History | undo, redo | | Paragraph style | paragraphStyle | | Font | fontFamily, fontSize, fontColor | | Marks | bold, italic, underline, code | | Alignment | alignLeft, alignCenter, alignRight, alignJustify | | Lists / quote | bulletList, numberedList, blockquote | | Indent | indentDecrease, indentIncrease | | Insert | link, table | | View | sourceToggle |

Hiding every item in a group also removes that group's divider — you'll never end up with a stray │ where a group used to be.

Recipes

Toolbar on the bottom:

<TrimEdit position="bottom" onChange={setHtml} />

Hide specific tools (keep everything else):

<TrimEdit
  hiddenToolbarItems={['link', 'table', 'indentDecrease', 'indentIncrease']}
  onChange={setHtml}
/>

No toolbar at all (e.g. you're building your own toolbar UI):

<TrimEdit showToolbar={false} onChange={setHtml} />

A full HTML email template (shell preserved automatically):

<TrimEdit
  initialValue={`<!DOCTYPE html>
<html>
  <head><style>body { font-family: Arial; }</style></head>
  <body><p>Hello</p></body>
</html>`}
  onChange={setHtml}
/>

Resetting content (initialValue is uncontrolled — remount by changing key):

<TrimEdit key={documentId} initialValue={loadedHtml} onChange={setHtml} />

Styling

trimedit/style.css covers every class name the component renders (.editor-surface, .toolbar*, .source-surface, .toolbar-dropdown*, etc.) — it's a complete default look, not just bare hooks, but every rule is scoped to those class names so you're free to override any of it with your own CSS.

Local development

Cloning this repo to work on the editor itself:

pnpm install
pnpm dev         # demo app at http://localhost:5173
pnpm typecheck
pnpm build       # builds the publishable package into dist/
pnpm preview     # preview a production build

Known limitations

  • Toggling a mark off when the selection only partially overlaps an existing one un-formats the whole matched element rather than splitting it at the selection boundary.
  • initialValue is uncontrolled — there's no controlled-value mode.
  • Toolbar dropdown contents (which headings, which preset colors) aren't individually configurable — hiddenToolbarItems hides a whole dropdown, not specific entries inside it.