Editor
betaLong writing in a field's frame, with a glass toolbar over selected words and pasting that keeps only clean HTML.
Preview
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.
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.
Locked once the quarter closes.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. 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
| Prop | Type | Default | What it does |
|---|---|---|---|
| value | string | - | HTML. Controlled: the editor only reloads when this differs from what it last sent. |
| defaultValue | string | - | HTML to start with. |
| onChange | (html: string, info: { text: string; words: number }) => void | - | Clean HTML ("" when empty) and the words, on every change. |
| label | ReactNode | - | Names the writing area. |
| hint | ReactNode | - | A short note under the writing area. It describes it. |
| error | ReactNode | - | Replaces the hint while set, in red, and marks the writing area invalid. |
| placeholder | string | - | Shown while it is empty. Never sent. |
| rows | number | 6 | The least height, in lines. It grows with its words. |
| size | "sm" | "md" | "lg" | "md" | Padding and label size. The words stay 13/18 and headings 16/20. |
| disabled | boolean | false | Locks the words. |
| showCount | boolean | false | A word count under the right edge. |
| status | ReactNode | - | Ends the label's line, like a saving status. |
| ref | Ref<HTMLDivElement> | - | Reaches the writing area. |
| id | string | - | Goes on the writing area, so the label still points at it. aria-*, data-*, and handlers go there too. |
| className | string | - | Extra classes on the frame. Later classes win. |
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} />