Button Group

Status Dots Filter

A status filter whose marker carries two signals at once — colour and shape: a dot, a ring and a square stay distinct even in black and white.

  • buttongroup
  • status
  • filter
  • a11y

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/buttongroup-031?lang=en

Task statuses
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 "buttongroup-031" (Status Dots Filter) 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/buttongroup-031.json

Registry item: https://vibeui.ru/r/buttongroup-031.json
Installs to: components/vibeui/buttongroup-031.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 status filter whose marker carries two signals at once — colour and shape: a dot, a ring and a square stay distinct even in black and white.

A multi-select status filter with markers distinguished by shape, not colour alone. Zero dependencies, one file, a server component.

## 3. How to use it
import { Buttongroup031 } from "@/components/vibeui/buttongroup-031"

<Buttongroup031
  statuses={[{ id: "new", label: "New", color: "oklch(0.6 0.17 250)", shape: "dot" }]}
  defaultValue={["new"]}
  label="Task statuses"
  name="status"
/>

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-buttongroup-031-* palette — do not swap it for your theme tokens
- the different marker shapes: colour alone separates nothing for colour-blind users or in print
- checkboxes instead of radios — statuses are filtered together, not one at a time
- the caption next to the marker: a bare dot needs a legend that is not on screen
- the z-index on selection and focus: with collapsed borders the neighbour clips the border and the ring
- the inline variable for the marker colour — otherwise every status needs a class of its own

## 6. You may change
- the statuses, their colours and shapes through statuses
- the initially enabled statuses through defaultValue
- the accessible group name through label
- the form field prefix through name and the selection colour through accent

## 7. Rules
- Every checkbox gets a name of the form name-id: on the server these are separate fields, not an array.
- There are exactly three shapes: a fourth status needs its own marker in the CSS.
- The component filters nothing: your code queries by the form values.
- 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/buttongroup-031.json
https://vibeui.ru/r/buttongroup-031.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в переменных --vibeui-buttongroup-031-*. Серверный: выбор множественный, внутри checkbox — статусы складываются, а не заменяют друг друга. Форма метки задаётся атрибутом data-shape: dot — круг с заливкой, ring — круг на inset box-shadow, square — скруглённый квадрат. Цвет приходит данными и подставляется инлайновой переменной --vibeui-buttongroup-031-mark: это тот случай, когда значение нельзя выразить классом. Границы сцепки схлопнуты через margin-inline-start:-1px, скругления только крайним, выбранный поднят z-index:1, сфокусированный — z-index:2.

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 Buttongroup031Status = {
  id: string
  label: string
  color: string
  shape: "dot" | "ring" | "square"
}

export type Buttongroup031Props = Omit<
  ComponentPropsWithoutRef<"fieldset">,
  "children"
> & {
  statuses?: Buttongroup031Status[]
  defaultValue?: string[]
  label?: string
  name?: string
  accent?: string
}

// Идея компонента: фильтр статусов, где метка несёт два признака сразу —
// цвет и форму. Кружок, кольцо и квадрат различаются при любой форме
// дальтонизма и в чёрно-белой печати, а цвет остаётся быстрым каналом для
// тех, кто его видит. Цвет приходит данными и подставляется инлайновой
// переменной --vibeui-buttongroup-031-mark: это единственный случай, когда
// значение нельзя выразить классом. Выбор множественный, поэтому внутри
// checkbox: статусы складываются, а не заменяют друг друга.
const STYLES = `
:where([data-vibeui-block="buttongroup-031"]){
--vibeui-buttongroup-031-surface:oklch(1 0 0);
--vibeui-buttongroup-031-fg:oklch(0.25 0.016 265);
--vibeui-buttongroup-031-muted:oklch(0.57 0.014 265);
--vibeui-buttongroup-031-border:oklch(0.89 0.008 265);
--vibeui-buttongroup-031-accent:oklch(0.45 0.02 265);
--vibeui-buttongroup-031-mark:oklch(0.6 0.02 265);
--vibeui-buttongroup-031-radius:0.625rem;
--vibeui-buttongroup-031-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="buttongroup-031"]{
box-sizing:border-box;display:inline-block;
margin:0;padding:0;border:0;
font-family:var(--vibeui-buttongroup-031-font);
}
[data-vibeui-block="buttongroup-031"] *{box-sizing:border-box}
[data-vibeui-block="buttongroup-031"] legend{
position:absolute;width:1px;height:1px;padding:0;margin:-1px;
overflow:hidden;clip-path:inset(50%);white-space:nowrap;
}
[data-vibeui-block="buttongroup-031"] [data-part="track"]{display:flex;isolation:isolate}
[data-vibeui-block="buttongroup-031"] [data-part="chip"]{
position:relative;z-index:0;
display:inline-flex;align-items:center;gap:0.4375rem;
height:2.25rem;padding:0 0.8125rem;margin-inline-start:-1px;
border:1px solid var(--vibeui-buttongroup-031-border);
background:var(--vibeui-buttongroup-031-surface);
color:var(--vibeui-buttongroup-031-muted);
font-size:0.8125rem;font-weight:600;line-height:1;white-space:nowrap;cursor:pointer;
transition:background-color .16s ease,color .16s ease,border-color .16s ease;
}
[data-vibeui-block="buttongroup-031"] [data-part="chip"]:first-child{
margin-inline-start:0;
border-start-start-radius:var(--vibeui-buttongroup-031-radius);
border-end-start-radius:var(--vibeui-buttongroup-031-radius);
}
[data-vibeui-block="buttongroup-031"] [data-part="chip"]:last-child{
border-start-end-radius:var(--vibeui-buttongroup-031-radius);
border-end-end-radius:var(--vibeui-buttongroup-031-radius);
}
[data-vibeui-block="buttongroup-031"] input{
position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer;
}
/* Форма метки — второй, не цветовой признак статуса. */
[data-vibeui-block="buttongroup-031"] [data-part="mark"]{
width:0.625rem;height:0.625rem;flex:none;
background:var(--vibeui-buttongroup-031-mark);
}
[data-vibeui-block="buttongroup-031"] [data-shape="dot"]{border-radius:9999px}
[data-vibeui-block="buttongroup-031"] [data-shape="ring"]{
border-radius:9999px;background:transparent;
box-shadow:inset 0 0 0 2.5px var(--vibeui-buttongroup-031-mark);
}
[data-vibeui-block="buttongroup-031"] [data-shape="square"]{border-radius:0.1875rem}
[data-vibeui-block="buttongroup-031"] [data-part="chip"]:hover{color:var(--vibeui-buttongroup-031-fg)}
[data-vibeui-block="buttongroup-031"] [data-part="chip"]:has(input:checked){
z-index:1;
background:oklch(0.965 0.004 265);
border-color:var(--vibeui-buttongroup-031-accent);
color:var(--vibeui-buttongroup-031-fg);
}
[data-vibeui-block="buttongroup-031"] [data-part="chip"]:has(input:focus-visible){
z-index:2;outline:2px solid var(--vibeui-buttongroup-031-accent);outline-offset:1px;
}
[data-vibeui-block="buttongroup-031"] [data-part="state"]{
position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="buttongroup-031"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_STATUSES: Buttongroup031Status[] = [
  { id: "new", label: "Новые", color: "oklch(0.6 0.17 250)", shape: "dot" },
  {
    id: "work",
    label: "В работе",
    color: "oklch(0.72 0.15 85)",
    shape: "ring",
  },
  {
    id: "done",
    label: "Готово",
    color: "oklch(0.6 0.14 150)",
    shape: "square",
  },
]

/**
 * Фильтр статусов, где метка различается формой, а не только цветом.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Buttongroup031({
  statuses = DEFAULT_STATUSES,
  defaultValue = ["new", "work"],
  label = "Статусы задач",
  name = "buttongroup-031",
  accent,
  className,
  style,
  ...props
}: Buttongroup031Props) {
  const palette = {
    ...(accent ? { "--vibeui-buttongroup-031-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-buttongroup-031" precedence="medium">
        {STYLES}
      </style>
      <fieldset
        {...props}
        data-vibeui-block="buttongroup-031"
        className={className}
        style={palette}
      >
        <legend>{label}</legend>
        <div data-part="track">
          {statuses.map((status) => (
            <label key={status.id} data-part="chip">
              <input
                type="checkbox"
                name={`${name}-${status.id}`}
                value={status.id}
                defaultChecked={defaultValue.includes(status.id)}
              />
              <span
                data-part="mark"
                data-shape={status.shape}
                aria-hidden="true"
                style={
                  {
                    "--vibeui-buttongroup-031-mark": status.color,
                  } as CSSProperties
                }
              />
              <span>{status.label}</span>
            </label>
          ))}
        </div>
      </fieldset>
    </>
  )
}