Mohamed Uvaish

Rich Text Editor

Text 44

About

Tiptap editor with a one-Tab-stop toolbar (arrow keys inside it), headings, lists, quotes, code, an inline link field instead of a browser prompt, and a character counter near the limit. Outputs HTML. Part of the Layro System: layro.vercel.app.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Rich Text Editor to your project and fits it to your theme.

Code

How Rich Text Editor is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import * as React from "react";
import { RichTextEditor } from "@/components/ui/rich-text-editor-toolbar";
import { Check, FileText, Share2 } from "lucide-react";

/* A proposal being written in the workspace. Select text and press Bold, or
   ⌘B; "Add link" opens an inline address field, not a browser prompt. The
   toolbar is one Tab stop: arrow keys move inside it. The counter shows up
   as the draft nears its 600-character limit. */

const START =
  "<h2>Scope of work</h2>" +
  "<p>Northwind will redesign the <strong>Vela Labs</strong> client portal over six weeks, starting <em>6 October</em>. The full brief is in the <a href=\"https://northwind.studio/briefs/vela\">shared folder</a>.</p>" +
  "<h3>Deliverables</h3>" +
  "<ol><li>Research and a journey map for the three core tasks</li><li>A design system in Figma, with tokens exported as <code>theme.json</code></li><li>Production-ready screens for signing, billing and members</li></ol>" +
  "<blockquote>We start once the deposit invoice (INV-2026-0927) is paid.</blockquote>" +
  "<p>Weekly reviews happen on Thursdays. Anything outside this scope is quoted separately.</p>";

const PEOPLE = [
  { initials: "MC", name: "Maya Chen" },
  { initials: "DW", name: "Daniel Wilson" },
  { initials: "OM", name: "Olivia Martinez" },
];

export default function RichTextEditorDemo() {
  const [chars, setChars] = React.useState(0);
  const [saved, setSaved] = React.useState(true);
  const timer = React.useRef<ReturnType<typeof setTimeout>>();

  return (
    <div className="flex min-h-screen w-full items-center justify-center bg-background p-6">
      <div className="w-full max-w-[780px] min-w-0 overflow-hidden rounded-[18px] [corner-shape:squircle] bg-card shadow-sm ring-1 ring-border">
        <header className="flex items-center gap-3 border-b border-border px-5 py-3">
          <FileText aria-hidden className="size-4 text-muted-foreground" />
          <p className="min-w-0 truncate text-[12.5px] text-muted-foreground">
            Proposals <span aria-hidden>/</span> <span className="text-foreground">Vela Labs, portal redesign</span>
          </p>
          <span className="ml-auto flex items-center gap-1 text-[12px] text-muted-foreground" aria-live="polite">
            {saved ? <><Check aria-hidden className="size-3.5" /> Saved</> : "Saving…"}
          </span>
          <span className="hidden -space-x-1.5 sm:flex" aria-label={`Editing: ${PEOPLE.map((p) => p.name).join(", ")}`}>
            {PEOPLE.map((p) => (
              <span key={p.initials} title={p.name} className="grid size-7 place-items-center rounded-full bg-muted text-[10.5px] font-semibold ring-2 ring-card">
                {p.initials}
              </span>
            ))}
          </span>
          <button
            type="button"
            className="inline-flex h-8 items-center gap-1.5 rounded-[10px] [corner-shape:squircle] bg-primary px-3 text-[12.5px] font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
          >
            <Share2 aria-hidden className="size-3.5" /> Share
          </button>
        </header>

        <div className="p-5">
          <RichTextEditor
            label="Proposal"
            maxLength={600}
            minHeight={320}
            defaultValue={START}
            onChange={(_, editor) => {
              setChars(editor.storage.characterCount.characters());
              setSaved(false);
              clearTimeout(timer.current);
              timer.current = setTimeout(() => setSaved(true), 900);
            }}
          />
          <p className="mt-2.5 text-[12px] text-muted-foreground">
            Last edited by Maya Chen · {chars ? `${chars} characters` : "just now"}
          </p>
        </div>
      </div>
    </div>
  );
}

Dependencies

More by Mohamed Uvaish

View all
  • Signature Pad

    Signature Pad

    420
  • Folder Card

    Folder Card

    336
  • Share Dialog

    Share Dialog

    308
  • Kanban Board

    Kanban Board

    249
  • Notification Panel

    Notification Panel

    102
  • Charts & KPI Cards

    Charts & KPI Cards

    86
  • Command Menu

    Command Menu

    34
  • Data Table

    Data Table

    32

More in Text

View all
  • Vertical Cut Reveal Characters

    Vertical Cut Reveal Characters

    103
  • Rainbow Text Effect

    Rainbow Text Effect

    47
  • Motion Blur Text

    Motion Blur Text

    36
  • Text Loop

    Text Loop

    310
  • Text Reveal

    Text Reveal

    27
  • Apple Hello Effect

    Apple Hello Effect

    610
  • Typography

    Typography

    4
  • Animated Text

    Animated Text

    8

Explore more

Part of Layro System — browse the full library on 21st.dev.