rich-text-lite
rich-text-lite
A lightweight, zero-dependency (besides React) rich text editor React component with a fully customizable toolbar.
Table of Contents
- Installation
- Quick Start
- Props
- Editor Events
- Features
- Toolbar Configuration
- Toolbar Buttons Reference
- CSP / Nonce Support
- Styling & CSS Customization
- Output Format
- Development
Installation
npm install rich-text-liteQuick Start
import { useState } from "react";
import { RichTextEditor } from "rich-text-lite";
import "rich-text-lite/dist/style.css";
function App() {
const [html, setHtml] = useState("");
return (
<RichTextEditor
value={html}
onChange={setHtml}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string |
"" |
HTML content for the editor |
onChange |
(html: string) => void |
— | Called with the updated HTML string on each edit |
onEditorChange |
(event: React.FormEvent<HTMLDivElement>, html: string) => void |
— | Called on editor input with both the native React event and latest HTML |
onFocus |
(event: React.FocusEvent<HTMLDivElement>) => void |
— | Called when the editor receives focus |
onBlur |
(event: React.FocusEvent<HTMLDivElement>) => void |
— | Called when the editor loses focus |
onKeyDown |
(event: React.KeyboardEvent<HTMLDivElement>) => void |
— | Called on keydown inside the editor (after internal shortcuts are handled) |
onKeyPress |
(event: React.KeyboardEvent<HTMLDivElement>) => void |
— | Called on keypress inside the editor |
onSelect |
(event: React.SyntheticEvent<HTMLDivElement>) => void |
— | Called when a selection event is fired by the editor element |
getEditorHTML |
(getter: () => string) => void |
— | Called with a getter function that returns the latest editor HTML on demand |
isNonceEnabled |
boolean |
false |
Whether to add a nonce attribute to injected <style> tags (for CSP) |
nonceValue |
string |
"" |
The nonce value to use when isNonceEnabled is true |
nonceHeaders |
string |
"" |
CSP policy string applied to the nonce meta tag when isNonceEnabled is true |
placeholder |
string |
"Start typing..." |
Placeholder text shown when editor is empty |
cleanPaste |
boolean |
true |
Sanitizes pasted HTML while keeping common rich text tags |
toolbarConfig.editor.placeholder |
string |
"Start typing..." |
Editor placeholder from config JSON (overrides default placeholder) |
toolbarConfig.editor.events |
object |
{} |
Alternate place to provide editor events (onChange, onFocus, onBlur, onKeyDown, onKeyPress, onSelect) |
toolbarConfig.selectComponent |
React.ComponentType |
built-in button dropdown | Custom UI component for toolbar dropdowns (heading, font, size, line-height) |
toolbarConfig.selectOptionComponent |
React.ComponentType |
built-in <option> |
Option item component for selectComponent (e.g. MUI MenuItem) |
toolbarConfig.dialogComponent |
React.ComponentType |
built-in Material UI Dialog |
Custom UI component for the link dialog wrapper |
toolbarConfig.lineHeight.values |
Array<string|number|{ value, label }> |
['1', '1.15', '1.5', '1.75', '2'] |
Configurable values for the line-height dropdown |
toolbarConfig.linkConfig |
object |
{} |
Configures link dialog fields and link-hover quick action popup |
toolbarConfig |
object |
{} |
Configuration object for customizing the toolbar (see below) |
Editor Events
You can subscribe to editor events using either top-level props or toolbarConfig.editor.events.
Top-level props take precedence when both are provided.
<RichTextEditor
value={html}
onChange={(nextHtml) => setHtml(nextHtml)}
onEditorChange={(event, nextHtml) => {
// event target + latest HTML in one callback
console.log("changed", nextHtml);
}}
onFocus={() => console.log("focus")}
onBlur={() => console.log("blur")}
onKeyDown={(e) => console.log("keydown", e.key)}
onKeyPress={(e) => console.log("keypress", e.key)}
onSelect={() => console.log("select")}
toolbarConfig={{
editor: {
events: {
// Used only when corresponding top-level prop is not provided
onFocus: () => console.log("focus from config"),
},
},
}}
/>import { useRef } from "react";
const getHtmlRef = useRef(() => "");
<RichTextEditor
value={html}
onChange={setHtml}
getEditorHTML={(getter) => {
getHtmlRef.current = getter;
}}
/>
// Call this whenever you need current editor HTML
const latestHtml = getHtmlRef.current();Notes:
onChangeremains the HTML-first callback:(html) => voidonEditorChangeis event-first and also provides HTML:(event, html) => voidonKeyDownstill includes built-in undo/redo shortcut handling by the editor
Features
| Feature | Description |
|---|---|
| Headings | Normal, H1, H2, H3, H4 |
| Font Family | Sans Serif, Serif, Monospace |
| Font Size | 10px–30px (2px increments), or "Default" to reset |
| Line Height | Dropdown with configurable values from toolbarConfig.lineHeight.values |
| Bold | Toggle bold |
| Italic | Toggle italic |
| Underline | Toggle underline |
| Strikethrough | Toggle strikethrough |
| Superscript | Toggle superscript |
| Subscript | Toggle subscript |
| Text Color | Color palette + custom hex + custom RGB input |
| Background Color | Color palette + custom hex + custom RGB input |
| Hyperlinks | Insert, edit, and remove links (opens in new tab) |
| Bullet List | Unordered list with style options: disc, circle, square |
| Numbered List | Ordered list with style options: decimal, upper-roman, lower-roman, lower-latin, upper-latin, lower-greek |
| Alignment | Align left, center, right, or justify |
| Indent / Outdent | Increase or decrease indentation |
| Text Direction | Toggle LTR / RTL |
| Insert Line | Inserts a horizontal rule (<hr>) |
| Undo / Redo | Toolbar buttons + keyboard shortcuts (Ctrl/Cmd+Z, Ctrl+Y, Cmd/Ctrl+Shift+Z) |
| Clear Formatting | Removes inline formatting from selected content |
| Clean Paste | Removes unsafe/noisy pasted markup while preserving common rich text |
Toolbar Configuration
All toolbar customization is done through the toolbarConfig prop.
<RichTextEditor
value={html}
onChange={setHtml}
toolbarConfig={{
lineHeight: {
values: [
"1",
{ value: "1.2", label: "Normal" },
{ value: "1.5", label: "Comfortable" },
{ value: "2", label: "Double" },
],
},
tooltipComponent: MyTooltip, // optional custom tooltip
options: {
bold: { visible: true, tooltip: "Bold (Ctrl+B)", icon: <MyBoldIcon /> },
italic: { visible: false }, // hides the italic button
// ... other buttons
},
}}
/>Hiding Buttons
Set visible: false on any button to hide it:
toolbarConfig={{
options: {
strikethrough: { visible: false },
direction: { visible: false },
}
}}Custom Tooltips
Override the tooltip text for any button:
toolbarConfig={{
options: {
bold: { tooltip: "Make Bold (Ctrl+B)" },
link: { tooltip: "Add Hyperlink" },
}
}}Custom Icons
Pass any React element as the icon for a button:
import { FaBold, FaItalic } from "react-icons/fa";
toolbarConfig={{
options: {
bold: { icon: <FaBold /> },
italic: { icon: <FaItalic /> },
}
}}For dropdown controls (heading, font, size, lineHeight), if an icon is provided then the trigger shows icon-only mode.
You can control trigger width with width in the same option object.
toolbarConfig={{
options: {
heading: { icon: <span>H</span>, width: 42 },
font: { icon: <span>F</span>, width: 40 },
size: { icon: <span>T</span>, width: 38 },
lineHeight: { icon: <span>LH</span>, width: 46 },
table: {
tableActions: {
rowActions: { icon: <span>R</span>, tooltip: "Row Actions", width: 40 },
columnActions: { icon: <span>C</span>, tooltip: "Column Actions", width: 40 },
deleteTable: { icon: <span>Del</span>, tooltip: "Delete Table" },
},
},
}
}}Table Action Popup Config
When you click a table cell, a compact table action popup appears in the editor.
These actions are configurable through toolbarConfig.options.table.tableActions and follow the same pattern as toolbar buttons.
Supported keys:
cellActions(alias:tableCellActions) - cell action dropdown triggercellStyleActions(aliases:tableCellStyleActions,cellStyles) - cell style dropdown triggercellProperties(alias:tableCellProperties) - cell properties popup triggertableProperties(alias:tableStyleProperties) - table properties popup triggerverticalAlign(aliases:tableVerticalAlignActions,tableVerticalAlign) - vertical align dropdown triggerhorizontalAlign(aliases:tableHorizontalAlignActions,tableHorizontalAlign) - horizontal align dropdown triggerrowActions(alias:tableRowActions) — row action dropdown triggercolumnActions(alias:tableColumnActions) — column action dropdown triggerdeleteTable(alias:tableDelete) — delete table button
Each key accepts:
{
visible?: boolean; // default: true
tooltip?: string; // custom tooltip
icon?: ReactNode; // custom icon
width?: number; // icon trigger width in px (dropdown triggers)
}Example:
toolbarConfig={{
options: {
table: {
tableActions: {
cellActions: {
visible: true,
tooltip: "Cell Actions",
icon: <MyCellIcon />,
width: 42,
},
cellStyleActions: {
visible: true,
tooltip: "Cell Style Actions",
icon: <MyCellStyleIcon />,
width: 42,
},
cellProperties: {
visible: true,
tooltip: "Cell Properties",
icon: <MyCellPropertiesIcon />,
panel: {N
fields: {
borderStyle: {
visible: true,
options: ["solid", "inset", "dashed", "double"],
},
borderColor: { visible: true },
borderWidth: { visible: true, min: 0, max: 10 },
backgroundColor: { visible: true },
},
},
},
tableProperties: {
visible: true,
tooltip: "Table Properties",
icon: <MyTablePropertiesIcon />,
panel: {
fields: {
borderStyle: {
visible: true,
options: ["solid", "inset", "dashed", "double"],
},
borderColor: { visible: true },
borderWidth: { visible: true, min: 0, max: 10 },
backgroundColor: { visible: true },
fontColor: { visible: true },
align: { visible: true, options: ["left", "center", "right"] },
},
},
},
verticalAlign: {
visible: true,
tooltip: "Vertical Align",
icon: <MyVerticalAlignIcon />,
width: 42,
},
horizontalAlign: {
visible: true,
tooltip: "Horizontal Align",
icon: <MyHorizontalAlignIcon />,
width: 42,
},
rowActions: {
visible: true,
tooltip: "Row Actions",
icon: <MyRowIcon />,
width: 42,
},
columnActions: {
visible: true,
tooltip: "Column Actions",
icon: <MyColumnIcon />,
width: 42,
},
deleteTable: {
visible: true,
tooltip: "Delete Table",
icon: <MyDeleteIcon />,
},
},
},
},
}}Custom Tooltip Component
Replace the built-in tooltip with your own component (e.g., Material UI Tooltip, Radix Tooltip, etc.). Your component must accept title and children props:
import { Tooltip as MuiTooltip } from "@mui/material";
function MyTooltip({ title, children }) {
return (
<MuiTooltip title={title} arrow placement="top">
{children}
</MuiTooltip>
);
}
<RichTextEditor
value={html}
onChange={setHtml}
toolbarConfig={{
tooltipComponent: MyTooltip,
}}
/>The custom component receives these props:
| Prop | Type | Description |
|---|---|---|
title |
string |
The tooltip text |
children |
ReactNode |
The button element to wrap |
arrow |
boolean |
Always true (hint for arrow display) |
placement |
string |
Always "top" |
Custom Select Component (Dropdown UI)
You can replace the built-in button dropdown controls (heading, font, size, line-height) with your own UI component.
Your component receives these props:
classNamevalueonChangeonBlurdisabledoptions(array of{ value, label, disabled?, hidden? })icon(icon configured intoolbarConfig.optionsfor that dropdown)tooltip(resolved tooltip text for that dropdown)iconOnly(boolean, true when icon-mode trigger is active)triggerWidth(resolved icon-mode width in px)children(native<option>elements for compatibility)
Example:
function MySelect({ className, value, onChange, disabled, options }) {
return (
<select
className={className}
value={value}
onChange={(e) => onChange(e)}
disabled={disabled}
>
{options.map((opt) => (
<option key={opt.value} value={opt.value} disabled={opt.disabled} hidden={opt.hidden}>
{opt.label}
</option>
))}
</select>
);
}
<RichTextEditor
value={html}
onChange={setHtml}
toolbarConfig={{
selectComponent: MySelect,
}}
/>For MUI Select, MenuItem is auto-used by default. You can still override with selectOptionComponent if needed:
import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
<RichTextEditor
value={html}
onChange={setHtml}
toolbarConfig={{
selectComponent: Select,
selectOptionComponent: MenuItem,
}}
/>Custom Dialog Component
You can replace the default link dialog wrapper by passing toolbarConfig.dialogComponent.
Your dialog component receives these props:
openonCloseclassNamepaperClassNametitlechildren
Example:
function MyDialog({ open, onClose, className, children }) {
if (!open) return null;
return (
<div className={className} role="dialog" aria-modal="true">
<div className="my-dialog-backdrop" onClick={onClose} />
<div className="my-dialog-panel">{children}</div>
</div>
);
}
<RichTextEditor
value={html}
onChange={setHtml}
toolbarConfig={{
dialogComponent: MyDialog,
}}
/>Link Config (Dialog + Hover Popup)
The link system supports extra fields in the insert/edit dialog:
- Text to display
- Title
- Open in new window
- Download link
It also supports a hover quick-action popup on links (enabled by default) with:
- Edit
- Copy
- Preview
- Unlink
All of this is configurable through toolbarConfig.linkConfig:
<RichTextEditor
value={html}
onChange={setHtml}
toolbarConfig={{
editor: {
placeholder: "Write your content here...",
},
linkConfig: {
dialog: {
text: {
title: "Insert Link",
urlLabel: "URL",
urlPlaceholder: "Enter URL...",
textLabel: "Text To Display",
textPlaceholder: "Displayed text",
titleLabel: "Title",
titlePlaceholder: "Tooltip title",
openInNewWindowLabel: "Open In New Window",
downloadLabel: "Download Link",
},
fields: {
text: { visible: true },
title: { visible: true },
openInNewWindow: { visible: true },
download: { visible: true },
},
actions: {
apply: {
visible: true,
icon: "✓",
label: "Apply",
tooltip: "Apply Link",
showLabel: false,
},
copy: {
visible: true,
icon: "⎘",
label: "Copy",
tooltip: "Copy Link",
showLabel: false,
},
preview: {
visible: true,
icon: "↗",
label: "Preview",
tooltip: "Open Link",
showLabel: false,
},
unlink: {
visible: true,
icon: "✕",
label: "Unlink",
tooltip: "Remove Link",
showLabel: false,
},
},
},
hoverPopup: {
enabled: true,
actions: {
edit: { visible: true, icon: "✎", label: "Edit", tooltip: "Edit Link", showLabel: false },
copy: { visible: true, icon: "⎘", label: "Copy", tooltip: "Copy Link", showLabel: false },
preview: { visible: true, icon: "↗", label: "Preview", tooltip: "Open Link", showLabel: false },
unlink: { visible: true, icon: "✕", label: "Unlink", tooltip: "Remove Link", showLabel: false },
},
},
},
}}
/>Defaults are applied automatically when any of these properties are omitted.
To disable link hover popup globally:
toolbarConfig={{
linkConfig: {
hoverPopup: {
enabled: false,
},
},
}}Toolbar Buttons Reference
These are the keys you can use inside toolbarConfig.options:
| Key | Default Tooltip | Description |
|---|---|---|
bold |
"Bold" | Bold button |
italic |
"Italic" | Italic button |
underline |
"Underline" | Underline button |
strikethrough |
"Strikethrough" | Strikethrough button |
superscript |
"Superscript" | Superscript button |
subscript |
"Subscript" | Subscript button |
link |
"Insert Link" | Link button |
fontColor |
"Text Color" | Font color picker button |
bgColor |
"Background Color" | Background color picker button |
heading |
"Headings" | Heading dropdown |
font |
"Font Family" | Font-family dropdown |
size |
"Font Size" | Font-size dropdown |
lineHeight |
"Line Height" | Line-height dropdown |
bulletList |
"Unordered List" | Unordered list style selector (disc/circle/square) |
numberedList |
"Ordered List" | Ordered list style selector (decimal, upper-roman, lower-roman, lower-latin, upper-latin, lower-greek) |
alignLeft |
"Align Left" | Align text left |
alignCenter |
"Align Center" | Align text center |
alignRight |
"Align Right" | Align text right |
alignJustify |
"Align Justify" | Justify text |
decreaseIndent |
"Decrease Indent" | Outdent button |
increaseIndent |
"Increase Indent" | Indent button |
direction |
"Switch to Right-to-Left" | Text direction toggle |
insertLine |
"Insert Line" | Insert horizontal rule (<hr>) |
undo |
"Undo" | Undo button |
redo |
"Redo" | Redo button |
clearFormatting |
"Clear Formatting" | Remove formatting button |
Each key accepts:
{
visible?: boolean; // default: true — set false to hide
tooltip?: string; // override default tooltip text
icon?: ReactNode; // override default icon/label
width?: number; // icon-mode trigger width in px (for heading/font/size/lineHeight)
}Table Hover Action Keys
Configure table hover popup actions using toolbarConfig.options.table.tableActions.
| Key | Default Tooltip | Description |
|---|---|---|
cellActions |
"Cell Actions" | Table cell actions dropdown trigger (alias: tableCellActions) |
cellStyleActions |
"Cell Style Actions" | Table cell style dropdown trigger (aliases: tableCellStyleActions, cellStyles) |
cellProperties |
"Cell Properties" | Table cell properties popup trigger (alias: tableCellProperties) |
verticalAlign |
"Vertical Align" | Table vertical align dropdown trigger (aliases: tableVerticalAlignActions, tableVerticalAlign) |
horizontalAlign |
"Horizontal Align" | Table horizontal align dropdown trigger (aliases: tableHorizontalAlignActions, tableHorizontalAlign) |
rowActions |
"Row Actions" | Table row actions dropdown trigger (alias: tableRowActions) |
columnActions |
"Column Actions" | Table column actions dropdown trigger (alias: tableColumnActions) |
deleteTable |
"Delete Table" | Delete table action button (alias: tableDelete) |
cellStyleActions dropdown options:
Highlighted: applies1px solid redborder to active/selected cells; selecting again resets the borderThick: applies1px double redborder to active/selected cells; selecting again resets the border
Each key accepts:
{
visible?: boolean; // default: true
tooltip?: string; // override default tooltip text
icon?: ReactNode; // override default icon/label
width?: number; // icon-mode trigger width in px (dropdown triggers)
panel?: {
fields?: {
borderStyle?: { visible?: boolean; options?: string[] }; // alias: borderType
borderColor?: { visible?: boolean };
borderWidth?: { visible?: boolean; min?: number; max?: number };
backgroundColor?: { visible?: boolean };
fontColor?: { visible?: boolean };
bold?: { visible?: boolean };
italic?: { visible?: boolean };
underline?: { visible?: boolean };
strikeThrough?: { visible?: boolean }; // alias: strikethrough
};
};
}CSP / Nonce Support
The editor injects a <style> tag at runtime for dynamic color/font classes. If your app uses a Content Security Policy, pass a nonce:
<RichTextEditor
value={html}
onChange={setHtml}
isNonceEnabled={true}
nonceValue="abc123"
nonceHeaders="style-src 'self' 'nonce-abc123';"
/>This adds nonce="abc123" to the injected style element.
Use nonceHeaders when you also want to pass the CSP policy string used by your app.
Styling & CSS Customization
Import the required stylesheet:
import "rich-text-lite/dist/style.css";Key CSS Classes
| Class | Element |
|---|---|
.rte-container |
Outermost wrapper |
.rte-toolbar |
Toolbar row |
.rte-editor |
The contentEditable area |
.rte-btn |
All toolbar buttons |
.rte-btn-active |
Active/toggled toolbar button |
.rte-select |
Dropdown selects (heading, font, size) |
.rte-color-picker |
Color picker popup |
.rte-link-popup |
Link insertion popup |
Overriding Styles
You can override any class in your own CSS:
/* Change editor min-height */
.rte-editor {
min-height: 300px;
}
/* Change toolbar background */
.rte-toolbar {
background: #f5f5f5;
}
/* Change active button color */
.rte-btn-active {
background: #dbeafe;
color: #1d4ed8;
}
/* Style the dropdowns */
.rte-select {
border-radius: 4px;
font-size: 13px;
}Output Format
The onChange callback receives raw HTML from the contentEditable area. Example output:
<p><b>Hello</b> <span style="font-size: 20px">world</span></p>
<h3>A heading</h3>
<ul><li>Item one</li><li>Item two</li></ul>You can render this HTML anywhere using dangerouslySetInnerHTML or a sanitizer like DOMPurify.
Development
git clone <repo-url>
cd newgen-rich-text-editor
npm install
npm run devStarts a dev server at http://localhost:5173 with a demo page.
Build
npm run buildOutputs to dist/:
rich-text-lite.es.js— ES modulerich-text-lite.umd.js— UMD bundlestyle.css— Required styles
License
MIT