← каталог
Button Styles
Инвентарь кнопок для билдера: ВИД (solid, outline, ghost, soft, gradient, glow) и ДВИЖЕНИЕ (lift, press, блик, заливка слева, стрелка) — независимые оси, любая пара легальна. Чистый CSS, focus-visible получает то же движение, что и hover.
csshoverclickbasic0reduced-motion ✓
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| variant? | ButtonVariant | "solid" | Вид: solid | outline | ghost | soft | gradient | glow. |
| motion? | ButtonMotion | "lift" | Движение по наведению: none | lift | press | sheen | fill | arrow. |
| children | ReactNode | — |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/button-styles.jsonЧто нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
import type { ButtonHTMLAttributes, ReactNode } from "react";
import "./button-styles.css";
export type ButtonVariant = "solid" | "outline" | "ghost" | "soft" | "gradient" | "glow";
export type ButtonMotion = "none" | "lift" | "press" | "sheen" | "fill" | "arrow";
export interface StyledButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Вид: solid | outline | ghost | soft | gradient | glow. */
variant?: ButtonVariant;
/** Движение по наведению: none | lift | press | sheen | fill | arrow. */
motion?: ButtonMotion;
children: ReactNode;
}
/**
* Инвентарь кнопок для билдера: ВИД и ДВИЖЕНИЕ — две независимые оси, любая пара легальна.
* Вся анимация в CSS (transform/opacity + цвета), состояния focus-visible получают то же
* движение, что и hover, — клавиатура не хуже мыши. Токены с фолбэком: с дизайн-системой
* кнопка слушается темы, без неё продолжает работать.
*/
export function ButtonStyles({ variant = "solid", motion = "lift", children, className, ...props }: StyledButtonProps) {
return (
<button
className={["msb", `msb--${variant}`, motion !== "none" ? `msb-m--${motion}` : "", className ?? ""]
.filter(Boolean)
.join(" ")}
{...props}
>
{children}
{motion === "arrow" && (
<span className="msb-tail" aria-hidden="true">
→
</span>
)}
</button>
);
}
/* Инвентарь кнопок: ВИД (variant) и ДВИЖЕНИЕ (motion) — независимые оси, любая пара
легальна. Билдер подбирает стиль, комбинируя классы msb--<вид> и msb-m--<движение>. */
.msb {
--msb-accent: var(--ds-accent, oklch(0.72 0.16 300));
--msb-accent-fg: var(--ds-accent-fg, oklch(0.99 0.01 300));
--msb-fg: var(--ds-fg, oklch(0.92 0.01 265));
--msb-border: var(--ds-border, oklch(0.3 0.02 275));
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
padding: 0.72em 1.6em;
border: 1px solid transparent;
border-radius: var(--ds-radius-pill, 999px);
font: inherit;
font-weight: 600;
font-size: 0.95rem;
line-height: 1.1;
color: var(--msb-fg);
background: none;
cursor: pointer;
overflow: hidden;
isolation: isolate;
transition:
color var(--ds-fast, 0.18s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
background-color var(--ds-fast, 0.18s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
border-color var(--ds-fast, 0.18s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
box-shadow var(--ds-base, 0.32s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
transform var(--ds-fast, 0.18s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}
.msb:focus-visible {
outline: 2px solid var(--msb-accent);
outline-offset: 3px;
}
/* ---- вид ---- */
.msb--solid {
background: var(--msb-accent);
color: var(--msb-accent-fg);
}
.msb--solid:hover {
background: color-mix(in oklab, var(--msb-accent) 86%, var(--msb-accent-fg));
}
.msb--outline {
border-color: var(--msb-border);
}
.msb--outline:hover {
border-color: var(--msb-accent);
color: var(--msb-accent);
}
.msb--ghost:hover {
background: color-mix(in oklab, var(--msb-fg) 8%, transparent);
}
.msb--soft {
background: color-mix(in oklab, var(--msb-accent) 14%, transparent);
color: var(--msb-accent);
}
.msb--soft:hover {
background: color-mix(in oklab, var(--msb-accent) 22%, transparent);
}
.msb--gradient {
background: linear-gradient(
115deg,
var(--msb-accent),
color-mix(in oklch longer hue, var(--msb-accent) 55%, oklch(0.7 0.15 220))
)
0 0 / 200% 100%;
color: var(--msb-accent-fg);
transition-property: background-position, color, box-shadow, transform;
transition-duration: var(--ds-slow, 0.6s);
}
.msb--gradient:hover {
background-position: 100% 0;
}
.msb--glow {
background: var(--msb-accent);
color: var(--msb-accent-fg);
box-shadow: 0 0 22px -6px color-mix(in oklab, var(--msb-accent) 70%, transparent);
}
.msb--glow:hover {
box-shadow:
0 0 34px -4px color-mix(in oklab, var(--msb-accent) 85%, transparent),
0 4px 18px -8px color-mix(in oklab, var(--msb-accent) 70%, transparent);
}
/* ---- движение ---- */
.msb-m--lift:hover {
transform: translateY(-2px);
}
.msb-m--lift:active {
transform: translateY(0);
transition-duration: 0.07s;
}
.msb-m--press:active {
transform: scale(0.955);
transition-duration: 0.07s;
}
/* блик пробегает по кнопке; слой поверх фона, но под текстом не нужен — он полупрозрачный */
.msb-m--sheen::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
105deg,
transparent 32%,
color-mix(in oklab, var(--msb-accent-fg, #fff) 32%, transparent) 50%,
transparent 68%
);
transform: translateX(-160%);
pointer-events: none;
}
.msb-m--sheen:hover::after {
transform: translateX(160%);
transition: transform var(--ds-slow, 0.6s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}
/* заливка выезжает слева ПОД текстом (z-index -1 при isolation на кнопке) */
.msb-m--fill::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: var(--msb-accent);
transform: scaleX(0);
transform-origin: left;
transition: transform var(--ds-base, 0.32s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}
.msb-m--fill:hover::before {
transform: scaleX(1);
}
.msb-m--fill:hover {
color: var(--msb-accent-fg);
border-color: var(--msb-accent);
}
/* стрелка-хвост: появляется из-за кромки, текст сдвигается ей навстречу */
.msb-m--arrow .msb-tail {
display: inline-block;
width: 0;
opacity: 0;
transform: translateX(-6px);
transition:
opacity var(--ds-fast, 0.18s),
width var(--ds-fast, 0.18s),
transform var(--ds-base, 0.32s) var(--ds-ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1));
}
.msb-m--arrow:hover .msb-tail {
width: 1em;
opacity: 1;
transform: translateX(0);
}
/* focus-visible получает то же движение, что и hover: клавиатура не хуже мыши */
.msb-m--lift:focus-visible {
transform: translateY(-2px);
}
.msb-m--fill:focus-visible::before {
transform: scaleX(1);
}
.msb-m--fill:focus-visible {
color: var(--msb-accent-fg);
}
@media (prefers-reduced-motion: reduce) {
.msb,
.msb::before,
.msb::after,
.msb .msb-tail {
transition: none;
}
.msb-m--lift:hover,
.msb-m--press:active {
transform: none;
}
.msb-m--sheen:hover::after {
transform: translateX(160%);
}
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0