# Rating Feedback

> A "Was this helpful?" emoji rating widget with keyboard radiogroup, comment box and thank-you state.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/rating-feedback
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/rating-feedback.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/rating-feedback-demo.tsx"
"use client"

import { RatingFeedback } from "@/components/rating-feedback"

export default function RatingFeedbackDemo() {
  return (
    <div className="flex w-full flex-wrap items-start justify-center gap-6">
      <RatingFeedback />
      <RatingFeedback question="How was your onboarding experience?" />
    </div>
  )
}
```

## Source

```tsx title="components/rating-feedback.tsx"
"use client"

import * as React from "react"
import { Check } from "lucide-react"

import { cn } from "@/lib/utils"

const FACES = [
  { emoji: "😡", label: "Terrible" },
  { emoji: "🙁", label: "Bad" },
  { emoji: "😐", label: "Okay" },
  { emoji: "🙂", label: "Good" },
  { emoji: "😍", label: "Great" },
] as const

export interface RatingFeedbackProps extends Omit<React.ComponentProps<"div">, "onSubmit"> {
  question?: string
  /** Called when the user presses Send. `rating` is 1-5. */
  onSubmit?: (feedback: { rating: number; comment: string }) => void
}

export function RatingFeedback({
  question = "Was this helpful?",
  onSubmit,
  className,
  ...props
}: RatingFeedbackProps) {
  const [rating, setRating] = React.useState<number | null>(null)
  const [comment, setComment] = React.useState("")
  const [sent, setSent] = React.useState(false)
  const itemRefs = React.useRef<Array<HTMLButtonElement | null>>([])
  const groupId = React.useId()

  function select(index: number, focus = false) {
    setRating(index + 1)
    if (focus) itemRefs.current[index]?.focus()
  }

  function onKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, index: number) {
    const last = FACES.length - 1
    let next: number | null = null
    if (event.key === "ArrowRight" || event.key === "ArrowDown") next = index === last ? 0 : index + 1
    else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = index === 0 ? last : index - 1
    else if (event.key === "Home") next = 0
    else if (event.key === "End") next = last
    if (next !== null) {
      event.preventDefault()
      select(next, true)
    }
  }

  function send() {
    if (rating === null) return
    onSubmit?.({ rating, comment: comment.trim() })
    setSent(true)
  }

  function reset() {
    setRating(null)
    setComment("")
    setSent(false)
  }

  return (
    <div
      className={cn("w-full max-w-sm rounded-2xl border bg-card p-5 text-card-foreground shadow-sm", className)}
      {...props}
    >
      {sent ? (
        <div role="status" className="flex flex-col items-center gap-3 py-4 text-center animate-in fade-in zoom-in-95 duration-300">
          <span className="flex size-11 items-center justify-center rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
            <Check className="size-5" />
          </span>
          <div>
            <p className="font-medium">Thanks for your feedback!</p>
            <p className="mt-1 text-sm text-muted-foreground">It helps us make things better.</p>
          </div>
          <button
            type="button"
            onClick={reset}
            className="rounded-md text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"
          >
            Send another response
          </button>
        </div>
      ) : (
        <>
          <p id={`${groupId}-q`} className="text-sm font-medium">
            {question}
          </p>
          <div role="radiogroup" aria-labelledby={`${groupId}-q`} className="mt-3 flex justify-between gap-1">
            {FACES.map((face, i) => {
              const checked = rating === i + 1
              return (
                <button
                  key={face.label}
                  ref={(el) => {
                    itemRefs.current[i] = el
                  }}
                  type="button"
                  role="radio"
                  aria-checked={checked}
                  aria-label={face.label}
                  title={face.label}
                  tabIndex={checked || (rating === null && i === 0) ? 0 : -1}
                  onClick={() => select(i)}
                  onKeyDown={(e) => onKeyDown(e, i)}
                  className={cn(
                    "flex size-12 items-center justify-center rounded-xl border text-2xl transition-all focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40",
                    checked
                      ? "scale-110 border-primary bg-primary/10"
                      : "border-transparent opacity-60 grayscale hover:bg-accent hover:opacity-100 hover:grayscale-0"
                  )}
                >
                  <span aria-hidden>{face.emoji}</span>
                </button>
              )
            })}
          </div>
          <p className="mt-2 h-4 text-center text-xs text-muted-foreground" aria-live="polite">
            {rating ? FACES[rating - 1].label : ""}
          </p>

          {rating !== null && (
            <div className="mt-3 grid gap-3 animate-in fade-in slide-in-from-top-1 duration-200">
              <label className="sr-only" htmlFor={`${groupId}-c`}>
                Tell us more
              </label>
              <textarea
                id={`${groupId}-c`}
                value={comment}
                onChange={(e) => setComment(e.target.value)}
                rows={3}
                placeholder="Tell us more (optional)"
                className="w-full resize-none rounded-lg border bg-background px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/40"
              />
              <div className="flex justify-end">
                <button
                  type="button"
                  onClick={send}
                  className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"
                >
                  Send
                </button>
              </div>
            </div>
          )}
        </>
      )}
    </div>
  )
}
```
