Was This Helpful
A "Was this helpful?" row: pick yes or no, a short follow-up opens, then it settles into a thank-you.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/helpful-feedback.json'use client'
import { useState } from 'react'
import './helpful-feedback.css'
export interface HelpfulFeedbackProps {
question?: string
reasons?: { yes: string[]; no: string[] }
onSubmit?: (r: { helpful: boolean; reason?: string }) => void
icons?: boolean
fill?: 'dark' | 'light'
className?: string
}
const DEF = { yes: ['Clear', 'Solved it', 'Good examples'], no: ['Confusing', 'Out of date', 'Missing info'] }
const Thumb = ({ down }: { down?: boolean }) => (
<svg viewBox="0 0 16 16" aria-hidden style={down ? { transform: 'scaleY(-1)' } : undefined}>
<path d="M5 7v6.5H2.5V7H5Zm0 0 2.6-4.6c.9 0 1.6.8 1.4 1.7L8.6 6H12a1.5 1.5 0 0 1 1.5 1.8l-.9 4.5a1.5 1.5 0 0 1-1.5 1.2H5" />
</svg>
)
/** Yes / No feedback with a quick-reason follow-up. */
export function HelpfulFeedback({ question = 'Was this helpful?', reasons = DEF, onSubmit, icons = true, fill = 'dark', className }: HelpfulFeedbackProps) {
const [ans, setAns] = useState<boolean | null>(null)
const [reason, setReason] = useState<string>()
const [sent, setSent] = useState(false)
const cls = ['sd-hf', className].filter(Boolean).join(' ')
if (sent)
return (
<div className={cls} data-fill={fill}>
<div className="sd-hf__thanks" role="status"><svg viewBox="0 0 16 16" aria-hidden><path d="m3.5 8.5 3 3 6-7" /></svg>Thanks, that helps us improve.</div>
</div>
)
return (
<div className={cls} data-fill={fill}>
<div className="sd-hf__top">
<span className="sd-hf__q">{question}</span>
<div className="sd-hf__btns">
{[true, false].map((y) => (
<button key={String(y)} type="button" className="sd-hf__b" aria-pressed={ans === y} onClick={() => { setAns(ans === y ? null : y); setReason(undefined) }}>
{icons && <Thumb down={!y} />}{y ? 'Yes' : 'No'}
</button>
))}
</div>
</div>
<div className="sd-hf__more" data-open={ans !== null || undefined}>
<div>
<div className="sd-hf__chips" role="radiogroup" aria-label="Reason">
{(ans === false ? reasons.no : reasons.yes).map((r) => (
<button key={r} type="button" role="radio" aria-checked={reason === r} tabIndex={ans === null ? -1 : 0} className="sd-hf__chip" onClick={() => setReason(r)}>{r}</button>
))}
</div>
<button type="button" className="sd-hf__send" tabIndex={ans === null ? -1 : 0} onClick={() => { setSent(true); onSubmit?.({ helpful: Boolean(ans), reason }) }}>Send feedback</button>
</div>
</div>
</div>
)
}.sd-hf { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-hf[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-hf button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-hf :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-hf .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-hf { width: min(100%, 320px); padding: 14px; border-radius: 18px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); }
.sd-hf__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.sd-hf__q { font-size: 13.5px; font-weight: 500; }
.sd-hf__btns { display: flex; gap: 6px; }
.sd-hf__b { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px; border-radius: 999px; font-size: 13px; box-shadow: inset 0 0 0 1px var(--line); transition: background-color 200ms, color 200ms, transform 150ms; }
.sd-hf__b:hover { background: var(--soft); }
.sd-hf__b:active { transform: scale(0.94); }
.sd-hf__b[aria-pressed='true'] { background: var(--strong); color: var(--on); box-shadow: none; }
.sd-hf__b svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linejoin: round; }
.sd-hf__b[aria-pressed='true'] svg { animation: sd-hf-pop 380ms cubic-bezier(0.3, 1.8, 0.4, 1); }
@keyframes sd-hf-pop { 40% { transform: scale(1.3) rotate(-8deg); } }
.sd-hf__more { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 300ms ease; }
.sd-hf__more[data-open] { grid-template-rows: 1fr; }
.sd-hf__more > div { overflow: hidden; }
.sd-hf__chips { display: flex; flex-wrap: wrap; gap: 6px; padding-top: 12px; }
.sd-hf__chip { height: 28px; padding: 0 11px; border-radius: 999px; font-size: 12.5px; background: var(--soft); color: var(--mute); transition: color 150ms, box-shadow 150ms; }
.sd-hf__chip[aria-checked='true'] { color: var(--fg); box-shadow: inset 0 0 0 1px var(--fg); }
.sd-hf__send { margin-top: 10px; width: 100%; height: 34px; border-radius: 10px; background: var(--strong) !important; color: var(--on) !important; font-weight: 600; font-size: 13px; }
.sd-hf__thanks { display: flex; align-items: center; gap: 8px; font-size: 13.5px; animation: sd-hf-in 360ms ease; }
.sd-hf__thanks svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
@keyframes sd-hf-in { from { opacity: 0; transform: translateY(4px); } }
@media (prefers-reduced-motion: reduce) { .sd-hf *, .sd-hf *::before, .sd-hf *::after { animation: none !important; transition: none !important; } }Overview
Two quiet buttons under a doc or answer. Choosing one fills it and slides open a one-line follow-up with quick reasons, so people can add context with a tap instead of typing.
Sending collapses everything into a thank-you line. The whole thing stays one row tall until someone engages.
When to use
- End of docs pages, help articles, AI answers.
- Anywhere you want a cheap quality signal.
When not to use
- Detailed surveys: use a form.
- Ratings that need a scale.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/helpful-feedback.json$
npx shadcn@latest add https://sundaeui.dev/r/helpful-feedback.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/helpful-feedback.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/helpful-feedback.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy helpful-feedback.tsx and helpful-feedback.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { HelpfulFeedback } from '@/components/sundae/helpful-feedback'
<HelpfulFeedback />Props
| Prop | Type | Default |
|---|---|---|
questionPrompt text. | string | 'Was this helpful?' |
reasonsQuick reasons per answer. | { yes: string[]; no: string[] } | — |
onSubmitCalled on send. | (r: { helpful: boolean; reason?: string }) => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Yes / No are toggle buttons with aria-pressed.
- Reasons are a radiogroup; the thank-you is announced politely.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-hf; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Was This Helpful free?
- Yes. Was This Helpful is free for personal and commercial projects, with full source on this page.
- How do I install Was This Helpful?
- Run npx shadcn@latest add https://sundaeui.dev/r/helpful-feedback.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Was This Helpful support keyboard and screen readers?
- Yes / No are toggle buttons with aria-pressed. Reasons are a radiogroup; the thank-you is announced politely.
- Does Was This Helpful respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →Rating Scale
Forms · RefinedPoll Builder
Forms · RefinedLikert Matrix
Forms · RefinedBug Report
Forms · RefinedOTP Field
Forms · Refined- Pro
Drop Zone
Forms · Refined