Buttons
Tooltip Icon Button
Кнопка со значком, у которой подпись всё-таки есть: одна строка уходит и в aria-label, и в подсказку, поэтому имя действия не может разойтись.
- button
- icon
- tooltip
- a11y
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/button-028
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «button-028» (Tooltip Icon Button) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/button-028.json
Registry item: https://vibeui.ru/r/button-028.json
Ставится в: components/vibeui/button-028.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Кнопка со значком, у которой подпись всё-таки есть: одна строка уходит и в aria-label, и в подсказку, поэтому имя действия не может разойтись.
Иконочная кнопка с подсказкой на CSS. Один проп label уходит сразу в aria-label и в видимый пузырёк, поэтому имя действия физически одно и то же для скринридера и для глаза. По наведению подсказка появляется с задержкой, по клавиатуре — мгновенно. Значки нарисованы бордюрами, иконочный пакет не нужен.
## 3. Как использовать
import { Button028 } from "@/components/vibeui/button-028"
<Button028 label="Удалить черновик" icon="trash" side="top" />
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- единственный источник имени: label попадает и в aria-label, и в подсказку — разъехаться им негде
- aria-hidden на пузырьке: иначе скринридер прочитает имя действия дважды
- показ подсказки на focus-visible, а не только на hover: с клавиатуры значок иначе безымянный
- задержку 0.35s для мыши и нулевую для клавиатуры — без задержки подсказки вспыхивают при проходе курсора над рядом кнопок
- pointer-events:none у пузырька: подсказка не должна перехватывать клик по кнопке
- размер кнопки не меньше 2.25rem и видимое фокусное кольцо: у маленькой цели они важнее всего
## 6. Можно менять
- имя действия — проп label
- значок — проп icon: info, trash или edit
- сторону подсказки — проп side
- акцентный цвет — проп accent
- внешние отступы через className
## 7. Правила
- Подсказка не заменяет подпись в интерфейсе, который смотрят с телефона: на тач-экране наведения нет, поэтому важное действие лучше подписать словами.
- Держи label коротким: пузырёк не переносится по строкам и на длинной фразе вылезет за края узкой панели.
- Пузырёк рисуется внутри потока: если у родителя стоит overflow:hidden, подсказку срежет — тогда меняй side или убирай обрезку.
- Не выноси CSS-переменные в globals.css: компонент должен оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/button-028.jsonhttps://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 собраны из бордюров и псевдоэлементов.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
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>
</>
)
}