shadcn.io is not affiliated with official shadcn/ui
Reaction Picker Dialog Block for React
A community chat with a reaction tray shown open under one message: six quick reactions, a More button that opens twelve more in one grid, arrow keys across it, and pills with counts on the message.
Installation
Overview
Let people answer a message with one tap, with this reaction tray for React with Base UI. It opens from the #general channel of Chirpbarn, a community chat: the page's header holds an outline Add reaction Button, and the thread, two messages and a composer, is drawn in a hairline frame with a plus mark on each corner, with the tray under the message it belongs to and the message's pills, a thumbs up with 3, a heart with 1, a party popper with 1, above it. The tray is a small Popover of reaction icons, never emoji: a quick row of six, Thumbs up, Heart, Laugh, Party popper, Eyes and Flame, and a More button that opens twelve more under it in one flat grid, six across and three down, with no search, tabs or skin tones. One line under the grid names the reaction you point at or focus. Choosing one puts it on the message as a pill with its count and closes the tray; choosing it again takes it back. It isn't modal, so the chat stays readable. For preview the tray is drawn open in place when the page loads; the header button opens the real Popover, with the same grid and the same state. For chats, comment threads, feeds and any message that takes a reaction.
FAQ
Why is the tray already open, and what do I keep?
So you can see it without a click. On load the tray is a copy drawn in place under the message: a native non-modal dialog element with no scrim, so nothing is trapped or focused, and the page around the preview keeps working. The Add reaction button opens the real tray as a non-modal Popover that hangs from the button (Escape closes it and focus goes back to the button), and the copy stays closed until a reload. Both show the same Tray component and share the block's state. In your app keep the Popover, anchor it to the message's own React button, and drop the copy: the "in-place" state and the dialog element beside the message.
How do the arrow keys work?
The reactions are one grid with one tab stop: Tab enters it and leaves it, and the arrows move across it, Left and Right along a row (reversed in a right-to-left layout), Up and Down between rows when More is open. Home and End go to the ends of the row, and with Control to the first and last reaction. The six in the quick row are always reachable; the other twelve are reachable only while More is open, and when it is closed they are unseen and skipped. Every reaction is a button named for what it is, with its pressed state, so a screen reader hears "Heart, pressed" on yours.
What does choosing do, and how are the counts made?
It toggles the reaction on the message, says "Added Heart" in a polite message and closes the tray, returning focus to the header button. The pill under the message is the others' count (typed) plus one for you, and yours is solid. Choosing it again, in a reopened tray where it shows as pressed, removes your one and says "Removed Heart". Reactions that nobody used have no pill, so the row only holds what exists, and its height is fixed so the thread never moves.
Why a line of text instead of tooltips?
Eighteen tooltips would each need a delay, a hover and a place, and none of them work on touch. One line under the grid shows the name of the reaction under the pointer or the focus, and on yours it adds "press to take it back". The line is for sighted people; the buttons carry their own names for everyone else.
Does it send the reaction, and how do I wire it up?
No. The reaction lives in this tab only, and the message in the drawn thread shows it. In your app send the toggle from pick, take the counts from the response (the server owns them) and keep the same pills. The icons are lucide icons in the current text colour; to use emoji or a custom set, replace the typed REACTIONS list and keep the buttons' names.
Is it accessible from the keyboard?
Yes. The tray is named Reactions, focus starts on its first reaction, and Escape closes it and returns focus to the header button. The trigger is passed to PopoverTrigger through render, and the popover is controlled with open and onOpenChange. More is a button with aria-expanded and aria-controls, and one polite status message reports what changed. The copy shown on load moves no focus; Escape inside it closes it and puts focus on the header button.
Related
- Pro
A team chat's page with its emoji picker shown open: a frequently used row, a search over 96 emoji, a skin tone select, six category tabs with an arrow-key grid, and a foot line that names the emoji in focus, adding each one to the message box behind it.
- Pro
Emoji Reactions Reviews
Votes and repliesFour skateboard reviews that each take six reactions from a popover of toggles with counts, show their top three beside a total, and feed a ranking of the product's reactions beside them behind a divider.
- Pro
Quick Reply Dialog
Pickers and searchA helpdesk ticket with its saved-replies dialog shown open: a searchable list of eight replies, a preview filled from the ticket, a flag and a field for each variable it cannot fill, an SMS character count and an Insert reply button that writes into the composer.
- Pro
App Rating Dialog
Drawers and popoversA water tracker's Today screen with its rating drawer shown open over a drawn phone: five stars with a word each, then a branch, where four or five stars offers the store and one to three asks what could be better, with topics and a note.
- Pro
Audio Player Dialog
Drawers and popoversAn ambient-sound library's page with its mini player shown open over it: a floating panel with the track, a seek slider and its time, Previous, Play and Next, and a volume slider with mute. It isn't modal, so the page stays usable, and nothing really plays.
- Pro
Cookie Consent Dialog
Drawers and popoversA recipe blog's first-visit cookie card shown open over its page: two sentences, then Reject all and Accept all as two equal outline buttons side by side, with no primary. It isn't modal, so the page stays readable, and the choice is written on the page's foot.