Buttons

Tooltip Icon Button

An icon button that still has a caption: one string feeds both the aria-label and the tooltip, so the name of the action cannot drift apart.

  • button
  • icon
  • 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/button-028?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 "button-028" (Tooltip Icon Button) 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/button-028.json

Registry item: https://vibeui.ru/r/button-028.json
Installs to: components/vibeui/button-028.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
An icon button that still has a caption: one string feeds both the aria-label and the tooltip, so the name of the action cannot drift apart.

An icon button with a CSS-only tooltip. A single label prop feeds both the aria-label and the visible bubble, so the screen reader and the eye get literally the same name. On hover the tooltip waits, on keyboard focus it appears at once. The glyphs are drawn with borders — no icon package needed.

## 3. How to use it
import { Button028 } from "@/components/vibeui/button-028"

<Button028 label="Delete draft" icon="trash" side="top" />

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 single source of the name: label lands in both the aria-label and the tooltip, with nowhere to drift
- aria-hidden on the bubble: otherwise the screen reader announces the action name twice
- the tooltip on focus-visible, not only on hover: from the keyboard the glyph would otherwise be nameless
- the 0.35s delay for the mouse and none for the keyboard — without it tooltips flash as the cursor crosses a row of buttons
- pointer-events:none on the bubble: a tooltip must not intercept the click meant for the button
- the button no smaller than 2.25rem and the visible focus ring: on a small target they matter most

## 6. You may change
- the action name through the label prop
- the glyph through the icon prop: info, trash or edit
- the tooltip side through the side prop
- the accent colour through the accent prop
- outer spacing through className

## 7. Rules
- A tooltip does not replace a caption in an interface used on a phone: there is no hover on a touch screen, so an important action deserves words.
- Keep label short: the bubble does not wrap and a long phrase escapes the edges of a narrow toolbar.
- The bubble is drawn in flow: a parent with overflow:hidden will clip it — change side or drop the clipping.
- 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/button-028.json
https://vibeui.ru/r/button-028.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-button-028-*. Серверный: хуков нет, подсказка целиком на CSS. Показ идёт по соседнему селектору button:hover + [data-part="tip"] и button:focus-visible + [data-part="tip"]: по мыши с задержкой 0.35s, по клавиатуре сразу. Сторона задаётся data-side на корне. Значки info, trash и edit собраны из бордюров и псевдоэлементов.

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 Button028Props = Omit<
  ComponentPropsWithoutRef<"button">,
  "children"
> & {
  /** Имя действия. Оно же уходит в aria-label и в подсказку. */
  label?: string
  icon?: "info" | "trash" | "edit"
  side?: "top" | "bottom"
  accent?: string
}

// Идея компонента: кнопка со значком, у которой подпись всё-таки есть — она
// живёт в подсказке. Одна строка label уходит и в aria-label, и в видимый
// пузырёк, поэтому имя действия физически не может разойтись между
// скринридером и глазом. Подсказка на CSS: по наведению с задержкой,
// по клавиатуре — сразу, без задержки и без JS.
const STYLES = `
:where([data-vibeui-block="button-028"]){
--vibeui-button-028-bg:oklch(1 0 0);
--vibeui-button-028-fg:oklch(0.32 0.016 265);
--vibeui-button-028-border:oklch(0.9 0.006 265);
--vibeui-button-028-accent:oklch(0.55 0.17 265);
--vibeui-button-028-tip:oklch(0.24 0.02 265);
--vibeui-button-028-tip-fg:oklch(0.98 0.005 265);
--vibeui-button-028-size:2.25rem;
--vibeui-button-028-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="button-028"]{
position:relative;display:inline-flex;
font-family:var(--vibeui-button-028-font);
}
[data-vibeui-block="button-028"] button{
appearance:none;cursor:pointer;
display:inline-flex;align-items:center;justify-content:center;
width:var(--vibeui-button-028-size);height:var(--vibeui-button-028-size);
padding:0;box-sizing:border-box;
border:1px solid var(--vibeui-button-028-border);border-radius:0.625rem;
background:var(--vibeui-button-028-bg);color:var(--vibeui-button-028-fg);
transition:color .16s ease,border-color .16s ease;
}
[data-vibeui-block="button-028"] button:hover{color:var(--vibeui-button-028-accent);border-color:var(--vibeui-button-028-accent)}
[data-vibeui-block="button-028"] button:focus-visible{outline:2px solid var(--vibeui-button-028-accent);outline-offset:2px}
/* Подсказка: по мыши с задержкой, по клавиатуре — сразу. */
[data-vibeui-block="button-028"] [data-part="tip"]{
position:absolute;left:50%;z-index:2;
padding:0.3125rem 0.5rem;border-radius:0.375rem;
background:var(--vibeui-button-028-tip);color:var(--vibeui-button-028-tip-fg);
font-size:0.75rem;font-weight:550;line-height:1.2;white-space:nowrap;
opacity:0;pointer-events:none;
transform:translate(-50%,0.25rem);
transition:opacity .14s ease,transform .14s ease;
transition-delay:0s;
}
[data-vibeui-block="button-028"][data-side="top"] [data-part="tip"]{bottom:calc(100% + 0.4375rem)}
[data-vibeui-block="button-028"][data-side="bottom"] [data-part="tip"]{top:calc(100% + 0.4375rem);transform:translate(-50%,-0.25rem)}
[data-vibeui-block="button-028"] [data-part="tip"]::after{
content:"";position:absolute;left:50%;width:0.5rem;height:0.5rem;
margin-left:-0.25rem;background:inherit;transform:rotate(45deg);
}
[data-vibeui-block="button-028"][data-side="top"] [data-part="tip"]::after{top:calc(100% - 0.25rem)}
[data-vibeui-block="button-028"][data-side="bottom"] [data-part="tip"]::after{bottom:calc(100% - 0.25rem)}
[data-vibeui-block="button-028"] button:hover + [data-part="tip"]{
opacity:1;transform:translate(-50%,0);transition-delay:.35s;
}
[data-vibeui-block="button-028"] button:focus-visible + [data-part="tip"]{
opacity:1;transform:translate(-50%,0);transition-delay:0s;
}
[data-vibeui-block="button-028"] [data-part="glyph"]{position:relative;display:block}
[data-vibeui-block="button-028"] [data-icon="info"]{
width:1.0625rem;height:1.0625rem;box-sizing:border-box;
border:1.5px solid currentColor;border-radius:9999px;
}
[data-vibeui-block="button-028"] [data-icon="info"]::before{
content:"";position:absolute;left:50%;top:0.1875rem;width:2px;height:2px;
margin-left:-1px;border-radius:1px;background:currentColor;
}
[data-vibeui-block="button-028"] [data-icon="info"]::after{
content:"";position:absolute;left:50%;top:0.4375rem;width:2px;height:0.375rem;
margin-left:-1px;border-radius:1px;background:currentColor;
}
[data-vibeui-block="button-028"] [data-icon="trash"]{
width:0.75rem;height:0.8125rem;margin-top:0.1875rem;box-sizing:border-box;
border:1.5px solid currentColor;border-top:0;border-radius:0 0 0.1875rem 0.1875rem;
}
[data-vibeui-block="button-028"] [data-icon="trash"]::before{
content:"";position:absolute;left:-0.125rem;top:-0.1875rem;width:1rem;height:1.5px;
border-radius:1px;background:currentColor;
}
[data-vibeui-block="button-028"] [data-icon="trash"]::after{
content:"";position:absolute;left:0.25rem;top:-0.375rem;width:0.25rem;height:1.5px;
border-radius:1px;background:currentColor;
}
[data-vibeui-block="button-028"] [data-icon="edit"]{
width:0.3125rem;height:0.8125rem;box-sizing:border-box;
border:1.5px solid currentColor;border-bottom:0;border-radius:0.125rem 0.125rem 0 0;
transform:rotate(45deg);
}
[data-vibeui-block="button-028"] [data-icon="edit"]::after{
content:"";position:absolute;left:-1.5px;top:100%;
border:0.1875rem solid transparent;border-top-color:currentColor;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="button-028"] *{animation:none!important;transition:none!important}}
`

/**
 * Кнопка со значком без подписи: имя действия живёт в aria-label и подсказке.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Button028({
  label = "Удалить черновик",
  icon = "trash",
  side = "top",
  accent,
  type = "button",
  className,
  style,
  ...props
}: Button028Props) {
  const palette = {
    ...(accent ? { "--vibeui-button-028-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-button-028" precedence="medium">
        {STYLES}
      </style>
      <span
        data-vibeui-block="button-028"
        data-side={side}
        className={className}
        style={palette}
      >
        <button {...props} type={type} aria-label={label}>
          <span data-part="glyph" data-icon={icon} />
        </button>
        {/* Подсказка — украшение: имя действия уже есть в aria-label. */}
        <span data-part="tip" aria-hidden="true">
          {label}
        </span>
      </span>
    </>
  )
}