Skip to content
txbrendan/ui

Response Button

beta

An 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. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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 })

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>
  );
}