Read By
betaHow many have read a message, out of how many it went to, with a peek that reminds the rest.
Preview
Team sync moves to Room 2 on Thursday. Bring your laptop to share.
many
all read
nobody
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/read-by
Use it for
- Under a message, a notice, or a form people were asked to read: "Read by 241 of 280".
- Pass everyone it went to in people. Mark read: true, or the time, like "10:12", once they have.
- Its peek lists who has not read it yet, each with Remind, and Remind all for two or more.
- Send the reminders in onRemind. It runs once the 8-second undo window ends, never on the press.
- Once everyone has read it, it says "Read by all 23" as plain text, with no peek.
Avoid it for
- A count with no one behind it, like page views. Use plain text.
- Showing who has read it. The peek is for the ones who have not.
Props
- people
Required. Everyone it went to. read is true, or when they read it, once they have.
Array<{ person: string; photo?: string; meta?: string; read?: boolean | string }>
- onRemind
Reminds people who have not read it, once the undo window ends. Remind all leaves out anyone already reminded.
(people: { person: string; photo?: string }[]) => void
- open
Controlled. The peek shows while true. Hover, focus, and Esc only ask, through onOpenChange.
boolean
- defaultOpen
Starts with the peek open, without taking focus.
boolean · default false
- onOpenChange
Hears the peek open and close, or what it asks for when controlled.
(open: boolean) => void
- className
Extra classes on the text. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| people | Array<{ person: string; photo?: string; meta?: string; read?: boolean | string }> | - | Required. Everyone it went to. read is true, or when they read it, once they have. |
| onRemind | (people: { person: string; photo?: string }[]) => void | - | Reminds people who have not read it, once the undo window ends. Remind all leaves out anyone already reminded. |
| open | boolean | - | Controlled. The peek shows while true. Hover, focus, and Esc only ask, through onOpenChange. |
| defaultOpen | boolean | false | Starts with the peek open, without taking focus. |
| onOpenChange | (open: boolean) => void | - | Hears the peek open and close, or what it asks for when controlled. |
| className | string | - | Extra classes on the text. Later classes win. |
Accessibility
- With people still to read it, the text is a focusable button that opens a dialog. Its name is the count, like "Read by 241 of 280".
- The peek is named by its title, like "39 haven't read it yet". Enter moves into it, and Esc closes it and puts focus back.
- Each Remind says what it did in the page's one polite live region. Undo arms 1.2 seconds after a key press, and ⌘Z or Ctrl+Z takes back the newest reminder.
- Once everyone has read it, the text is plain, with no button and no Tab stop.
Examples
Under a message
<ReadBy
people={[
{ person: "Kate Clarke", read: "10:12" },
{ person: "Tom Hale", read: true },
{ person: "Ava Moss", meta: "Design team" },
{ person: "Rob Kerr", meta: "Support team" },
]}
onRemind={(people) => console.log("remind", people)}
/>