Skip to content

Repository files navigation

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.

About

A lightweight WYSIWYG editor that produces clean HTML.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages