Rich Text Editor — Free HTML CSS JS WYSIWYG Snippet

Rich Text Editor · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

contenteditable editor: browser-native WYSIWYG editing with full keyboard and clipboard support
Active toolbar states: queryCommandState() toggles .active on Bold/Italic/Underline/Strikethrough/List/Align buttons on every cursor move
Inline link insertion: saves and restores selection range to insert links without losing the user's text selection
H1/H2 block formatting: formatBlock execCommand converts current paragraph to heading tags with styled typography
Ordered and unordered lists: insertOrderedList/insertUnorderedList with correct tab-level indentation via CSS padding-left
Text alignment: justifyLeft/justifyCenter/justifyRight with active state tracking per paragraph block
Placeholder overlay: absolutely positioned div that hides when editor has content — works with contenteditable unlike native placeholder
Live character count and HTML output: textContent.length on input event, innerHTML dump to a read-only copyable textarea

About this UI Snippet

Rich Text Editor — WYSIWYG contenteditable Toolbar, Active States, Link Insertion & HTML Output

Screenshot of the Rich Text Editor snippet rendered live

A rich text editor is one of the most requested UI components for web apps — comment boxes (add an @mention autocomplete), blog editors (or a markdown live preview), email composers, note-taking tools, and form text areas all benefit from giving users basic formatting control. Building one from scratch with pure HTML, CSS, and vanilla JavaScript is entirely possible using the browser's built-in contenteditable attribute and the document.execCommand() API. This snippet delivers a complete, production-quality WYSIWYG editor: a styled toolbar with 14 controls, active state tracking, inline link insertion, a live character count, and an HTML output viewer — with zero dependencies.

How contenteditable and execCommand work

The contenteditable="true" attribute on a div turns it into an editable region. The browser manages its own internal selection and editing state. document.execCommand(commandName, false, value) operates on the current selection inside the focused contenteditable element. Commands like bold, italic, underline, and strikeThrough toggle inline formatting tags (<b>, <i>, <u>, <strike>) around the selected text. formatBlock wraps the current block in a heading (H1, H2) or paragraph tag. insertOrderedList and insertUnorderedList convert the current paragraph to a list. justifyLeft, justifyCenter, and justifyRight set the text-align of the current block.

Active toolbar state with queryCommandState

document.queryCommandState(commandName) returns true when the current selection or cursor position is inside text that has that formatting applied. The updateToolbar() function iterates over every toolbar button with a data-cmd attribute and calls queryCommandState to toggle the .active CSS class. This function is called on every keyup, mouseup, and selectionchange event — so the toolbar always reflects the formatting at the cursor. When the user clicks inside a bold word, the Bold button immediately appears pressed.

Inline link insertion with selection preservation

Inserting a link requires the user to first select text, then open the link input row without losing the selection. When the link button is clicked, the current selection range is cloned and stored in savedRange using window.getSelection().getRangeAt(0).cloneRange(). The link input row slides open and focuses the URL input. When the user clicks Insert (or presses Enter), the saved range is restored via sel.removeAllRanges(); sel.addRange(savedRange) before calling exec('createLink', url). After insertion, all new <a> elements without a target get target="_blank" and rel="noopener noreferrer" applied automatically.

Placeholder text without a real input

A contenteditable div does not support the native placeholder attribute. This snippet overlays an absolutely positioned div.placeholder on top of the editor area. The syncPlaceholder() function hides it (adds the .hidden class, which sets opacity: 0) whenever editor.textContent.length > 0 or the editor contains any HTML tags. This covers both typed text and formatted blocks like headings or lists that produce HTML but may have no visible characters yet.

Live character count and HTML output

The character count reads editor.textContent.length on every input event — textContent strips all HTML tags and counts only visible characters. The "Get HTML" button reads editor.innerHTML and injects it into a read-only textarea below the editor. The textarea auto-selects on open so the user can copy the HTML immediately. This makes the snippet useful for generating HTML content that will be saved to a database or CMS.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to work through the selection-preservation trick by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the link button clones the current selection range into a saved variable before the URL input steals focus, and why that saved range must be restored onto window.getSelection before calling the createLink command. The same assistant can help you optimize it — ask whether calling updateToolbar on every keyup, mouseup, and selectionchange event is redundant given they can fire in quick succession, and whether that could be debounced without making the toolbar feel laggy. It's also useful for extending the editor: ask it to add a font-size or text-color dropdown wired through execCommand, implement a real placeholder-clearing fix for headings and lists that produce empty-looking HTML, or add paste handling that strips foreign formatting from pasted content. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a WYSIWYG rich text editor in plain HTML, CSS, and JavaScript using a contenteditable div and document.execCommand — no external editor library, no ProseMirror or Quill.

Requirements:
- A contenteditable div as the editing surface, with a toolbar of buttons for bold, italic, underline, strikethrough, heading levels, ordered and unordered lists, text alignment (left/center/right), and undo/redo, where each button's click calls execCommand with the button's associated command name and optional value, then refocuses the editor.
- Toolbar mousedown handlers must call preventDefault before running the command, so the browser's text selection inside the editable area is never lost when a toolbar button is clicked.
- Implement live active-state highlighting: on every keyup, mouseup, and selectionchange event (while the editor is focused), check queryCommandState for each formatting command and toggle an active visual class on the matching toolbar button, so the toolbar always reflects the formatting under the current cursor position.
- Implement link insertion that preserves the user's text selection across a UI interruption: when the link toolbar button is clicked, clone the current selection range into a saved variable before showing a URL input field (which will steal focus), then on confirming the URL, restore the saved range onto the live selection before calling the createLink command — and afterward force every newly created link without a target attribute to open in a new tab with rel-based security attributes.
- Since a contenteditable element does not support the native placeholder attribute, implement a fake placeholder using an absolutely positioned overlay div that hides itself whenever the editor has any text content or any HTML tags inside it (not just when textContent is non-empty), so headings or lists with no visible text still correctly hide the placeholder.
- Add a live character counter driven by textContent length on the input event, and a button that dumps the editor's current innerHTML into a read-only, auto-selecting textarea for copying.

Step by step

How to Use

  1. 1
    Click in the editor area and start typingThe placeholder text disappears on first keystroke. Type your content — the editor behaves like a normal text area but produces rich HTML. The character count at the bottom updates live as you type.
  2. 2
    Select text and click toolbar buttons to formatHighlight any word or phrase, then click Bold, Italic, Underline, or Strikethrough in the toolbar. The button appears pressed (active state) while the cursor is inside formatted text. Click H1 or H2 to convert the current paragraph to a heading block.
  3. 3
    Insert links with the link buttonSelect the text you want to link, then click the link (chain) button in the toolbar. A small input row appears — paste or type the URL and press Insert or Enter. The selected text becomes a clickable link that opens in a new tab. Press Cancel to dismiss without inserting.
  4. 4
    Use list and alignment buttonsClick the ordered list button to create a numbered list, or the unordered list button for bullet points. Use the three alignment buttons (left, center, right) to change paragraph alignment. These work on the current block the cursor is in — no selection needed.
  5. 5
    Undo and redo changesClick the Undo button to reverse the last change, or Redo to reapply it. These call the browser's native undo stack which tracks every execCommand and keystroke. Standard keyboard shortcuts Ctrl+Z and Ctrl+Y also work inside the editor without any extra code.
  6. 6
    Click Get HTML to copy the editor outputPress the "Get HTML" button to reveal a read-only textarea containing the raw HTML from the editor. The textarea auto-selects the content so you can press Ctrl+C to copy it. Use this HTML to save to a database, pass to a backend API, or render in another part of your page.

Real-world uses

Common Use Cases

Comment and feedback forms with basic formatting
Replace plain textareas in comment boxes, review forms, and feedback widgets with this editor. Users can bold key phrases, add bullet points, or include links — making responses clearer and more useful. The HTML output can be sanitised server-side and stored in a text column.
Note-taking and document editor in web apps
Embed this editor in note-taking tools, internal wikis, or CMS admin panels. The Get HTML button lets developers inspect the output during prototyping. Connect editor.innerHTML to an autosave function that posts to your API on a debounced input event for live note saving.
Email composer and newsletter content editor
Use as an email body composer inside marketing or CRM tools. Bold subject lines, add hyperlinks to CTAs, and structure content with H2 subheadings. The HTML output can be dropped directly into an email template or passed to an API like SendGrid as the html_content field.
Prototype WYSIWYG interfaces without a library
When designing or pitching a content editor feature, this snippet gives you a working prototype in minutes without committing to a library like TipTap, Quill, or ProseMirror. Copy the HTML/CSS/JS into a Codepen or Figma prototype iframe to demonstrate the interaction to stakeholders.
Learn contenteditable, execCommand, and Selection APIs
This snippet teaches the three browser APIs that power all WYSIWYG editors: contenteditable for editable regions, document.execCommand() for formatting commands, and window.getSelection() with Range for precise cursor and selection manipulation. Understanding these is essential before moving to advanced editor frameworks.
Starter for a custom editor built without a framework
Use this as the foundation for a custom editor tailored to your needs. Add font-size and text-color dropdowns with execCommand("fontSize") and execCommand("foreColor"). Add an image insert button with execCommand("insertImage"). Replace execCommand with Selection.getRangeAt() insertions for custom block types like code blocks or callouts.

Got questions?

Frequently Asked Questions

execCommand() is marked as "deprecated" in MDN documentation, but browsers continue to support it and have no announced removal timeline — it remains the only reliable cross-browser way to implement WYSIWYG editing without a full framework. Modern editors like Quill and older versions of TipTap use it internally. For production apps requiring advanced formatting (tables, embeds, collaborative editing), a library like TipTap (ProseMirror-based) or Lexical is recommended. For simple formatting — bold, italic, lists, links, headings — execCommand() works reliably in all major browsers.

Read editor.innerHTML for the full rich HTML, or editor.textContent for plain text only. To autosave, add a debounced input event listener: let timer; editor.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => { fetch("/api/save", { method: "POST", body: JSON.stringify({ html: editor.innerHTML }), headers: { "Content-Type": "application/json" } }); }, 800); }). Always sanitise the HTML server-side before storing or rendering it — use a library like DOMPurify on the client or a server-side sanitiser to strip script tags and event attributes.

For font size, add a select element to the toolbar: <select id="font-size"><option value="1">Small</option><option value="3">Normal</option><option value="5">Large</option></select> and handle its change event with exec("fontSize", e.target.value). For text colour, add <input type="color" id="text-color"> and handle its input event with exec("foreColor", e.target.value). Both use execCommand() with a value argument. Add the same toolbar button active-state logic by calling document.queryCommandValue("fontSize") and document.queryCommandValue("foreColor") in updateToolbar() to keep the controls in sync with the cursor position.

Never inject editor.innerHTML directly into a page without sanitisation. On the client, run the saved HTML through DOMPurify.sanitize(html) before rendering — this strips script tags, onerror attributes, and javascript: href values. Install DOMPurify via npm or load it from a CDN. On the server (Node.js), use the sanitize-html package with a strict allowedTags list covering only the tags this editor produces: b, i, u, strike, h1, h2, ul, ol, li, a, p, br, div, span. Set allowedAttributes to { a: ["href", "target", "rel"] } and strip all other attributes.