Skip to content
txbrendan/ui

Editor

beta

Long writing in a field's frame, with a glass toolbar over selected words and pasting that keeps only clean HTML.

Preview

Newsletter · OctoberSaved 18:04

October news

The Design team shipped the new website on Tuesday, two weeks early. Thanks to everyone who tested it.

Coming up:

  • Demo day, Tuesday at 6
  • Team lunch, Friday the 30th

Read the full update on our site.

Select words to format them.

39 words

As readers will see it

October news

The Design team shipped the new website on Tuesday, two weeks early. Thanks to everyone who tested it.

Coming up:

  • Demo day, Tuesday at 6
  • Team lunch, Friday the 30th

Read the full update on our site.

Minutes
Report

Locked once the quarter closes.

Notice

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/editor

Use it for

  • Newsletters, minutes, reports, and announcements: writing with headings, lists, and links.
  • Save from onChange. It gets clean HTML, "" when empty, and the words.
  • Show saving with status, like a StatusText that says Saved 18:04. It ends the label's line.
  • Show the saved HTML anywhere with Prose, in the same look.

Avoid it for

  • A message to people. Use Composer.
  • A line or two of plain text. Use Textarea or Input.
  • Rich layouts, tables, or images. It keeps paragraphs, headings, lists, bold, italic, and links only.

Props

  • value

    HTML. Controlled: the editor only reloads when this differs from what it last sent.

    string

  • defaultValue

    HTML to start with.

    string

  • onChange

    Clean HTML ("" when empty) and the words, on every change.

    (html: string, info: { text: string; words: number }) => void

  • label

    Names the writing area.

    ReactNode

  • hint

    A short note under the writing area. It describes it.

    ReactNode

  • error

    Replaces the hint while set, in red, and marks the writing area invalid.

    ReactNode

  • placeholder

    Shown while it is empty. Never sent.

    string

  • rows

    The least height, in lines. It grows with its words.

    number · default 6

  • size

    Padding and label size. The words stay 13/18 and headings 16/20.

    "sm" | "md" | "lg" · default "md"

  • disabled

    Locks the words.

    boolean · default false

  • showCount

    A word count under the right edge.

    boolean · default false

  • status

    Ends the label's line, like a saving status.

    ReactNode

  • ref

    Reaches the writing area.

    Ref<HTMLDivElement>

  • id

    Goes on the writing area, so the label still points at it. aria-*, data-*, and handlers go there too.

    string

  • className

    Extra classes on the frame. Later classes win.

    string

Accessibility

  • The writing area is a role="textbox" with aria-multiline, named by the label and described by the hint or error.
  • Select words and a toolbar shows over them. Alt+F10 moves into it. It is one Tab stop, and ← → Home and End move along it.
  • Each button says its state with aria-pressed. Its tooltip teaches its key: ⌘B, ⌘I, ⌘K, or # - or 1. then Space at the start of a line.
  • Esc steps back: out of the link field, then from the toolbar to the selected words, then it hides the toolbar. In a Sheet, that Esc never closes the sheet.
  • A link asks for its address in the toolbar. Enter applies it, and empty removes it. Only web and email addresses are linked.
  • Under reduced motion the toolbar moves at once.

Examples

A newsletter that saves as you write

<Editor
  label="Newsletter · October"
  value={html}
  showCount
  status={<StatusText tone="success">Saved 18:04</StatusText>}
  onChange={(next) => save(next)}
/>

Showing it

<Prose html={newsletter.html} />