Rich text editor

WYSIWYG editor powered by TipTap, styled to match Squad UI.

Headings, lists, inline formatting, links, undo/redo.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/rich-text-editor.json
Usage
Use as a controlled (`value` + `onChange`) or uncontrolled (`defaultValue`) editor.
TSImport
import { RichTextEditor } from "@/components/ui/rich-text-editor";
TSExample
import * as React from "react";
import { RichTextEditor } from "@/components/ui/rich-text-editor";

export function Example() {
  const [value, setValue] = React.useState("<p>Hello <strong>Squad</strong>.</p>");
  return (
    <RichTextEditor
      value={value}
      onChange={setValue}
      placeholder="Start writing…"
    />
  );
}
Composition
Anatomy of the RichTextEditor.
RichTextEditor (TipTap-backed)
├── Toolbar (top or bottom)
│   ├── Headings (H1/H2/H3)
│   ├── Bold / Italic / Underline / Strike / Code
│   ├── Lists (bullet / ordered)
│   ├── Blockquote
│   ├── Link (popover)
│   ├── Undo / Redo
│   ├── Spell-check popover (optional, retext stack lazy-loaded)
│   └── Dictation mic (optional, Web Speech API)
├── EditorContent (contenteditable)
└── Footer (optional character count)
Default toolbar
Headings, lists, inline formatting, links, undo/redo.

Headings, lists, inline formatting, links, undo/redo.

Inline markdown (ClickUp-style description)
`RichTextEditorInline` — a flat, borderless variant that takes markdown in and out via `@tiptap/markdown`. There is no fixed toolbar; highlighting text raises a floating BubbleMenu with formatting tools, just like a ClickUp task description. Pass `collapsible` to clip long content to a fixed height with a fade and an Expand/Collapse toggle, like long code blocks. Type `/` at the start of a word for a slash-command palette (headings, lists, quote, code block, divider) — entries gated off via `disabledFeatures` are hidden from it too.

Flat ClickUp-style description — markdown in/out, highlight text for a floating toolbar.

Character limit
Cap content length using the character-count extension.

Enforced via @tiptap/extension-character-count.

Toolbar on bottom
Pin the toolbar below the editable area.
Error state
Pass a string to `error` to show the red outline and an inline message below the editor.
With caption
Pass `caption` to render helper copy under the label. Error wins when both are present.

Markdown-style formatting is supported.

With dictation
Pass `dictation` to mount a Web Speech API mic at the end of the toolbar. Chrome/Edge only — the button hides itself in unsupported browsers (Safari, Firefox, mobile WebKit).

Web Speech API mic in the toolbar — Chrome/Edge only; hides itself elsewhere.

With spell check
Pass `spellCheck` to lazily load the retext stack (spelling, repeated words, a/an, redundant acronyms, contractions, smart quotes, double spaces) and surface issues in a popover triggered from a toolbar button. The button shows a red badge with the issue count.

Lazy-loaded retext stack underlines misspellings, repeated words, a/an mistakes, and more. Hover an underline for the message.

Disabled
Read-only editor for display purposes.
Rich markdown
`<Composer richText>` writes rich markdown — CommonMark and GFM plus callouts, buttons, highlight, colour and underline — and `<RichMarkdown>` draws it back as React elements, never HTML. One service (`lib/rich-markdown*.ts`) defines the syntax for both, so what the composer saves is exactly what the view shows.

What changed this round

We took your notes from the call and tightened the layout. Headlines are bigger, and the key date is highlighted so nobody misses it.

Next steps
  1. Review the three concepts

    • Leave a pin on anything that feels off

    • Approve the one you love

  2. Confirm the go-live date

Open the project

Rich markdown syntax
What rich markdown adds to CommonMark and GFM.
==highlighted==
++underlined++
[color value="green"]coloured[/color]      gray red orange yellow green blue purple pink
[button href="https://…"]Label[/button]
:::callout {type="warning"}                 default brand info success warning destructive
Any blocks — **marks**, lists, headings.
:::
> [!NOTE]                                   GitHub alerts read as callouts too
API Reference
Props exposed by the RichTextEditor and RichMarkdown.

RichTextEditor

PropTypeDefaultDescription
valuestring—Controlled HTML value. Pair with `onChange`.
defaultValuestring—Uncontrolled initial HTML.
onChange(html: string) => void—Fires whenever the editor content changes.
placeholderstring—Empty-state placeholder text.
disabledbooleanfalseRender as read-only.
toolbarPosition"top" | "bottom""top"Pin the toolbar above or below the editable region.
maxLengthnumber—Cap content via `@tiptap/extension-character-count`; shows a counter footer.
errorboolean | stringfalseError state. `true` applies the red outline only; a string also renders the inline message (`text-xs mt-1.5 text-red-500`) below the editor.
captionReactNode—Helper copy shown below the editor via FieldHint. Error wins when both are present.
dictationbooleanfalseMount a Web Speech API mic at the end of the toolbar. Streams transcribed phrases into the editor at the current cursor. Auto-hides in unsupported browsers (Safari, Firefox, mobile WebKit).
spellCheckbooleanfalseRun a lazy-loaded retext stack (retext-spell + dictionary-en, retext-repeated-words, retext-indefinite-article, retext-redundant-acronyms, retext-contractions, retext-quotes, retext-sentence-spacing) on the editor's text and surface issues in a toolbar popover with a count badge. Each entry shows the offending word + the retext message in a DescriptionList.
disabledFeaturesRichTextFeature[]—Hide toolbar features, e.g. `["h1", "blockquote", "indent"]`. Features: bold, italic, underline, strike, code, h1, h2, h3, bulletList, orderedList, blockquote, link.
minHeightnumber—Minimum editable area height in pixels.
classNamestring—Override or extend the wrapper classes.

RichTextEditorInline

PropTypeDefaultDescription
valuestring—Controlled markdown value. Pair with `onChange`.
defaultValuestring—Uncontrolled initial markdown.
onChange(markdown: string) => void—Fires with the content serialized back to markdown via `editor.getMarkdown()`.
placeholderstring"Add a description…"Empty-state placeholder text.
disabledbooleanfalseRender as read-only.
minHeightnumber80Minimum editable area height in pixels.
collapsiblebooleanfalseCollapse the content to `collapsedHeight` with a fade-out gradient and an Expand overlay button, like collapsible code blocks. The toggle only appears when the content overflows.
defaultCollapsedbooleantrueInitial collapsed state for `collapsible`.
collapsedHeightnumber160Collapsed max-height in pixels.
collapsedLinesnumber—Collapsed height expressed in text lines (24px each). Takes precedence over `collapsedHeight`.
autoHeightbooleanfalseFit the collapsed viewport to the space remaining below the editor in the browser viewport (re-measured on resize). Takes precedence over `collapsedLines`/`collapsedHeight`.
expandOnFocusbooleantrueAuto-expand when the editor gains focus (click/typing), like ClickUp descriptions.
disabledFeaturesRichTextFeature[]—Hide toolbar features, e.g. `["h1", "blockquote", "indent"]`. Features: bold, italic, underline, strike, code, h1, h2, h3, bulletList, orderedList, blockquote, link, indent (indent is inline-only).
classNamestring—Override or extend the wrapper classes.

RichMarkdown

PropTypeDefaultDescription
sourcestring—The markdown to draw — what the Composer wrote.
documentboolean—Templated prose: fill `{{token}}` from `tokens`. Off for somebody's own words.
tokensRecord<string, string | null | undefined>—`{{name}}` → the value to draw.
resolveMention(handle: string) => { consumed: string; label: string } | null—Draw `@someone` as a chip. Absent, mentions are not looked for.
renderAttachment(id: string, name: string) => ReactNode—Draw a file placed in the text as `![name](att:ID)`. Absent, its name is drawn.
variant"comment" | "page""comment"`page` draws a document: readable body size, real heading levels with anchors (`richMarkdownOutline(source)` lists them for an "On this page"), and the documentation blocks — steps, tabs, accordions, cards, file trees, titled code blocks — at full size.
renderCode({ lang, text, highlight }) => ReactNode—Draw a code block yourself (syntax highlighting). Null keeps the plain block, its title bar and highlighted lines.
classNamestring—On the wrapper.