Message Row
betaA message with quick actions. Pointing at it or moving focus into it springs in Reply, and Edit on your own, with R and E keys. A reply or an edit opens in place.
Preview
Team sync moves to Room 3 on Wednesday while Room 2 is repainted. Same time, same link.
Josh Tait Got it, thanks Anna.
Spare chargers are in the cupboard. Last one out locks up, please.
Can someone cover my desk on Wednesday? I'm on a late shift.
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/message-row
Use it for
- Messages in a chat, a team feed, or an activity list, stacked in a card like list rows.
- Set own on the viewer's messages, so they get Edit. Pass me, so a new reply shows their name and photo.
- Save what onAction hands you. The reply or edit already shows in place, and new props with it replace the local copy.
- Give peek the author's role, team, and next event, so their name opens a card.
Avoid it for
- A new message to a group. Use Composer, which says who it reaches.
- Rows that are not messages, like orders or files. Use a list row.
- Extra actions without a one-key shortcut. Each row's keys are how the keyboard reaches them fast.
Props
- author
Required. Who posted it. Initials show until the photo loads.
{ person: string; photo?: string } | string
- time
Required. When, like "18:02".
string
- text
Required. The message. Line breaks show.
string
- own
The viewer's own message: adds Edit, with E.
boolean · default false
- me
Who is replying, so a new reply shows their name and photo.
{ person: string; photo?: string } | string · default "You"
- context
Where it was posted, like "in Design". It follows the time.
string
- replies
Replies already there, each { author, text }.
MessageReply[]
- onAction
A reply was sent or the message was edited, and it shows in place. Save it here.
(action: "reply" | "edit", text: string) => void
- peek
The author's peek, like role, team, and next event. Without it, the name is plain.
Omit<PeekCardProps, "person" | "photo">
- extraActions
More quick actions after Reply and Edit, like Pin. Give each a one-key shortcut.
HoverAction[]
- divider
A 1px hairline under it, inset to its padding. The last row has none.
boolean · default true
- className
Extra classes on the row. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| author | { person: string; photo?: string } | string | - | Required. Who posted it. Initials show until the photo loads. |
| time | string | - | Required. When, like "18:02". |
| text | string | - | Required. The message. Line breaks show. |
| own | boolean | false | The viewer's own message: adds Edit, with E. |
| me | { person: string; photo?: string } | string | "You" | Who is replying, so a new reply shows their name and photo. |
| context | string | - | Where it was posted, like "in Design". It follows the time. |
| replies | MessageReply[] | - | Replies already there, each { author, text }. |
| onAction | (action: "reply" | "edit", text: string) => void | - | A reply was sent or the message was edited, and it shows in place. Save it here. |
| peek | Omit<PeekCardProps, "person" | "photo"> | - | The author's peek, like role, team, and next event. Without it, the name is plain. |
| extraActions | HoverAction[] | - | More quick actions after Reply and Edit, like Pin. Give each a one-key shortcut. |
| divider | boolean | true | A 1px hairline under it, inset to its padding. The last row has none. |
| className | string | - | Extra classes on the row. Later classes win. |
Accessibility
- Each message is an article, named by its author and time, and a Tab stop of its own.
- Focus on the message, or anywhere in it, shows its actions, the same as pointing at it. R replies and E edits while they show.
- The keys stay quiet while you type in a field.
- Reply puts the cursor in a field named Reply to {first name}. Enter sends, and Esc cancels without closing anything around it.
- When the reply or edit closes, focus comes back to the message, so the keyboard keeps its place.
- Replies are a list named Replies to {author}. The hairline is a border, so it stays in forced colors.
Examples
A plain message, with its time
<MessageRow author={{ person: "Anna Price" }} time="18:02" text="Team sync moves to Room 3 on Wednesday." />Your own, with replies saved
<MessageRow
author={me}
me={me}
own
time="18:10"
context="in Design"
text={message.text}
replies={message.replies}
onAction={(action, text) => (action === "edit" ? saveEdit(message.id, text) : saveReply(message.id, text))}
/>With a peek on the name
<MessageRow
author={{ person: "Sam Carter" }}
time="Mon 09:12"
text="Demo day is Tuesday at 6."
peek={{ subtitle: "Head of design · Design", facts: [["Next", "Design review, Tue 19:00"]] }}
/>