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

Shimmer Button

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

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

Пропсы

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

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

Установка

npx shadcn@latest add https://motiva.pages.dev/r/shimmer-button.json
Что нужно в проекте до установки
  1. components.json в корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.
  2. Алиас @ объявлен дважды: "paths": {"@/*": ["./src/*"]} в корневом tsconfig.json (в Vite он часто содержит только references — тогда CLI создаёт папку@) и resolve.alias в конфиге сборщика.
  3. Токены подключены один раз на приложение:import "@/styles/motiva-tokens.css" — они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему.
  4. Своя типографика не должна спорить: если у вас на обёрткеtext-align: center, он протечёт внутрь блоков.
  5. Проект без TypeScript? Всё равно нужен минимальныйtsconfig.json с pathsи "tsx": true в components.json: CLI грузит tsconfig безусловно, а с "tsx": false идёт за jsconfig.json и падает так же. Приехавший рядом.tsx можно не трогать — вам нужен.vanilla.js.

Экспорт

↓ Скачать .zip

Код

"use client";

import type { CSSProperties, ReactNode } from "react";

export interface ShimmerButtonProps {
  children?: ReactNode;
  /** Цвет блика (формат «#ffffff»). */
  shine?: string;
  /** Длительность пробега блика, сек (формат «2.6»). */
  duration?: number;
  onClick?: () => void;
}

/**
 * Кнопка с бегущим бликом: по границе крутится коническая подсветка (@property + анимируемый
 * угол), а по самой плашке раз в цикл пробегает диагональный «солнечный зайчик».
 * Оба слоя — псевдоэлементы, поэтому кнопка остаётся обычной кнопкой: контент не обёрнут,
 * фокус и семантика на месте. Уважает prefers-reduced-motion.
 */
export function ShimmerButton({
  children = "Начать бесплатно",
  shine = "#ffffff",
  duration = 2.6,
  onClick,
}: ShimmerButtonProps) {
  return (
    <>
      <style>{`
        @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; }
      `}</style>
      <button
        className="shb"
        onClick={onClick}
        style={{ "--shb-shine": shine, "--shb-dur": `${duration}s` } as CSSProperties}
      >
        {children}
      </button>
    </>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0