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.
- Features
- Installation
- Quick start
- Props
- Toolbar items reference
- Recipes
- Styling
- Local development
- Known limitations
- 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.
npm install trimeditreact 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.
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.
| 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. |
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.
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} />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.
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- 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.
initialValueis uncontrolled — there's no controlled-valuemode.- Toolbar dropdown contents (which headings, which preset colors) aren't
individually configurable —
hiddenToolbarItemshides a whole dropdown, not specific entries inside it.