Inputs

Half Stars

Half stars without a line of JS: the fill is a stripe over the grey row, and its width comes from the checked radio through :has().

  • rating
  • stars
  • half
  • css-only

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-002?lang=en

How was it

Ваша оценка: 0,5 из 5

Ваша оценка: 1 из 5

Ваша оценка: 1,5 из 5

Ваша оценка: 2 из 5

Ваша оценка: 2,5 из 5

Ваша оценка: 3 из 5

Ваша оценка: 3,5 из 5

Ваша оценка: 4 из 5

Ваша оценка: 4,5 из 5

Ваша оценка: 5 из 5

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-002" (Half Stars) 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-002.json

Registry item: https://vibeui.ru/r/rating-002.json
Installs to: components/vibeui/rating-002.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
Half stars without a line of JS: the fill is a stripe over the grey row, and its width comes from the checked radio through :has().

A star rating with a half-star step built on radios and :has(), no JS. Server component, zero dependencies, one file.

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

<Rating002 legend="How was it" name="quality" defaultValue={3.5} />

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-002-* palette — do not swap it for your theme tokens
- its own light surface: the rating gets shown over any background
- the fill stripe over the grey row: cutting stars in half would need icons, while percentages are enough here
- the radios under the hit zones: they hand over keyboard support, form submission and group semantics for free
- the ten half-star hit zones: without them a half cannot be picked with a mouse
- clear:both on the star row: a legend with float:left otherwise makes it wrap around
- pointer-events:none on the fill stripe: otherwise it swallows the clicks meant for the stars

## 6. You may change
- legend — the question above the row
- name — the radio group name in the form
- defaultValue — the pre-selected score
- the star size through the row font-size
- the accent through the accent prop

## 7. Rules
- The scale is fixed at five stars: ten values are encoded in :has rules, another length means rewriting them.
- The five stars are text: replace them with an icon in a font without the ★ glyph.
- The score cannot be cleared — a radio never unchecks; add a reset button if you need one.
- 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-002.json
https://vibeui.ru/r/rating-002.json

Компонент самодостаточен: один файл, ноль зависимостей, палитра в локальных переменных --vibeui-rating-002-*. Серверный: всё держится на радиокнопках и :has(). Половинки сделаны не разрезанными звёздами, а полосой закраски поверх серого ряда — любая дробность сводится к одному числу процентов в переменной --vibeui-rating-002-fill. Зоны нажатия — десять прозрачных меток по половине звезды, спозиционированных абсолютно поверх ряда; выбор мышью попадает в половинку, а стрелки работают как в обычной группе радиокнопок.

Component source

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

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Rating002Props = Omit<
  ComponentPropsWithoutRef<"fieldset">,
  "children" | "defaultValue"
> & {
  legend?: string
  name?: string
  defaultValue?: number
  accent?: string
}

// Идея компонента: половинки звёзд без единой строки JS. Пять целых делений —
// грубая шкала: «четыре с половиной» люди говорят постоянно, а поставить не
// могут. Половинки сделаны не разрезанными звёздами, а полосой закраски поверх
// серого ряда: ширина полосы задаётся по отмеченному радио через :has(), и
// любая дробность сводится к одному числу процентов. Зоны нажатия — десять
// прозрачных меток поверх ряда, по половине звезды каждая, поэтому выбор
// работает мышью, а стрелками — как в обычной группе радиокнопок.
const STYLES = `
:where([data-vibeui-block="rating-002"]){
--vibeui-rating-002-surface:oklch(1 0 0);
--vibeui-rating-002-shell:oklch(0.9 0.006 265);
--vibeui-rating-002-fg:oklch(0.23 0.014 265);
--vibeui-rating-002-muted:oklch(0.55 0.014 265);
--vibeui-rating-002-empty:oklch(0.88 0.008 265);
--vibeui-rating-002-accent:oklch(0.75 0.16 78);
--vibeui-rating-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--vibeui-rating-002-fill:0%;
}
/* Своя светлая подложка: оценку показывают поверх любого фона. */
[data-vibeui-block="rating-002"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:17rem;box-sizing:border-box;margin:0;padding:0.875rem;
background:var(--vibeui-rating-002-surface);
border:1px solid var(--vibeui-rating-002-shell);border-radius:0.875rem;
font-family:var(--vibeui-rating-002-font);color:var(--vibeui-rating-002-fg);
}
/* legend во float даёт обтекание: clear возвращает нормальный поток. */
[data-vibeui-block="rating-002"] legend{
float:left;width:100%;padding:0;margin-bottom:0.5rem;font-size:0.875rem;font-weight:650;
}
[data-vibeui-block="rating-002"] [data-part="stars"]{
clear:both;position:relative;display:inline-block;
font-size:1.75rem;line-height:1;letter-spacing:0.125rem;
}
[data-vibeui-block="rating-002"] [data-part="track"]{color:var(--vibeui-rating-002-empty)}
/* Закраска — полоса поверх серого ряда: дробность сводится к процентам. */
[data-vibeui-block="rating-002"] [data-part="fill"]{
position:absolute;inset:0 auto 0 0;overflow:hidden;white-space:nowrap;
width:var(--vibeui-rating-002-fill);color:var(--vibeui-rating-002-accent);
pointer-events:none;
}
[data-vibeui-block="rating-002"] input{position:absolute;opacity:0;pointer-events:none}
/* Десять прозрачных зон по половине звезды: мышь попадает в половинку. */
[data-vibeui-block="rating-002"] [data-part="hit"]{
position:absolute;top:0;bottom:0;width:10%;cursor:pointer;border-radius:0.25rem;
}
[data-vibeui-block="rating-002"] input:focus-visible + [data-part="hit"]{
outline:2px solid var(--vibeui-rating-002-accent);outline-offset:2px;
}
[data-vibeui-block="rating-002"]:has(input[value="0.5"]:checked){--vibeui-rating-002-fill:10%}
[data-vibeui-block="rating-002"]:has(input[value="1"]:checked){--vibeui-rating-002-fill:20%}
[data-vibeui-block="rating-002"]:has(input[value="1.5"]:checked){--vibeui-rating-002-fill:30%}
[data-vibeui-block="rating-002"]:has(input[value="2"]:checked){--vibeui-rating-002-fill:40%}
[data-vibeui-block="rating-002"]:has(input[value="2.5"]:checked){--vibeui-rating-002-fill:50%}
[data-vibeui-block="rating-002"]:has(input[value="3"]:checked){--vibeui-rating-002-fill:60%}
[data-vibeui-block="rating-002"]:has(input[value="3.5"]:checked){--vibeui-rating-002-fill:70%}
[data-vibeui-block="rating-002"]:has(input[value="4"]:checked){--vibeui-rating-002-fill:80%}
[data-vibeui-block="rating-002"]:has(input[value="4.5"]:checked){--vibeui-rating-002-fill:90%}
[data-vibeui-block="rating-002"]:has(input[value="5"]:checked){--vibeui-rating-002-fill:100%}
/* Пока курсор в ряду, показываем не выбор, а то, что будет выбрано. */
[data-vibeui-block="rating-002"]:has([data-value="0.5"]:hover){--vibeui-rating-002-fill:10%}
[data-vibeui-block="rating-002"]:has([data-value="1"]:hover){--vibeui-rating-002-fill:20%}
[data-vibeui-block="rating-002"]:has([data-value="1.5"]:hover){--vibeui-rating-002-fill:30%}
[data-vibeui-block="rating-002"]:has([data-value="2"]:hover){--vibeui-rating-002-fill:40%}
[data-vibeui-block="rating-002"]:has([data-value="2.5"]:hover){--vibeui-rating-002-fill:50%}
[data-vibeui-block="rating-002"]:has([data-value="3"]:hover){--vibeui-rating-002-fill:60%}
[data-vibeui-block="rating-002"]:has([data-value="3.5"]:hover){--vibeui-rating-002-fill:70%}
[data-vibeui-block="rating-002"]:has([data-value="4"]:hover){--vibeui-rating-002-fill:80%}
[data-vibeui-block="rating-002"]:has([data-value="4.5"]:hover){--vibeui-rating-002-fill:90%}
[data-vibeui-block="rating-002"]:has([data-value="5"]:hover){--vibeui-rating-002-fill:100%}
/* Число тоже без JS: видна та строка, чей value отмечен. */
[data-vibeui-block="rating-002"] [data-part="value"]{
display:none;margin:0;font-size:0.75rem;color:var(--vibeui-rating-002-muted);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="rating-002"] [data-part="values"]{min-height:1rem}
[data-vibeui-block="rating-002"]:has(input[value="0.5"]:checked) [data-part="value"][data-value="0.5"],
[data-vibeui-block="rating-002"]:has(input[value="1"]:checked) [data-part="value"][data-value="1"],
[data-vibeui-block="rating-002"]:has(input[value="1.5"]:checked) [data-part="value"][data-value="1.5"],
[data-vibeui-block="rating-002"]:has(input[value="2"]:checked) [data-part="value"][data-value="2"],
[data-vibeui-block="rating-002"]:has(input[value="2.5"]:checked) [data-part="value"][data-value="2.5"],
[data-vibeui-block="rating-002"]:has(input[value="3"]:checked) [data-part="value"][data-value="3"],
[data-vibeui-block="rating-002"]:has(input[value="3.5"]:checked) [data-part="value"][data-value="3.5"],
[data-vibeui-block="rating-002"]:has(input[value="4"]:checked) [data-part="value"][data-value="4"],
[data-vibeui-block="rating-002"]:has(input[value="4.5"]:checked) [data-part="value"][data-value="4.5"],
[data-vibeui-block="rating-002"]:has(input[value="5"]:checked) [data-part="value"][data-value="5"]{display:block}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="rating-002"] *{animation:none!important;transition:none!important}}
`

const STEPS = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5]

/**
 * Оценка звёздами с половинками, собранная на радиокнопках и :has().
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Rating002({
  legend = "Насколько понравилось",
  name = "vibeui-rating-002",
  defaultValue = 3.5,
  accent,
  className,
  style,
  ...props
}: Rating002Props) {
  const palette = {
    ...(accent ? { "--vibeui-rating-002-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-rating-002" precedence="medium">
        {STYLES}
      </style>
      <fieldset
        {...props}
        data-vibeui-block="rating-002"
        className={className}
        style={palette}
      >
        <legend>{legend}</legend>
        <div data-part="stars">
          <span data-part="track" aria-hidden="true">
            ★★★★★
          </span>
          <span data-part="fill" aria-hidden="true">
            ★★★★★
          </span>
          {STEPS.map((step, index) => (
            <span key={step}>
              <input
                type="radio"
                name={name}
                value={step}
                defaultChecked={step === defaultValue}
                aria-label={`${step} из 5`}
              />
              <span
                data-part="hit"
                data-value={step}
                style={{ left: `${index * 10}%` }}
              />
            </span>
          ))}
        </div>
        <div data-part="values" aria-live="polite">
          {STEPS.map((step) => (
            <p key={step} data-part="value" data-value={step}>
              Ваша оценка: {step.toString().replace(".", ",")} из 5
            </p>
          ))}
        </div>
      </fieldset>
    </>
  )
}