Inputs

Thumbs Vote

A binary vote with counters: "was this article helpful" has two answers, and five stars only make the choice longer.

  • rating
  • vote
  • thumbs
  • feedback

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/rating-005?lang=en

Was this article helpful?

Голос анонимный и его можно отозвать повторным нажатием.

Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "rating-005" (Thumbs Vote) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/rating-005.json

Registry item: https://vibeui.ru/r/rating-005.json
Installs to: components/vibeui/rating-005.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
A binary vote with counters: "was this article helpful" has two answers, and five stars only make the choice longer.

A thumbs up / thumbs down vote with counters, where pressing the same button again withdraws the vote. Zero dependencies, one file.

## 3. How to use it
import { Rating005 } from "@/components/vibeui/rating-005"

<Rating005 question="Was this article helpful?" up={128} down={6} />

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-rating-005-* palette — do not swap it for your theme tokens
- its own light surface: the block gets shown over any background
- withdrawing the vote on a second press: an accidental click is otherwise irreversible
- the instant counter update: "recorded" without the digit moving reads as a lie
- aria-pressed on the buttons: this is a selection state, not a one-off press
- different colours for up and down: identical buttons force reading the icon
- the aria-labels on the buttons: they hold an icon and a number, not words

## 6. You may change
- question — the question itself
- up and down — the starting vote counters
- the thumb icon
- the thank-you text shown after voting
- the accent through the accent prop

## 7. Rules
- The counter is local: send the vote to the server and feed the real numbers back.
- Nothing limits repeat voting — put the anti-fraud logic on the backend.
- The down button reuses the same icon rotated 180 degrees: a second file is not needed.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/rating-005.json
https://vibeui.ru/r/rating-005.json

Компонент самодостаточен: один файл, ноль зависимостей, палитра в локальных переменных --vibeui-rating-005-*. Клиентский: голос в состоянии. Повторное нажатие на ту же кнопку снимает голос — случайный клик обязан откатываться. Счётчики складываются из пришедших чисел и собственного голоса, поэтому цифра двигается сразу после нажатия: «спасибо, учтено» без движения счётчика выглядит как обман. Состояние кнопок передаётся через aria-pressed, а цвет зоны — через data-vote.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

"use client"

import { useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Rating005Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "defaultValue" | "onChange"
> & {
  question?: string
  up?: number
  down?: number
  accent?: string
}

// Идея компонента: бинарная оценка вместо шкалы. «Помогла ли статья» — вопрос
// с двумя ответами, и пять звёзд тут только удлиняют выбор. Повторное нажатие
// снимает голос: случайный клик обязан откатываться, иначе человек уходит с
// чужим для себя ответом. Счётчики пересчитываются сразу, включая свой голос,
// потому что «спасибо, учтено» без движения цифры выглядит как обман.
const STYLES = `
:where([data-vibeui-block="rating-005"]){
--vibeui-rating-005-surface:oklch(1 0 0);
--vibeui-rating-005-shell:oklch(0.9 0.006 265);
--vibeui-rating-005-fg:oklch(0.23 0.014 265);
--vibeui-rating-005-muted:oklch(0.55 0.014 265);
--vibeui-rating-005-border:oklch(0.88 0.008 265);
--vibeui-rating-005-up:oklch(0.55 0.15 155);
--vibeui-rating-005-down:oklch(0.56 0.18 25);
--vibeui-rating-005-accent:var(--vibeui-rating-005-up);
--vibeui-rating-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: блок показывают поверх любого фона. */
[data-vibeui-block="rating-005"]{
display:flex;flex-direction:column;gap:0.625rem;
width:100%;max-width:20rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-rating-005-surface);
border:1px solid var(--vibeui-rating-005-shell);border-radius:0.875rem;
font-family:var(--vibeui-rating-005-font);color:var(--vibeui-rating-005-fg);
}
[data-vibeui-block="rating-005"] [data-part="question"]{margin:0;font-size:0.875rem;font-weight:650}
[data-vibeui-block="rating-005"] [data-part="row"]{display:flex;gap:0.5rem}
[data-vibeui-block="rating-005"] button{
appearance:none;cursor:pointer;flex:1 1 0;
display:inline-flex;align-items:center;justify-content:center;gap:0.375rem;
height:2.5rem;padding:0 0.75rem;
border:1px solid var(--vibeui-rating-005-border);border-radius:0.625rem;
background:var(--vibeui-rating-005-surface);color:var(--vibeui-rating-005-fg);
font:inherit;font-size:0.8125rem;font-weight:650;font-variant-numeric:tabular-nums;
transition:border-color .14s ease,background-color .14s ease,color .14s ease;
}
[data-vibeui-block="rating-005"] button[data-vote="up"]:hover{border-color:var(--vibeui-rating-005-up)}
[data-vibeui-block="rating-005"] button[data-vote="down"]:hover{border-color:var(--vibeui-rating-005-down)}
/* Отданный голос виден заливкой: «учтено» без изменения кнопки не читается. */
[data-vibeui-block="rating-005"] button[data-vote="up"][aria-pressed="true"]{
border-color:transparent;color:var(--vibeui-rating-005-up);
background:color-mix(in oklch,var(--vibeui-rating-005-up) 14%,white);
}
[data-vibeui-block="rating-005"] button[data-vote="down"][aria-pressed="true"]{
border-color:transparent;color:var(--vibeui-rating-005-down);
background:color-mix(in oklch,var(--vibeui-rating-005-down) 14%,white);
}
[data-vibeui-block="rating-005"] button:focus-visible{outline:2px solid var(--vibeui-rating-005-accent);outline-offset:2px}
[data-vibeui-block="rating-005"] svg{width:1.125rem;height:1.125rem;flex:none}
[data-vibeui-block="rating-005"] [data-part="note"]{
margin:0;font-size:0.75rem;line-height:1.4;color:var(--vibeui-rating-005-muted);
}
[data-vibeui-block="rating-005"] [data-part="note"] b{color:var(--vibeui-rating-005-fg);font-weight:650}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="rating-005"] *{animation:none!important;transition:none!important}}
`

const THUMB =
  "M7 21V10.5l4.2-7.1a1.6 1.6 0 0 1 3 .8V9h4.3a1.7 1.7 0 0 1 1.66 2.05l-1.4 7A2 2 0 0 1 16.8 19.7H7Zm-4 0h4V10.5H3Z"

/**
 * Бинарная оценка «палец вверх / палец вниз» со счётчиками и отзывом голоса.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Rating005({
  question = "Статья оказалась полезной?",
  up = 128,
  down = 6,
  accent,
  className,
  style,
  ...props
}: Rating005Props) {
  const [vote, setVote] = useState<"up" | "down" | null>(null)

  const palette = {
    ...(accent ? { "--vibeui-rating-005-accent": accent } : null),
    ...style,
  } as CSSProperties

  const counts = {
    up: up + (vote === "up" ? 1 : 0),
    down: down + (vote === "down" ? 1 : 0),
  }

  return (
    <>
      <style href="vibeui-rating-005" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="rating-005"
        className={className}
        style={palette}
      >
        <p data-part="question">{question}</p>
        <div data-part="row">
          {(["up", "down"] as const).map((side) => (
            <button
              key={side}
              type="button"
              data-vote={side}
              aria-pressed={vote === side}
              aria-label={side === "up" ? "Да, помогла" : "Нет, не помогла"}
              // Повторное нажатие снимает голос: случайный клик надо откатывать.
              onClick={() => setVote(vote === side ? null : side)}
            >
              <svg
                viewBox="0 0 24 24"
                fill="currentColor"
                aria-hidden="true"
                style={
                  side === "down" ? { transform: "rotate(180deg)" } : undefined
                }
              >
                <path d={THUMB} />
              </svg>
              {counts[side]}
            </button>
          ))}
        </div>
        <p data-part="note" aria-live="polite">
          {vote === null ? (
            "Голос анонимный и его можно отозвать повторным нажатием."
          ) : (
            <>
              <b>Спасибо, голос учтён.</b> Нажмите ту же кнопку ещё раз, чтобы
              его отозвать.
            </>
          )}
        </p>
      </div>
    </>
  )
}