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

Ripple Rings

Пульс от центра: кольца расходятся и тают, фазы распределены отрицательными задержками одной анимации. Чистый CSS, подложка отключается для сокета.

cssautobasic0reduced-motion ✓
🎛 Настройки — крути и копируй

Пропсы

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

ПропТипПо умолчаниюЧто это
backdrop?string | falseBACKDROPПодложка под кольцами: CSS-значение `background` или `false`. `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она.
count?number5Сколько колец в пульсе.
period?number6Секунд на один пробег кольца от центра до края.
hue?number300Тон колец, градусы oklch.
children?ReactNodeСодержимое поверх (в витрине — подпись, в секции обычно пусто).

Установка

npx shadcn@latest add https://motiva.pages.dev/r/ripple-rings.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

Код

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

/** Подложка по умолчанию — как в витрине. */
const BACKDROP = "radial-gradient(ellipse at 50% 50%, oklch(0.17 0.02 270), oklch(0.13 0.015 270))";

export interface RippleRingsProps {
  /**
   * Подложка под кольцами: CSS-значение `background` или `false`.
   * `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она.
   */
  backdrop?: string | false;
  /** Сколько колец в пульсе. */
  count?: number;
  /** Секунд на один пробег кольца от центра до края. */
  period?: number;
  /** Тон колец, градусы oklch. */
  hue?: number;
  /** Содержимое поверх (в витрине — подпись, в секции обычно пусто). */
  children?: ReactNode;
}

/**
 * Пульс от центра: кольца расходятся и тают, распределённые по фазе отрицательными
 * задержками одной анимации. Reduced-motion / data-shot — кольца стоят концентрической
 * рябью (статичную раскладку держат базовые inline scale/opacity, анимация с явными
 * from/to их перекрывает, пока жива). Чистый CSS, 0 КБ.
 */
export function RippleRings({ backdrop = BACKDROP, count = 5, period = 6, hue = 300, children }: RippleRingsProps = {}) {
  const n = Math.max(2, Math.round(count));
  return (
    <div
      className="rr"
      style={{
        position: "absolute",
        inset: 0,
        overflow: "hidden",
        ...(backdrop === false ? null : { background: backdrop }),
      }}
    >
      <style>{`
        /* размерный контейнер: кольцо меряется в РОДИТЕЛЕ (cqmin), не во вьюпорте */
        .rr { container-type: size; }
        .rr-ring {
          position: absolute; left: 50%; top: 50%;
          width: 88cqmin; height: 88cqmin;
          margin: -44cqmin;
          border-radius: 999px;
          border: 1px solid var(--rr-line);
          animation: rr-pulse var(--rr-period) linear infinite;
          animation-delay: var(--rr-delay);
        }
        @keyframes rr-pulse {
          from { transform: scale(0.12); opacity: 0; }
          14%  { opacity: var(--rr-alpha); }
          to   { transform: scale(1); opacity: 0; }
        }
        @media (prefers-reduced-motion: reduce) { .rr-ring { animation: none; } }
      `}</style>
      {Array.from({ length: n }, (_, i) => (
        <span
          key={i}
          className="rr-ring"
          style={
            {
              // база для статики: концентрическая рябь с угасанием к краю
              transform: `scale(${(0.2 + (0.8 * (i + 1)) / n).toFixed(3)})`,
              opacity: (0.5 * (1 - i / n) + 0.08).toFixed(3),
              "--rr-period": `${period}s`,
              "--rr-delay": `${(-period * i) / n}s`,
              "--rr-alpha": "0.5",
              "--rr-line": `oklch(0.7 0.12 ${hue} / 0.6)`,
            } as CSSProperties
          }
        />
      ))}
      {children && (
        <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}>{children}</div>
      )}
    </div>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); идея-референс — Magic UI Ripple (MIT)v1.0.0