Componenteditors0.76 KB gzipZero dependencies

Feedback Vote

Up and down vote pair with live tally.

Preview

Live Preview
42

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add feedback-vote

Or install the full npm package:

npm install bigbullui
import { FeedbackVote } from "@/components/ui/feedback-vote";

<FeedbackVote />

Usage guidance

Use Feedback Vote when a ticket stub surface needs Up and down vote pair with live tally. Keep props minimal and prefer composition with sibling stubs.

Avoid Feedback Vote for long form flows or dense data grids where a dedicated layout block fits better.

Source code

feedback-vote.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface FeedbackVoteProps extends React.HTMLAttributes<HTMLDivElement> {
  upLabel?: string;
  downLabel?: string;
  onVote?: (vote: "up" | "down") => void;
}

/** Up and down vote pair with live tally stamp. */
export function FeedbackVote({ upLabel = "Useful", downLabel = "Off", onVote, className, ...props }: FeedbackVoteProps) {
  const [vote, setVote] = React.useState<"up" | "down" | null>(null);
  const [tally, setTally] = React.useState(42);
  const cast = (v: "up" | "down") => {
    if (vote === v) return;
    setTally((t) => t + (v === "up" ? 1 : -1) - (vote === "up" ? 1 : vote === "down" ? -1 : 0));
    setVote(v);
    onVote?.(v);
  };
  return (
    <div className={cn("flex items-center gap-2", className)} {...props}>
      <button
        type="button"
        aria-pressed={vote === "up"}
        onClick={() => cast("up")}
        className={cn(
          "rounded-md border-2 border-dashed px-3 py-1 font-mono text-xs font-bold uppercase focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
          vote === "up" ? "border-solid border-foreground bg-primary text-primary-foreground" : "border-border bg-card text-muted-foreground hover:text-foreground"
        )}
      >
        Approve {upLabel}
      </button>
      <span className="rounded bg-secondary px-2 py-1 font-mono text-xs font-bold text-accent" aria-live="polite">{tally}</span>
      <button
        type="button"
        aria-pressed={vote === "down"}
        onClick={() => cast("down")}
        className={cn(
          "rounded-md border-2 border-dashed px-3 py-1 font-mono text-xs font-bold uppercase focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
          vote === "down" ? "border-solid border-destructive bg-destructive text-white" : "border-border bg-card text-muted-foreground hover:text-foreground"
        )}
      >
        Reject {downLabel}
      </button>
    </div>
  );
}