Response Button
betaAn availability or RSVP answer, like Going, Can't go, Maybe, or Online. Answers sit in a ResponseGroup named for the question, and work like native radios.
Preview
Tuesday workshop
19:00 · Room 2
Your answer: Not replied. Tab in, then use the arrow keys.
Small answers, inline in each person's row.
- Tom Hale
- Cal Nash
- Mark Clarke
sm
md
lg
chosen
disabled
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/response-button
Use it for
- Answering an event or an invite: two to four answers in a ResponseGroup named for the question, like 'Tuesday workshop'.
- Give each answer the tone of its meaning: success for Going, danger for Can't go, warning for Maybe, neutral for Online.
- md (32px) on event pages. sm (28px) inline in rows and status strips.
- Add an icon before the words, like a check for Going, so the answer reads without its color.
Avoid it for
- Answers that can be picked together. Use checkboxes.
- A long list of choices. Use RadioGroup or Select.
- A ResponseButton on its own, outside a ResponseGroup. It then has no arrow keys and no single Tab stop.
Props
- children
The answer, like 'Going', with an optional icon before it.
ReactNode
- tone
Color role of the answer. The chosen answer fills with it, and an open one hints it on hover.
"neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"
- size
28, 32, or 36px tall at the default density, scaled by density.
"sm" | "md" | "lg" · default "md"
- selected
This answer is the chosen one.
boolean · default false
- onClick
Runs on a press, and when the arrow keys move onto it. Choose the answer here.
() => void
- disabled
Blocks the answer. The arrow keys skip it.
boolean · default false
- ref
Reaches the button. id, aria-* and data-* land on it too.
Ref<HTMLButtonElement>
- className
Extra classes on the button. Later classes win.
string
- ResponseGroup
Component. The radio group for one question's answers, in a row with a 6px gap. Its name is required.
ComponentProps<"div"> & ({ "aria-label": string } | { "aria-labelledby": string })
| Prop | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | - | The answer, like 'Going', with an optional icon before it. |
| tone | "neutral" | "brand" | "danger" | "success" | "warning" | "neutral" | Color role of the answer. The chosen answer fills with it, and an open one hints it on hover. |
| size | "sm" | "md" | "lg" | "md" | 28, 32, or 36px tall at the default density, scaled by density. |
| selected | boolean | false | This answer is the chosen one. |
| onClick | () => void | - | Runs on a press, and when the arrow keys move onto it. Choose the answer here. |
| disabled | boolean | false | Blocks the answer. The arrow keys skip it. |
| ref | Ref<HTMLButtonElement> | - | Reaches the button. id, aria-* and data-* land on it too. |
| className | string | - | Extra classes on the button. Later classes win. |
| ResponseGroup | ComponentProps<"div"> & ({ "aria-label": string } | { "aria-labelledby": string }) | - | Component. The radio group for one question's answers, in a row with a 6px gap. Its name is required. |
Accessibility
- Each answer is a radio, named by its words, with aria-checked for the chosen one.
- It finds its group on the page, the closest role='radiogroup'. ResponseGroup is that group, named for the question.
- The group is one Tab stop: the chosen answer, or the first one when none is chosen.
- The arrow keys move and choose, and wrap. Home and End jump to the ends. Disabled answers are skipped. In right-to-left text, left and right swap.
- The chosen answer differs by fill as well as color, so it reads without color.
Examples
Answering an event
import { Check, CircleHelp, Video, X } from "lucide-react";
import { useState } from "react";
function Rsvp() {
const [answer, setAnswer] = useState<string>();
const answers = [
{ value: "going", label: "Going", tone: "success", icon: <Check /> },
{ value: "maybe", label: "Maybe", tone: "warning", icon: <CircleHelp /> },
{ value: "no", label: "Can't go", tone: "danger", icon: <X /> },
{ value: "online", label: "Online", tone: "neutral", icon: <Video /> },
] as const;
return (
<ResponseGroup aria-label="Tuesday workshop">
{answers.map(({ value, label, tone, icon }) => (
<ResponseButton key={value} tone={tone} selected={answer === value} onClick={() => setAnswer(value)}>
{icon}
{label}
</ResponseButton>
))}
</ResponseGroup>
);
}