Motiva
Мои коллекции
← каталог

Shimmer Button

Кнопка с бегущим бликом: по кайме крутится коническая подсветка (@property), по плашке раз в цикл проходит зайчик. Оба слоя — псевдоэлементы, кнопка остаётся кнопкой.

csshoverrevealintermediate~2 КБreduced-motion ✓
↗
🎛 Настройки — крути и копируй

Пропсы

Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.

ПропТипПо умолчаниюЧто это
children?ReactNode"Начать бесплатно"
shine?string"#ffffff"Цвет блика.
duration?number2.6Длительность пробега блика, сек.
onClick?() => void;—

Установка

npx shadcn@latest add https://motiva.pages.dev/r/shimmer-button.json
Что нужно в проекте до установки
  1. Файл components.json в корне

    Из него CLI узнаёт, куда класть файлы. Алиасы — в такой форме:

    "aliases": { "components": "@/components", "lib": "@/lib" }

    Нет файла — установка не начнётся вовсе.

  2. Алиас @ объявлен дважды

    В корневом tsconfig.json и в конфиге сборщика. В Vite корневой tsconfig часто содержит только references — этого мало.

    "paths": { "@/*": ["./src/*"] }        // tsconfig.json
    resolve.alias = { "@": "/src" }        // vite.config.ts

    Без paths CLI молча создаёт папку с именем @.

  3. Токены подключены один раз на приложение

    Они приезжают вместе с записью — достаточно импортировать их в точке входа:

    import "@/styles/motiva-tokens.css";

    Без них блок потеряет цвета, ритм и тему.

  4. Своя типографика не спорит с блоком

    Наследуемые свойства протекают внутрь: text-align: center на обёртке отцентрует содержимое секции. То же касается font-familyи line-height.

  5. Проект без TypeScript? всё равно нужен tsconfig

    CLI грузит tsconfig.json безусловно, а при "tsx": false идёт за jsconfig.json и падает так же. Минимум — файл с pathsи "tsx": true в components.json.

    Приехавший рядом .tsx можно не трогать — вам нужен .vanilla.js.

Экспорт

↓ Скачать .zip

Код

@property --shb-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
@keyframes shb-spin { to { --shb-angle: 360deg; } }
@keyframes shb-sweep { 0% { transform: translateX(-140%) skewX(-18deg); } 55%, 100% { transform: translateX(240%) skewX(-18deg); } }

.shb {
  position: relative; isolation: isolate; overflow: hidden;
  padding: 13px 26px; border: none; border-radius: 999px; cursor: pointer;
  font: 600 0.95rem/1 var(--ds-font, system-ui, sans-serif); letter-spacing: -0.01em;
  /* плашка и текст — ПАРА токенов, а не два независимых цвета. Раньше здесь стоял
     тёмный фон и var(--ds-fg): в тёмной теме читалось, в светлой --ds-fg темнеет,
     и надпись пропадала на тёмной плашке. Акцентная пара контрастна в обеих. */
  color: var(--ds-accent-fg, oklch(0.14 0.02 275));
  background: var(--ds-accent, oklch(0.72 0.16 300));
  transition: transform 0.18s var(--ds-ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)), box-shadow 0.25s;
  box-shadow: 0 6px 22px var(--ds-accent-ring, oklch(0.5 0.2 295 / 0.25));
}
.shb:hover { transform: translateY(-1px); background: var(--ds-accent-hover, oklch(0.78 0.16 300));
  box-shadow: 0 10px 30px var(--ds-accent-ring, oklch(0.5 0.2 295 / 0.4)); }
.shb:active { transform: translateY(0) scale(0.985); }
.shb:focus-visible { outline: 2px solid var(--ds-accent, oklch(0.75 0.16 300)); outline-offset: 3px; }

/* вращающаяся кайма: конический градиент под маской-рамкой */
.shb::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1.5px;
  background: conic-gradient(from var(--shb-angle),
    transparent 0deg, var(--shb-shine) 40deg, transparent 90deg,
    transparent 180deg, var(--shb-shine) 220deg, transparent 270deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  animation: shb-spin var(--shb-dur) linear infinite;
  z-index: -1;
}
/* диагональный зайчик по плашке */
.shb::after {
  content: ""; position: absolute; top: -20%; left: 0; width: 45%; height: 140%;
  background: linear-gradient(90deg, transparent, var(--shb-shine), transparent);
  opacity: 0.32; filter: blur(6px); pointer-events: none;
  animation: shb-sweep var(--shb-dur) cubic-bezier(0.5, 0, 0.2, 1) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .shb::before, .shb::after { animation: none; }
  .shb::after { opacity: 0; }
}
html[data-shot] .shb::before, html[data-shot] .shb::after { animation: none; }
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0