Feedback Banner
betaA banner that asks one question with a few answer buttons, says thanks after an answer, and can be dismissed.
Preview
How is the new editor?
One click. It shapes what we fix next.
after an answer
Thanks for your feedback
We read every answer, and we will keep improving the editor.
default answers, no icon, not dismissible
Did this answer your question?
three answers, tone neutral
How was setting up your account?
narrow container
How is the new editor?
One click. It shapes what we fix next.
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 block. It also adds every component it uses. Its own files go in your
componentsfolder, and they are yours to edit.npx shadcn@latest add @txbrendan/feedback-banner
Use it for
- Asking how a new feature is going, right where people use it.
- A yes or no question at the end of a help article: "Did this answer your question?".
- Ask one short question with two or three answers. Send the answer in onValueChange.
- Save the answer or the dismissal, and stop showing the banner to that person.
- Write a question that the default Yes and No answer, or pass options with your own labels.
Avoid it for
- Long surveys or free-text answers. Link to a form instead.
- Errors, warnings, or news. Use Alert for a whole page, or Notice under the thing it is about.
- Asking before people have used the thing you ask about.
- Showing it on every visit. Ask once, then respect the answer or the dismissal.
Props
- title
The question, like "Is the new editor working for you?". It also names the answer buttons. Required.
ReactNode
- description
One short line under the question: why you ask, or how long it takes.
ReactNode
- options
The answers, one button each, all the same weight. Each value must be unique.
{ value: string; label: ReactNode; icon?: ReactNode }[] · default [{ value: "yes", label: "Yes" }, { value: "no", label: "No" }]
- onValueChange
Called with the option's value when people pick one. Send it to your feedback store here.
(value: string) => void
- value
The picked option's value, when you control it. null shows the question.
string | null
- defaultValue
The picked option's value to start with. Set it to start on the thank-you message.
string | null · default null
- icon
An icon element, like <MessageCircle />. It sits in a round tile and turns into a check after an answer.
ReactNode
- tone
Color of the icon tile.
"neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"
- thanksTitle
Shown in place of the question after an answer.
ReactNode · default "Thanks for your feedback"
- thanksDescription
One line under thanksTitle, like what happens next.
ReactNode
- dismissible
Shows the dismiss button.
boolean · default true
- dismissLabel
The dismiss button's name for screen readers.
string · default "Dismiss"
- onDismiss
Called when people dismiss the banner, after it hides. Save this, so you do not ask again.
() => void
- className
Extra classes on the outer div, which holds the container query.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| title | ReactNode | - | The question, like "Is the new editor working for you?". It also names the answer buttons. Required. |
| description | ReactNode | - | One short line under the question: why you ask, or how long it takes. |
| options | { value: string; label: ReactNode; icon?: ReactNode }[] | [{ value: "yes", label: "Yes" }, { value: "no", label: "No" }] | The answers, one button each, all the same weight. Each value must be unique. |
| onValueChange | (value: string) => void | - | Called with the option's value when people pick one. Send it to your feedback store here. |
| value | string | null | - | The picked option's value, when you control it. null shows the question. |
| defaultValue | string | null | null | The picked option's value to start with. Set it to start on the thank-you message. |
| icon | ReactNode | - | An icon element, like <MessageCircle />. It sits in a round tile and turns into a check after an answer. |
| tone | "neutral" | "brand" | "danger" | "success" | "warning" | "neutral" | Color of the icon tile. |
| thanksTitle | ReactNode | "Thanks for your feedback" | Shown in place of the question after an answer. |
| thanksDescription | ReactNode | - | One line under thanksTitle, like what happens next. |
| dismissible | boolean | true | Shows the dismiss button. |
| dismissLabel | string | "Dismiss" | The dismiss button's name for screen readers. |
| onDismiss | () => void | - | Called when people dismiss the banner, after it hides. Save this, so you do not ask again. |
| className | string | - | Extra classes on the outer div, which holds the container query. |
Accessibility
- The answer buttons sit in a group named by the question, so each one reads with the question.
- After an answer, the thank-you message takes the place of the question, and focus moves to it, so screen readers read it.
- The icon tile is hidden from screen readers. Write a question that makes sense without it.
- The dismiss button is named by dismissLabel and described by the question or the thanks. Dismissing removes the banner, so focus falls back to the page. If that matters, move focus in onDismiss.
Examples
Ask about a new feature
<FeedbackBanner
icon={<MessageCircle />}
tone="brand"
title="How is the new editor?"
description="One click. It shapes what we fix next."
options={[
{ value: "good", label: "Love it", icon: <ThumbsUp /> },
{ value: "bad", label: "Needs work", icon: <ThumbsDown /> },
]}
onValueChange={(answer) => sendFeedback("editor", answer)}
onDismiss={() => saveSetting("editorFeedbackHidden", true)}
/>Yes or no, at the end of an article
<FeedbackBanner
title="Did this answer your question?"
thanksDescription="We read every answer."
onValueChange={(answer) => sendFeedback(articleId, answer)}
dismissible={false}
/>