Floating feedback card (bottom-right) with a 1–5 star rating. A comment box reveals for low scores, a brand Submit button appears once a rating is picked, and a success check plays on submit. Self-shows with a per-user cooldown and never touches host form state.
Click to open the feedback card.
Comments always show; the placeholder flips by score.
npx shadcn@latest add https://sdk-components.thesqd.com/r/experience-rating.jsonimport { ExperienceRating } from "@/components/blocks/experience-rating";import { ExperienceRating } from "@/components/blocks/experience-rating";
export function Feedback() {
return (
<ExperienceRating
// Anything here is passed straight through to onSubmit.
metadata={{ userId: "abc", formId: "welcome", route: "/onboarding" }}
onSubmit={async ({ rating, comments, metadata }) => {
// Persist however the host app likes (its own route, Supabase, …).
await fetch("/api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rating, comments, metadata }),
});
}}
/>
);
}{
"rating": 3,
"comments": "test",
"metadata": {}
}ExperienceRating — the floating feedback card (self-showing, cooldown-gated)
ExperienceRatingDemo — button-triggered preview
ExperienceRatingScoredDemo — score-aware placeholder preview| Prop | Type | Default | Description |
|---|---|---|---|
metadata | Record<string, any> | {} | Passed straight through to onSubmit and into the emitted object. |
onSubmit | (r: { rating, comments, metadata }) => void | Promise | — | Called on submit. Host decides how to persist. Success check awaits the promise. |
onDismiss | () => void | — | Fired when dismissed via the X. |
open | boolean | — | Controlled visibility. When set, auto-show + cooldown are bypassed. |
onOpenChange | (open: boolean) => void | — | Fires with the next open state. |
ratingStyle | "emoji" | "stars" | "emoji" | Rating control style. |
emojis | string[] | ["😞","🙁","😐","🙂","😄"] | Override the 5 emojis (low→high). Only used when ratingStyle="emoji". |
title | ReactNode | "How was your experience?" | Card heading. |
description | ReactNode | "Your feedback helps us improve." | Sub-line under the title. |
commentThreshold | number | 3 | Show the comment box when the rating is ≤ this value. Set to 5 to always show. |
commentPlaceholder | string | ((rating: number) => string) | "Tell us what we could do better…" | Comment placeholder. Pass a function of the rating to vary it by score, e.g. "Tell us what we did right" on 5. |
submitLabel | ReactNode | "Submit feedback" | Submit button label. |
successLabel | ReactNode | "Thanks for the feedback!" | Success-state message. |
cooldownDays | number | 7 | Don't re-prompt within this many days of last shown/dismissed/filled. |
storageKey | string | "squad-experience-rating" | localStorage namespace for the cooldown timestamps. |
anchor | "fixed" | "absolute" | "inline" | "fixed" | "fixed" portals to <body>; "absolute" anchors to the nearest positioned parent; "inline" draws it in the page flow, always shown, with no dismiss and no cooldown. |
value | { rating: number; comments?: string | null } | null | — | Inline only: an answer already given, drawn instead of the form. |
valueDescription | ReactNode | — | Inline, answered: the line under the heading, in the description's style (e.g. when it was left). |
disabled | boolean | false | Inline only: shown but not answerable. |
disabledHint | ReactNode | — | Shown under a disabled rating to say why. |
size | "sm" | "md" | "lg" | "md" | How big the faces (or stars) are. |
className | string | — | Classes for the card surface (background, border, padding). Same as classNames.surface. |
classNames | { surface?, title?, description?, rating?, option?, submit? } | — | Per-part classes, merged after the block's own, to restyle any part without forking it. |