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

Ripple Rings

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

cssautobasicreduced-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 в корне

    Из него 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

Код

const css = `/* размерный контейнер: кольцо меряется в РОДИТЕЛЕ (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; } }
`;
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 href="motiva-ripple-rings" precedence="medium">{css}</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)v1.0.0