Checkbox

Choice Limit

A group with a cap: at the limit the remaining options switch off, while a bar and a counter show in advance how many picks are left.

  • checkbox
  • limit
  • survey
  • group

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/checkbox-012?lang=en

What matters most to you

1 из 3

Можно отметить ещё 2.

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 "checkbox-012" (Choice Limit) 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/checkbox-012.json

Registry item: https://vibeui.ru/r/checkbox-012.json
Installs to: components/vibeui/checkbox-012.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 group with a cap: at the limit the remaining options switch off, while a bar and a counter show in advance how many picks are left.

A checkbox group with a cap: a "2 of 3" counter, a fill bar and unchecked options disabled at the limit. The rule is explained before the tap, not as an error after it. Zero dependencies, one file.

## 3. How to use it
import { Checkbox012 } from "@/components/vibeui/checkbox-012"

<Checkbox012
  legend="What matters most to you"
  limit={3}
  options={options}
  onChange={setValue}
/>

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-checkbox-012-* palette — do not swap it for your theme tokens
- disabling only the unchecked options: users must always be able to undo their own pick
- the "how many of how many" counter — without it the limit looks like a bug
- the fill bar next to the counter: length registers before the number does
- the sentence under the list explaining what to do at the limit
- keeping blocked options in the list: removing them is a lie, they return once a tick is cleared

## 6. You may change
- the options array and the initial defaultValue
- limit — the selection cap
- legend — the group's name
- the onChange handler and the accent through the accent prop

## 7. Rules
- A limit below two turns the group into a single choice: use radios, not checkboxes.
- The component reports no over-limit attempt: it simply prevents one.
- With more than ten options add a search — blocked rows in a long list irritate.
- 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/checkbox-012.json
https://vibeui.ru/r/checkbox-012.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-checkbox-012-*. Клиентский: "use client". Достигнутый лимит ставит disabled только невыбранным пунктам, поэтому снять галочку можно всегда. Полоса заполнения — span с шириной в процентах (динамическое значение, класса для него нет). Текст под списком объясняет запрет словами, счётчик объявлен role="status".

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 Checkbox012Props = Omit<
  ComponentPropsWithoutRef<"fieldset">,
  "children" | "onChange"
> & {
  legend?: string
  options?: string[]
  limit?: number
  defaultValue?: string[]
  onChange?: (value: string[]) => void
  accent?: string
}

// Идея компонента: группа с потолком выбора. На лимите невыбранные пункты
// выключаются, а счётчик заранее показывает, сколько осталось, — запрет
// объясняется до нажатия, а не после него сообщением об ошибке.
const STYLES = `
:where([data-vibeui-block="checkbox-012"]){
--vibeui-checkbox-012-bg:oklch(1 0 0);
--vibeui-checkbox-012-fg:oklch(0.22 0.014 265);
--vibeui-checkbox-012-muted:oklch(0.56 0.014 265);
--vibeui-checkbox-012-border:oklch(0.9 0.006 265);
--vibeui-checkbox-012-hover:oklch(0.975 0.003 265);
--vibeui-checkbox-012-accent:oklch(0.58 0.16 35);
--vibeui-checkbox-012-track:oklch(0.93 0.006 265);
--vibeui-checkbox-012-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="checkbox-012"]{
display:flex;flex-direction:column;gap:0.125rem;
width:100%;max-width:20rem;box-sizing:border-box;
margin:0;padding:0.875rem;border:1px solid var(--vibeui-checkbox-012-border);border-radius:0.875rem;
background:var(--vibeui-checkbox-012-bg);
font-family:var(--vibeui-checkbox-012-font);color:var(--vibeui-checkbox-012-fg);
}
[data-vibeui-block="checkbox-012"] legend{float:left;width:100%;padding:0;font-size:0.875rem;font-weight:650}
[data-vibeui-block="checkbox-012"] [data-part="meter"]{
clear:both;display:flex;align-items:center;gap:0.5rem;
margin:0.375rem 0 0.5rem;font-size:0.75rem;color:var(--vibeui-checkbox-012-muted);
font-variant-numeric:tabular-nums;
}
/* Полоса заполнения — вторая, невербальная подсказка о лимите: число
   замечают не все, длину — все. */
[data-vibeui-block="checkbox-012"] [data-part="track"]{
flex:1;height:0.25rem;border-radius:9999px;overflow:hidden;
background:var(--vibeui-checkbox-012-track);
}
[data-vibeui-block="checkbox-012"] [data-part="fill"]{
display:block;height:100%;border-radius:9999px;
background:var(--vibeui-checkbox-012-accent);
transition:width .2s ease;
}
[data-vibeui-block="checkbox-012"] label{
display:flex;align-items:center;gap:0.625rem;
min-height:2.125rem;padding:0 0.5rem;margin:0 -0.5rem;border-radius:0.5rem;
font-size:0.875rem;cursor:pointer;
transition:background-color .15s ease;
}
[data-vibeui-block="checkbox-012"] label:hover{background:var(--vibeui-checkbox-012-hover)}
/* Заблокированный лимитом пункт остаётся в списке и просто гаснет: убрать
   его было бы враньём — он доступен, как только снимут другую галку. */
[data-vibeui-block="checkbox-012"] label:has(input:disabled){
cursor:not-allowed;color:var(--vibeui-checkbox-012-muted);
}
[data-vibeui-block="checkbox-012"] label:has(input:disabled):hover{background:transparent}
[data-vibeui-block="checkbox-012"] input{
appearance:none;position:relative;flex:none;cursor:inherit;
width:1.0625rem;height:1.0625rem;margin:0;box-sizing:border-box;
border:1.5px solid var(--vibeui-checkbox-012-border);border-radius:0.3125rem;
background:var(--vibeui-checkbox-012-bg);
transition:background-color .15s ease,border-color .15s ease;
}
[data-vibeui-block="checkbox-012"] input:checked{border-color:transparent;background:var(--vibeui-checkbox-012-accent)}
[data-vibeui-block="checkbox-012"] input:checked::after{
content:"";position:absolute;left:50%;top:50%;
width:0.25rem;height:0.4375rem;margin:-0.3125rem 0 0 -0.125rem;
border-right:2px solid oklch(0.99 0.01 35);border-bottom:2px solid oklch(0.99 0.01 35);
transform:rotate(45deg);
}
[data-vibeui-block="checkbox-012"] input:disabled{background:var(--vibeui-checkbox-012-track);border-color:transparent}
[data-vibeui-block="checkbox-012"] input:focus-visible{outline:2px solid var(--vibeui-checkbox-012-accent);outline-offset:2px}
[data-vibeui-block="checkbox-012"] [data-part="note"]{
margin:0.5rem 0 0;font-size:0.75rem;line-height:1.4;color:var(--vibeui-checkbox-012-muted);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="checkbox-012"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_OPTIONS = [
  "Скорость загрузки",
  "Понятность интерфейса",
  "Поддержка",
  "Цена",
  "Надёжность",
  "Внешний вид",
]

/**
 * Группа чекбоксов с потолком выбора: на лимите остальные выключаются,
 * счётчик показывает остаток. Один файл, ноль зависимостей.
 */
export function Checkbox012({
  legend = "Что для вас важнее всего",
  options = DEFAULT_OPTIONS,
  limit = 3,
  defaultValue = ["Цена"],
  onChange,
  accent,
  className,
  style,
  ...props
}: Checkbox012Props) {
  const [value, setValue] = useState<string[]>(defaultValue)

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

  const full = value.length >= limit
  const left = Math.max(0, limit - value.length)

  const toggle = (option: string) => {
    const next = value.includes(option)
      ? value.filter((item) => item !== option)
      : [...value, option]

    setValue(next)
    onChange?.(next)
  }

  return (
    <>
      <style href="vibeui-checkbox-012" precedence="medium">
        {STYLES}
      </style>
      <fieldset
        {...props}
        data-vibeui-block="checkbox-012"
        className={className}
        style={palette}
      >
        <legend>{legend}</legend>
        <p data-part="meter">
          <span data-part="track">
            <span
              data-part="fill"
              style={{ width: `${(value.length / limit) * 100}%` }}
            />
          </span>
          <span role="status">
            {value.length} из {limit}
          </span>
        </p>
        {options.map((option) => {
          const checked = value.includes(option)

          return (
            <label key={option}>
              <input
                type="checkbox"
                checked={checked}
                disabled={!checked && full}
                onChange={() => toggle(option)}
              />
              <span>{option}</span>
            </label>
          )
        })}
        <p data-part="note">
          {full
            ? "Выбрано максимум. Снимите одну галочку, чтобы поставить другую."
            : `Можно отметить ещё ${left}.`}
        </p>
      </fieldset>
    </>
  )
}