Toggle Group

Format Group

A multi-select toggle group: type styles stack, so any number of buttons can be pressed at once and the sample updates immediately.

  • togglegroup
  • multiple
  • formatting
  • aria-pressed

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

Выбрано: 1 из 3

Multiple selection: bold, italic and underline switch on independently of each other.

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 "togglegroup-002" (Format Group) 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/togglegroup-002.json

Registry item: https://vibeui.ru/r/togglegroup-002.json
Installs to: components/vibeui/togglegroup-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
A multi-select toggle group: type styles stack, so any number of buttons can be pressed at once and the sample updates immediately.

A multi-select toggle group: three type styles switch on independently and apply to the sample at once. One file, zero dependencies.

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

<Togglegroup002
  label="Text style"
  defaultValue={["bold"]}
  onChange={(value) => console.log(value)}
/>

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-togglegroup-002-* palette — do not swap it for your theme tokens
- independent aria-pressed on every button: single selection here would be a lie
- the counter of selected items: pointless with three buttons, essential with ten
- the instant effect on the sample — the group drives the view, it does not fill a form
- the visible focus ring on every button
- the panel's own light surface: dark text must bring its own background

## 6. You may change
- the group name through label
- the initial set through defaultValue
- the sample copy through text
- the pressed colour through accent

## 7. Rules
- onChange returns an array of ids, not a string: their order is the order of presses.
- For mutually exclusive options use a single-select group; there is none here.
- Three type styles at once read badly — that is the editor's problem, not the component's.
- 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/togglegroup-002.json
https://vibeui.ru/r/togglegroup-002.json

Один файл, ноль зависимостей, палитра --vibeui-togglegroup-002-*. Клиентский: useState хранит массив выбранных идентификаторов. Множественность — это и есть причина, по которой здесь aria-pressed, а не radio: три состояния включаются независимо. Подсветка не выбирает «одну активную», а следует за собственным aria-pressed каждой кнопки; образец получает начертания через data-bold, data-italic и data-underline на корне. Отличие от buttongroup: там кнопки выполняют действия и после нажатия ничего не помнят, здесь нажатие — это включённое состояние.

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 Togglegroup002Props = Omit<
  ComponentPropsWithoutRef<"section">,
  "children" | "onChange"
> & {
  label?: string
  defaultValue?: string[]
  text?: string
  onChange?: (value: string[]) => void
  accent?: string
}

// Идея компонента: та же группа toggle-кнопок, но с множественным выбором.
// Начертания складываются, поэтому нажатых кнопок может быть сколько угодно —
// и это ровно та причина, по которой здесь aria-pressed, а не radio.
const STYLES = `
:where([data-vibeui-block="togglegroup-002"]){
--vibeui-togglegroup-002-bg:oklch(1 0 0);
--vibeui-togglegroup-002-fg:oklch(0.22 0.014 265);
--vibeui-togglegroup-002-muted:oklch(0.55 0.014 265);
--vibeui-togglegroup-002-border:oklch(0.9 0.006 265);
--vibeui-togglegroup-002-surface:oklch(0.97 0.004 265);
--vibeui-togglegroup-002-accent:oklch(0.5 0.02 265);
--vibeui-togglegroup-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="togglegroup-002"]{
box-sizing:border-box;display:flex;flex-direction:column;gap:0.75rem;
width:100%;max-width:24rem;padding:0.875rem;
border:1px solid var(--vibeui-togglegroup-002-border);border-radius:0.875rem;
background:var(--vibeui-togglegroup-002-bg);color:var(--vibeui-togglegroup-002-fg);
font-family:var(--vibeui-togglegroup-002-font);
}
[data-vibeui-block="togglegroup-002"] *{box-sizing:border-box}
[data-vibeui-block="togglegroup-002"] [data-part="head"]{
display:flex;align-items:center;justify-content:space-between;gap:0.75rem;
}
[data-vibeui-block="togglegroup-002"] [data-part="group"]{
display:inline-flex;gap:0.25rem;
}
[data-vibeui-block="togglegroup-002"] button{
appearance:none;cursor:pointer;font:inherit;
display:inline-flex;align-items:center;justify-content:center;
width:2.125rem;height:2.125rem;padding:0;
border:1px solid var(--vibeui-togglegroup-002-border);border-radius:0.5rem;
background:var(--vibeui-togglegroup-002-bg);color:var(--vibeui-togglegroup-002-muted);
transition:background-color .15s ease,color .15s ease,border-color .15s ease;
}
[data-vibeui-block="togglegroup-002"] button svg{width:1rem;height:1rem}
[data-vibeui-block="togglegroup-002"] button:hover{background:var(--vibeui-togglegroup-002-surface)}
[data-vibeui-block="togglegroup-002"] button:focus-visible{
outline:2px solid var(--vibeui-togglegroup-002-accent);outline-offset:2px;
}
/* Каждая кнопка независима: нажатых может быть три сразу, поэтому подсветка
   не выбирает «одну активную», а просто следует за своим aria-pressed. */
[data-vibeui-block="togglegroup-002"] button[aria-pressed="true"]{
background:var(--vibeui-togglegroup-002-accent);
border-color:var(--vibeui-togglegroup-002-accent);
color:oklch(0.99 0 0);
}
[data-vibeui-block="togglegroup-002"] [data-part="count"]{
margin:0;font-size:0.75rem;color:var(--vibeui-togglegroup-002-muted);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="togglegroup-002"] [data-part="sample"]{
margin:0;padding:0.75rem;border-radius:0.625rem;
background:var(--vibeui-togglegroup-002-surface);
font-size:0.9375rem;line-height:1.55;
font-weight:400;font-style:normal;text-decoration:none;
}
[data-vibeui-block="togglegroup-002"][data-bold="true"] [data-part="sample"]{font-weight:700}
[data-vibeui-block="togglegroup-002"][data-italic="true"] [data-part="sample"]{font-style:italic}
[data-vibeui-block="togglegroup-002"][data-underline="true"] [data-part="sample"]{
text-decoration:underline;text-underline-offset:0.2em;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="togglegroup-002"] *{animation:none!important;transition:none!important}}
`

const OPTIONS = [
  {
    id: "bold",
    label: "Полужирный",
    d: "M5.4 2.8h4.2a2.9 2.9 0 0 1 0 5.8H5.4zm0 5.8h4.9a3.1 3.1 0 0 1 0 6.2H5.4z",
    fill: true,
  },
  {
    id: "italic",
    label: "Курсив",
    d: "M11.5 3h-4M9 13H5M9.8 3 7 13",
    fill: false,
  },
  {
    id: "underline",
    label: "Подчёркнутый",
    d: "M4.6 2.8v5.4a3.4 3.4 0 0 0 6.8 0V2.8M3.6 14.4h8.8",
    fill: false,
  },
]

/**
 * Группа toggle-кнопок с множественным выбором: начертания складываются
 * и применяются к образцу сразу. Один файл, ноль зависимостей.
 */
export function Togglegroup002({
  label = "Начертание текста",
  defaultValue = ["bold"],
  text = "Множественный выбор: жирный, курсив и подчёркивание включаются независимо друг от друга.",
  onChange,
  accent,
  className,
  style,
  ...props
}: Togglegroup002Props) {
  const [value, setValue] = useState<string[]>(defaultValue)

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

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

    setValue(next)
    onChange?.(next)
  }

  return (
    <>
      <style href="vibeui-togglegroup-002" precedence="medium">
        {STYLES}
      </style>
      <section
        {...props}
        data-vibeui-block="togglegroup-002"
        data-bold={value.includes("bold")}
        data-italic={value.includes("italic")}
        data-underline={value.includes("underline")}
        className={className}
        style={palette}
      >
        <div data-part="head">
          <div data-part="group" role="group" aria-label={label}>
            {OPTIONS.map((option) => (
              <button
                key={option.id}
                type="button"
                aria-pressed={value.includes(option.id)}
                aria-label={option.label}
                title={option.label}
                onClick={() => toggle(option.id)}
              >
                <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
                  <path
                    d={option.d}
                    fill={option.fill ? "currentColor" : "none"}
                    stroke="currentColor"
                    strokeWidth={option.fill ? 0 : 1.4}
                    strokeLinecap="round"
                  />
                </svg>
              </button>
            ))}
          </div>
          <p data-part="count" role="status">
            Выбрано: {value.length} из {OPTIONS.length}
          </p>
        </div>
        <p data-part="sample">{text}</p>
      </section>
    </>
  )
}