Button Group

Icon Action Group

A group of bare icons where every button still has a name: the caption sits as real text for screen readers, and a visible tooltip appears on hover and on focus.

  • buttongroup
  • icons
  • tooltip
  • 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-006?lang=en

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-006" (Icon Action 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/buttongroup-006.json

Registry item: https://vibeui.ru/r/buttongroup-006.json
Installs to: components/vibeui/buttongroup-006.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 of bare icons where every button still has a name: the caption sits as real text for screen readers, and a visible tooltip appears on hover and on focus.

A cluster of icon-only buttons, each carrying a real text name and a tooltip. Zero dependencies, one file, a server component.

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

<Buttongroup006
  actions={[{ id: "undo", label: "Undo" }, { id: "copy", label: "Copy" }]}
  label="Quick actions"
  shape="square"
/>

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-006-* palette — do not swap it for your theme tokens
- the text name inside the button under clip-path: unlike aria-label, find-in-page can reach it
- aria-hidden on the visible tooltip — otherwise the screen reader announces the name twice
- showing the tooltip on :focus-visible, not just :hover: from the keyboard the icons are otherwise unreadable
- the z-index on focus: with collapsed borders the neighbouring button clips the ring
- a button no smaller than 2.25rem — the touch target is already at its limit

## 6. You may change
- the buttons and their names through actions
- the accessible group name through label
- the shape of the edge buttons through shape
- the focus ring colour through accent

## 7. Rules
- Icons live as paths in the ICONS map: an unknown id silently falls back to the "copy" path.
- The tooltip pops upwards and gets clipped if the group sits at the top edge of a scrolling container.
- Bare icons are recognised worse: for rare actions add text or move them into a menu.
- 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-006.json
https://vibeui.ru/r/buttongroup-006.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в переменных --vibeui-buttongroup-006-*. Серверный: хуков нет. Имя кнопки — настоящий текст внутри неё, спрятанный clip-path:inset(50%): его читает скринридер и находит поиск по странице, в отличие от aria-label. Видимая подсказка — отдельный aria-hidden узел, иначе имя прочиталось бы дважды; показывается она и на :hover, и на :focus-visible. Границы схлопнуты через margin-inline-start:-1px, скругления заданы логическими свойствами крайним кнопкам, а форму переключает атрибут data-shape. При фокусе кнопка поднимается 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 Buttongroup006Action = {
  id: string
  label: string
}

export type Buttongroup006Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  actions?: Buttongroup006Action[]
  label?: string
  shape?: "square" | "round"
  accent?: string
}

// Идея компонента: группа из одних значков, у которой имя есть у каждой
// кнопки. Подпись лежит внутри кнопки текстом и спрятана clip-path — так её
// читает скринридер и находит поиск по странице. Видимая подсказка над
// значком — отдельный aria-hidden узел, чтобы имя не прочиталось дважды.
// Показывается она и по наведению, и по :focus-visible: с клавиатуры набор
// безымянных значков иначе неразличим.
const STYLES = `
:where([data-vibeui-block="buttongroup-006"]){
--vibeui-buttongroup-006-surface:oklch(1 0 0);
--vibeui-buttongroup-006-fg:oklch(0.27 0.016 265);
--vibeui-buttongroup-006-muted:oklch(0.53 0.014 265);
--vibeui-buttongroup-006-border:oklch(0.88 0.008 265);
--vibeui-buttongroup-006-hover:oklch(0.96 0.004 265);
--vibeui-buttongroup-006-accent:oklch(0.55 0.17 265);
--vibeui-buttongroup-006-tip:oklch(0.24 0.016 265);
--vibeui-buttongroup-006-radius:0.625rem;
--vibeui-buttongroup-006-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="buttongroup-006"]{
box-sizing:border-box;display:inline-flex;isolation:isolate;
font-family:var(--vibeui-buttongroup-006-font);
}
[data-vibeui-block="buttongroup-006"] *{box-sizing:border-box}
[data-vibeui-block="buttongroup-006"][data-shape="round"]{--vibeui-buttongroup-006-radius:9999px}
[data-vibeui-block="buttongroup-006"] button{
appearance:none;cursor:pointer;font:inherit;
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-006-border);
background:var(--vibeui-buttongroup-006-surface);
color:var(--vibeui-buttongroup-006-muted);
transition:background-color .16s ease,color .16s ease;
}
[data-vibeui-block="buttongroup-006"] button:first-child{
margin-inline-start:0;
border-start-start-radius:var(--vibeui-buttongroup-006-radius);
border-end-start-radius:var(--vibeui-buttongroup-006-radius);
}
[data-vibeui-block="buttongroup-006"] button:last-child{
border-start-end-radius:var(--vibeui-buttongroup-006-radius);
border-end-end-radius:var(--vibeui-buttongroup-006-radius);
}
[data-vibeui-block="buttongroup-006"] button:hover{
z-index:1;
background:var(--vibeui-buttongroup-006-hover);
color:var(--vibeui-buttongroup-006-fg);
}
/* Обводка поднята над соседями: границы схлопнуты, и без z-index её левый
   край уходит под следующую кнопку. */
[data-vibeui-block="buttongroup-006"] button:focus-visible{
z-index:2;
outline:2px solid var(--vibeui-buttongroup-006-accent);outline-offset:1px;
color:var(--vibeui-buttongroup-006-fg);
}
[data-vibeui-block="buttongroup-006"] svg{width:1.0625rem;height:1.0625rem}
/* Имя кнопки: остаётся текстом в доступном дереве, но не занимает места. */
[data-vibeui-block="buttongroup-006"] [data-part="name"]{
position:absolute;width:1px;height:1px;overflow:hidden;
clip-path:inset(50%);white-space:nowrap;
}
[data-vibeui-block="buttongroup-006"] [data-part="tip"]{
position:absolute;bottom:calc(100% + 0.4375rem);left:50%;translate:-50% 0;
padding:0.1875rem 0.4375rem;border-radius:0.375rem;
background:var(--vibeui-buttongroup-006-tip);color:oklch(0.99 0 0);
font-size:0.6875rem;font-weight:600;line-height:1.3;white-space:nowrap;
opacity:0;pointer-events:none;
transition:opacity .14s ease;
}
[data-vibeui-block="buttongroup-006"] button:hover [data-part="tip"],
[data-vibeui-block="buttongroup-006"] button:focus-visible [data-part="tip"]{opacity:1}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="buttongroup-006"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ACTIONS: Buttongroup006Action[] = [
  { id: "undo", label: "Отменить" },
  { id: "redo", label: "Повторить" },
  { id: "copy", label: "Скопировать" },
  { id: "share", label: "Поделиться" },
]

const ICONS: Record<string, string> = {
  undo: "M7 5 3.5 8.5 7 12M3.5 8.5h7.5a4 4 0 0 1 0 8H8",
  redo: "M12 5l3.5 3.5L12 12m3.5-3.5H8a4 4 0 0 0 0 8h3",
  copy: "M6.5 6.5V4.2c0-.4.3-.7.7-.7h7.6c.4 0 .7.3.7.7v7.6c0 .4-.3.7-.7.7h-2.3M3.5 7.9c0-.4.3-.7.7-.7h7.6c.4 0 .7.3.7.7v7.6c0 .4-.3.7-.7.7H4.2a.7.7 0 0 1-.7-.7z",
  share:
    "M9.5 12.5v-9m0 0L6.3 6.7M9.5 3.5l3.2 3.2M4 11v4.3c0 .4.3.7.7.7h9.6c.4 0 .7-.3.7-.7V11",
}

/**
 * Группа значков без подписей, но с именем у каждой кнопки.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Buttongroup006({
  actions = DEFAULT_ACTIONS,
  label = "Быстрые действия",
  shape = "square",
  accent,
  className,
  style,
  ...props
}: Buttongroup006Props) {
  const palette = {
    ...(accent ? { "--vibeui-buttongroup-006-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-buttongroup-006" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="buttongroup-006"
        data-shape={shape}
        role="group"
        aria-label={label}
        className={className}
        style={palette}
      >
        {actions.map((action) => (
          <button key={action.id} type="button">
            <svg viewBox="0 0 19 19" fill="none" aria-hidden="true">
              <path
                d={ICONS[action.id] ?? ICONS.copy}
                stroke="currentColor"
                strokeWidth="1.5"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
            <span data-part="name">{action.label}</span>
            <span data-part="tip" aria-hidden="true">
              {action.label}
            </span>
          </button>
        ))}
      </div>
    </>
  )
}