Button Group

Text Style Toggles

Text styles as a multi-select on checkboxes: each letter is drawn in the very style it turns on and serves as its own sample.

  • buttongroup
  • editor
  • checkbox
  • typography

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

Text style
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-024" (Text Style Toggles) 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-024.json

Registry item: https://vibeui.ru/r/buttongroup-024.json
Installs to: components/vibeui/buttongroup-024.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
Text styles as a multi-select on checkboxes: each letter is drawn in the very style it turns on and serves as its own sample.

A multi-select of text styles where every letter is drawn in its own style. Zero dependencies, one file, a server component.

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

<Buttongroup024
  styles={[{ id: "bold", glyph: "B", label: "Bold" }]}
  defaultValue={["bold"]}
  label="Text style"
  name="style"
/>

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-024-* palette — do not swap it for your theme tokens
- checkboxes instead of radios: styles combine, and deselection has to work
- the letter drawn in its own style — the sample explains the action faster than any caption
- the text name under clip-path: a single letter means nothing to a screen reader
- the z-index on focus: with collapsed borders the neighbour clips the ring
- a button no smaller than 2.25rem — the touch target is already at its limit

## 6. You may change
- the styles, their letters and names through styles
- the initially enabled styles through defaultValue
- the accessible group name through label
- the form field prefix through name and the fill 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.
- The letters depend on the UI language: in English they are B, I, U, S and have to be passed through styles.
- The component formats nothing: applying it to the text is your editor's job.
- 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-024.json
https://vibeui.ru/r/buttongroup-024.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в переменных --vibeui-buttongroup-024-*. Серверный: внутри checkbox, а не radio — начертания сочетаются, браузер сам объявит «флажок, установлен», и снять выбор можно повторным нажатием. Каждая буква получает data-glyph и рисуется своим начертанием: Ж полужирная, К курсивная, П подчёркнутая, З зачёркнутая — образец и есть подпись. Настоящее имя лежит рядом под clip-path, потому что одна буква ничего не скажет скринридеру. Границы схлопнуты через margin-inline-start:-1px, скругления только крайним, включённый тумблер заливается тёмным, сфокусированный поднимается 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 Buttongroup024Style = {
  id: string
  glyph: string
  label: string
}

export type Buttongroup024Props = Omit<
  ComponentPropsWithoutRef<"fieldset">,
  "children"
> & {
  styles?: Buttongroup024Style[]
  defaultValue?: string[]
  label?: string
  name?: string
  accent?: string
}

// Идея компонента: множественный выбор там, где сцепка обычно означает
// «один из». Начертания сочетаются, поэтому внутри лежат checkbox, а не
// radio: браузер сам объявит «флажок, установлен», и снять выбор можно
// повторным нажатием — с radio это было бы невозможно. Каждая буква
// нарисована тем начертанием, которое включает: Ж полужирная, К курсивная,
// П подчёркнутая — образец и есть подпись. Настоящее имя лежит рядом под
// clip-path, потому что одна буква ничего не скажет скринридеру.
const STYLES = `
:where([data-vibeui-block="buttongroup-024"]){
--vibeui-buttongroup-024-surface:oklch(1 0 0);
--vibeui-buttongroup-024-fg:oklch(0.24 0.016 265);
--vibeui-buttongroup-024-muted:oklch(0.55 0.014 265);
--vibeui-buttongroup-024-border:oklch(0.88 0.008 265);
--vibeui-buttongroup-024-accent:oklch(0.28 0.03 265);
--vibeui-buttongroup-024-on-accent:oklch(0.99 0.002 265);
--vibeui-buttongroup-024-radius:0.5rem;
--vibeui-buttongroup-024-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="buttongroup-024"]{
box-sizing:border-box;display:inline-block;
margin:0;padding:0;border:0;
font-family:var(--vibeui-buttongroup-024-font);
}
[data-vibeui-block="buttongroup-024"] *{box-sizing:border-box}
[data-vibeui-block="buttongroup-024"] legend{
position:absolute;width:1px;height:1px;padding:0;margin:-1px;
overflow:hidden;clip-path:inset(50%);white-space:nowrap;
}
[data-vibeui-block="buttongroup-024"] [data-part="track"]{display:flex;isolation:isolate}
[data-vibeui-block="buttongroup-024"] [data-part="toggle"]{
position:relative;z-index:0;
display:inline-flex;align-items:center;justify-content:center;
width:2.25rem;height:2.25rem;margin-inline-start:-1px;
border:1px solid var(--vibeui-buttongroup-024-border);
background:var(--vibeui-buttongroup-024-surface);
color:var(--vibeui-buttongroup-024-muted);
cursor:pointer;
transition:background-color .16s ease,color .16s ease,border-color .16s ease;
}
[data-vibeui-block="buttongroup-024"] [data-part="toggle"]:first-child{
margin-inline-start:0;
border-start-start-radius:var(--vibeui-buttongroup-024-radius);
border-end-start-radius:var(--vibeui-buttongroup-024-radius);
}
[data-vibeui-block="buttongroup-024"] [data-part="toggle"]:last-child{
border-start-end-radius:var(--vibeui-buttongroup-024-radius);
border-end-end-radius:var(--vibeui-buttongroup-024-radius);
}
[data-vibeui-block="buttongroup-024"] input{
position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer;
}
[data-vibeui-block="buttongroup-024"] [data-part="name"]{
position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);
}
/* Буква нарисована тем начертанием, которое включает. */
[data-vibeui-block="buttongroup-024"] [data-part="glyph"]{
font-size:0.9375rem;line-height:1;
}
[data-vibeui-block="buttongroup-024"] [data-glyph="bold"]{font-weight:800}
[data-vibeui-block="buttongroup-024"] [data-glyph="italic"]{font-style:italic;font-weight:600}
[data-vibeui-block="buttongroup-024"] [data-glyph="underline"]{
font-weight:600;text-decoration:underline;text-underline-offset:0.1875rem;
}
[data-vibeui-block="buttongroup-024"] [data-glyph="strike"]{font-weight:600;text-decoration:line-through}
[data-vibeui-block="buttongroup-024"] [data-part="toggle"]:hover{color:var(--vibeui-buttongroup-024-fg)}
[data-vibeui-block="buttongroup-024"] [data-part="toggle"]:has(input:checked){
z-index:1;
background:var(--vibeui-buttongroup-024-accent);
border-color:var(--vibeui-buttongroup-024-accent);
color:var(--vibeui-buttongroup-024-on-accent);
}
[data-vibeui-block="buttongroup-024"] [data-part="toggle"]:has(input:focus-visible){
z-index:2;
outline:2px solid var(--vibeui-buttongroup-024-accent);outline-offset:2px;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="buttongroup-024"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_STYLES: Buttongroup024Style[] = [
  { id: "bold", glyph: "Ж", label: "Полужирный" },
  { id: "italic", glyph: "К", label: "Курсив" },
  { id: "underline", glyph: "П", label: "Подчёркнутый" },
  { id: "strike", glyph: "З", label: "Зачёркнутый" },
]

/**
 * Начертания текста множественным выбором на checkbox: буква и есть образец.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Buttongroup024({
  styles = DEFAULT_STYLES,
  defaultValue = ["bold"],
  label = "Начертание",
  name = "buttongroup-024",
  accent,
  className,
  style,
  ...props
}: Buttongroup024Props) {
  const palette = {
    ...(accent ? { "--vibeui-buttongroup-024-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-buttongroup-024" precedence="medium">
        {STYLES}
      </style>
      <fieldset
        {...props}
        data-vibeui-block="buttongroup-024"
        className={className}
        style={palette}
      >
        <legend>{label}</legend>
        <div data-part="track">
          {styles.map((item) => (
            <label key={item.id} data-part="toggle">
              <input
                type="checkbox"
                name={`${name}-${item.id}`}
                value={item.id}
                defaultChecked={defaultValue.includes(item.id)}
              />
              <span data-part="glyph" data-glyph={item.id} aria-hidden="true">
                {item.glyph}
              </span>
              <span data-part="name">{item.label}</span>
            </label>
          ))}
        </div>
      </fieldset>
    </>
  )
}