velvet-writer
v1.3.5
Published
Zero-dependency React rich text editor with glassmorphism UI, slash commands, markdown shortcuts, tables, and dark mode. ~55KB minified. Tiptap & Quill alternative.
Maintainers
Keywords
Readme
Velvet Writer — React Rich Text Editor
Zero-dependency React rich text editor with a built-in glassmorphic UI, Notion-style slash commands, inline markdown shortcuts, contextual tables, and automatic light/dark mode. One component, no configuration maze.
npm install velvet-writer| | | | :--- | :--- | | 🎮 Live demo | poojagohel.github.io/velvet-writer | | ⚡ StackBlitz starter | Open in browser | | 📦 npm | velvet-writer | | 📊 Comparison | vs Tiptap, Slate, Quill |
⭐ If Velvet Writer saves you time, star the repo — it helps other developers discover the package.
Contents
- Why Velvet Writer
- Installation
- Quick start
- Variants
- Props reference
- Toolbar customization
- Toolbar position
- Theming
- Framework integration
- Advanced usage
- Features
- Keyboard & markdown shortcuts
- What you can import
- Support & contribution
- License
⚖️ Why Velvet Writer?
- Zero runtime dependencies — no Slate, no ProseMirror, no helper libraries. Pure React and native DOM APIs, ~55KB minified.
- Out-of-the-box UI — a single component installs a fully-styled, premium editor. No headless assembly required.
- Notion-style
/commands — type/to insert headings, quotes, tables, images, and more from a floating menu. - Inline markdown shortcuts —
**bold**,# heading,- list,`code`auto-format as you type. - Four visual variants — Premium, Flat, Classic, and Comment, each suited to a different UI context.
- Granular toolbar control — show/hide entire tool groups, or individual tools within a group.
- Production-ready — automated tests, CI, error boundaries, and safe local-storage auto-save.
📦 Installation
npm install velvet-writer
# or
yarn add velvet-writer[!NOTE] Optional emoji support The emoji toolbar button depends on
emoji-picker-react. Install it only if you want the picker enabled:npm install emoji-picker-reactWithout it, the emoji button and
insertEmojitool are automatically hidden — nothing breaks.
🚀 Quick Start
import { AdvanceTextEditor } from 'velvet-writer';
import 'velvet-writer/dist/index.css';
function App() {
return (
<AdvanceTextEditor
accentColor="#a855f7"
mode="system"
onChange={(html) => console.log(html)}
/>
);
}That's it — import the component, import the stylesheet once anywhere in your app, and render it. No providers, no context setup.
🎨 Variants
Pass variant to switch the editor's visual chrome. All four share the same props and behavior — only the shell styling changes.
| Variant | Best for | Look |
| :--- | :--- | :--- |
| 'premium' (default) | Landing pages, feature-rich editors | Frosted glassmorphism, rounded corners, soft shadow |
| 'flat' | Embedding inside your own card/panel | No shadow or background chrome — just the content |
| 'classic' | Documents, long-form writing | Paper-like sheet with a centered writing column |
| 'comment' | Chat boxes, discussion threads, replies | Compact, low-profile, action bar docked to one edge |
<AdvanceTextEditor variant="comment" toolbarPosition="bottom" maxHeight="240px" />⚙️ Props Reference
| Prop | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| accentColor | string | '#a855f7' | Primary color for highlights, the cursor, selection, and focus outlines. |
| mode | 'light' \| 'dark' \| 'system' | 'dark' | Color scheme. 'system' follows the OS/browser preference automatically. |
| variant | 'premium' \| 'flat' \| 'classic' \| 'comment' | 'premium' | Visual shell — see Variants. |
| toolbarPosition | 'top' \| 'bottom' | 'top' | Docks the formatting toolbar to the top or bottom of the editor. |
| placeholder | string | 'Start typing your masterpiece...' | Placeholder shown when the document is empty. |
| initialValue | string | '<p><br></p>' | Starting HTML content. |
| onChange | (html: string) => void | — | Called on every content change with clean, semantic HTML. |
| minHeight | string \| number | — | Minimum height of the writing area. |
| maxHeight | string \| number | 620px internally | Caps the writing area's height; content scrolls internally once it grows past this. |
| padding | string | — | Inner padding of the writing area (e.g. '24px 32px'). |
| fontSize | string | — | Base content font size (e.g. '1rem'). |
| autoSaveKey | string | — | When set, persists content to localStorage under this key and restores it on mount. |
| className | string | — | Extra class name(s) applied to the outer container. |
| visibleTools | ToolbarToolId[] \| object | MINIMAL_TOOLBAR_TOOLS | Which toolbar tools to show — see Toolbar customization. |
🛠️ Toolbar Customization
By default (no visibleTools passed) the toolbar shows only history and basicFormatting — a minimal starting point. Pass visibleTools to control exactly what's shown.
The full tool catalog
| Group ID | Label | Children (individually toggleable) |
| :--- | :--- | :--- |
| history | Undo / Redo | — |
| basicFormatting | Bold / Italic / Underline / Strike | — |
| blockFormat | Paragraph / Heading dropdown (H1–H4) | — |
| fontFamily | Font family dropdown | — |
| fontSize | Font size dropdown | — |
| textCase | UPPERCASE / lowercase / Title Case | — |
| lists | Ordered list, bullet list, blockquote | — |
| scripts | Subscript / superscript | — |
| indent | Outdent / indent | — |
| align | Left / center / right / justify | — |
| colors | Text color / highlight color | — |
| insert | Insert Media | insertLink, insertImage, insertTable, insertHr, insertEmoji |
| utility | Utilities | utilityZen, utilityExport, utilityClear, utilityCodeView, utilityFullscreen, utilityHelp |
TOOLBAR_TOOLS (exported) contains this same data at runtime — including label and description for each entry — so you can build a settings UI from it directly instead of hardcoding labels.
Presets
import { MINIMAL_TOOLBAR_TOOLS, DEFAULT_TOOLBAR_TOOLS } from 'velvet-writer';MINIMAL_TOOLBAR_TOOLS—['history', 'basicFormatting']. Used automatically ifvisibleToolsis omitted.DEFAULT_TOOLBAR_TOOLS— every top-level group (and therefore every child tool too).
Array syntax
Pass a flat array of ToolbarToolIds. A parent ID implicitly includes all of its children.
<AdvanceTextEditor
visibleTools={['history', 'basicFormatting', 'blockFormat', 'insert']}
// 'insert' here means: Link + Image + Table + Divider + Emoji, all shown
/>To show only some children of a group, list the child IDs instead of the parent:
<AdvanceTextEditor
visibleTools={['history', 'basicFormatting', 'insertLink', 'insertImage']}
// only Link and Image show — Table, Divider, and Emoji stay hidden
/>Object syntax
For a friendlier config shape (handy for JSON-driven settings, e.g. from a CMS or database), visibleTools also accepts an object — or an array mixing strings and objects:
<AdvanceTextEditor
visibleTools={{
history: true,
basicFormatting: true,
blockFormat: true,
insert: ['link', 'image', 'table'], // only these three Insert tools
utility: ['export', 'help'], // only these two Utility tools
}}
/>// Mixed array form works too
<AdvanceTextEditor
visibleTools={['history', 'basicFormatting', { insert: ['link', 'table'] }]}
/>Accepted child keys: insert → link, image, table, hr (alias divider), emoji. utility → zen, export, clear (alias clearFormatting), codeView, fullscreen, help.
Building a live toggle UI
import { useState } from 'react';
import { AdvanceTextEditor, TOOLBAR_TOOLS, MINIMAL_TOOLBAR_TOOLS, type ToolbarToolId } from 'velvet-writer';
import 'velvet-writer/dist/index.css';
function App() {
const [visibleTools, setVisibleTools] = useState<ToolbarToolId[]>(MINIMAL_TOOLBAR_TOOLS);
return (
<div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
{TOOLBAR_TOOLS.map((tool) => (
<button
key={tool.id}
title={tool.description}
onClick={() =>
setVisibleTools((prev) =>
prev.includes(tool.id) ? prev.filter((t) => t !== tool.id) : [...prev, tool.id]
)
}
>
{visibleTools.includes(tool.id) ? '✓' : '+'} {tool.label}
</button>
))}
</div>
<AdvanceTextEditor accentColor="#a855f7" mode="dark" visibleTools={visibleTools} />
</div>
);
}↕️ Toolbar Position
Dock the toolbar to either edge of the editor with toolbarPosition:
<AdvanceTextEditor toolbarPosition="bottom" />This is especially useful for the comment variant, where a bottom-docked action bar matches familiar chat/reply UI patterns (Slack, Discord, GitHub comments). All four variants support both positions.
🌈 Theming
accentColordrives every highlight in the UI — active toolbar buttons, the text cursor, selection color, link color, and focus rings. Any valid CSS color works.modeswitches between a built-in light and dark palette, or'system'to follow the OS preference reactively.- The editor's theme is fully independent of your app's own theme — you can run a dark-mode editor inside a light-mode page, or vice versa.
🖥️ Framework Integration
Vite / Create React App
Works out of the box. See the minimal Vite starter or run it on StackBlitz.
Next.js (App Router)
Velvet Writer touches the DOM directly, so load it client-side only:
'use client';
import dynamic from 'next/dynamic';
import 'velvet-writer/dist/index.css';
const AdvanceTextEditor = dynamic(
() => import('velvet-writer').then((m) => m.AdvanceTextEditor),
{ ssr: false, loading: () => <p>Loading editor…</p> }
);
export default function EditorPage() {
return (
<main style={{ padding: 24 }}>
<AdvanceTextEditor mode="system" accentColor="#a855f7" />
</main>
);
}🔌 Advanced Usage
Blog post / CMS editor with auto-save
import { useState } from 'react';
import { AdvanceTextEditor } from 'velvet-writer';
import 'velvet-writer/dist/index.css';
function BlogEditor() {
const [content, setContent] = useState('');
const handlePublish = async () => {
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ htmlContent: content }),
});
};
return (
<div style={{ maxWidth: 800, margin: '0 auto', padding: 20 }}>
<AdvanceTextEditor
accentColor="#6366f1"
mode="light"
variant="flat"
placeholder="Start draft..."
onChange={setContent}
autoSaveKey="my-unique-blog-key"
/>
<button onClick={handlePublish} style={{ marginTop: 12 }}>Publish Post</button>
</div>
);
}Editing existing content
import { AdvanceTextEditor } from 'velvet-writer';
import 'velvet-writer/dist/index.css';
function EditPost({ initialHtml }: { initialHtml: string }) {
return (
<AdvanceTextEditor
initialValue={initialHtml}
accentColor="#10b981"
mode="dark"
variant="premium"
minHeight="500px"
maxHeight="800px"
padding="30px 40px"
fontSize="1.1rem"
/>
);
}React Hook Form
import { Controller, useForm } from 'react-hook-form';
import { AdvanceTextEditor } from 'velvet-writer';
import 'velvet-writer/dist/index.css';
function FormWrapper() {
const { control, handleSubmit } = useForm({
defaultValues: { bodyContent: '<h2>Pre-filled Header</h2>' },
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="bodyContent"
control={control}
render={({ field }) => (
<AdvanceTextEditor initialValue={field.value} onChange={field.onChange} accentColor="#a855f7" />
)}
/>
<button type="submit">Submit</button>
</form>
);
}Comment / reply box
<AdvanceTextEditor
variant="comment"
toolbarPosition="bottom"
visibleTools={['basicFormatting', 'insertLink', 'utilityHelp']}
maxHeight="200px"
placeholder="Write a reply…"
/>✨ Features
- Slash commands — type
/for a floating menu to insert paragraphs, headings, quotes, tables, images, and more. - Interactive tables — insert via a row/column grid picker or custom size input; a floating contextual toolbar lets you add/delete rows and columns from any cell.
- Image resizing — click any inserted image to drag-resize it directly, with a numeric width/height panel.
- Zen mode — a distraction-free focus toggle for long-form writing.
- Code view — toggle between rich text and the raw HTML source, with a built-in formatter.
- Export — export to Markdown (
.md), print/PDF, or copy clean HTML to the clipboard. - Word/character count with goals — live counts in the status bar; click to set a target word count and track progress.
- Auto-save — opt-in local-storage persistence via
autoSaveKey, restored automatically on mount. - Fixed height + internal scroll — the writing area caps at a sensible height by default (override with
maxHeight) instead of growing the page indefinitely. - Fullscreen — expand the editor to fill the viewport for focused editing sessions.
⌨️ Keyboard & Markdown Shortcuts
| Shortcut | Action |
| :--- | :--- |
| Ctrl/Cmd + B | Bold |
| Ctrl/Cmd + I | Italic |
| Ctrl/Cmd + U | Underline |
| Ctrl/Cmd + S or Ctrl/Cmd + Shift + X | Strikethrough |
| Ctrl/Cmd + Z | Undo |
| Ctrl/Cmd + Y or Ctrl/Cmd + Shift + Z | Redo |
| Tab / Shift + Tab | Indent/outdent a list item, or move between table cells when inside a table |
[!NOTE]
Ctrl/Cmd + Striggers strikethrough inside the editor rather than a browser "Save Page" dialog — this only applies while the editor is focused.
Markdown shortcuts (type at the start of a line, then Space)
| Syntax | Result |
| :--- | :--- |
| # | Heading 1 |
| ## | Heading 2 |
| ### | Heading 3 |
| > | Blockquote |
| - | Bulleted list |
| 1. | Numbered list |
Inline markdown (parsed as you type)
| Syntax | Result |
| :--- | :--- |
| **text** | Bold |
| *text* | Italic |
| ~~text~~ | ~~Strikethrough~~ |
| `code` | Inline code |
📤 What You Can Import
import {
AdvanceTextEditor, // the editor component
EDITOR_VERSION, // current package version, as a string
TOOLBAR_TOOLS, // full tool catalog with labels/descriptions/children
DEFAULT_TOOLBAR_TOOLS, // every tool group enabled
MINIMAL_TOOLBAR_TOOLS, // ['history', 'basicFormatting']
type AdvanceTextEditorProps,
type EditorMode, // 'light' | 'dark' | 'system'
type EditorStats, // { words: number; characters: number }
type ToolbarToolId,
type ToolbarToolMeta,
} from 'velvet-writer';
import 'velvet-writer/dist/index.css';🤝 Support & Contribution
Contributions are welcome — open an issue to discuss a change before submitting a large PR.
📄 License
MIT © Pooja Gohel
