Ripple Button
Кнопка с Material-рябью, расходящейся из точки клика. CSS + пара строк JS.
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| children | ReactNode | — |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/ripple-button.jsonЧто нужно в проекте до установки
Файл
components.jsonв корнеИз него CLI узнаёт, куда класть файлы. Алиасы — в такой форме:
"aliases": { "components": "@/components", "lib": "@/lib" }Нет файла — установка не начнётся вовсе.
Алиас
@объявлен дваждыВ корневом
tsconfig.jsonи в конфиге сборщика. В Vite корневой tsconfig часто содержит толькоreferences— этого мало."paths": { "@/*": ["./src/*"] } // tsconfig.json resolve.alias = { "@": "/src" } // vite.config.tsБез
pathsCLI молча создаёт папку с именем@.Токены подключены один раз на приложение
Они приезжают вместе с записью — достаточно импортировать их в точке входа:
import "@/styles/motiva-tokens.css";Без них блок потеряет цвета, ритм и тему.
Своя типографика не спорит с блоком
Наследуемые свойства протекают внутрь:
text-align: centerна обёртке отцентрует содержимое секции. То же касаетсяfont-familyиline-height.Проект без TypeScript? всё равно нужен tsconfig
CLI грузит
tsconfig.jsonбезусловно, а при"tsx": falseидёт заjsconfig.jsonи падает так же. Минимум — файл сpathsи"tsx": trueвcomponents.json.Приехавший рядом
.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
.rb {
position: relative;
overflow: hidden;
padding: 0.9rem 2.2rem;
border: none;
border-radius: 12px;
background: linear-gradient(135deg, oklch(0.62 0.19 280), oklch(0.72 0.15 220));
color: #fff;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
isolation: isolate;
}
.rb-ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.45);
transform: scale(0);
animation: rb-ripple 0.6s ease-out;
pointer-events: none;
}
@keyframes rb-ripple {
to {
transform: scale(2.4);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.rb-ripple {
animation-duration: 0.01s;
}
}
"use client";
import { type MouseEvent, type ReactNode } from "react";
import "./ripple-button.css";
/** Кнопка с Material-рябью из точки клика (CSS + добавление span на клик). */
export function RippleButton({ children, ...props }: { children: ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
function onClick(e: MouseEvent<HTMLButtonElement>) {
const btn = e.currentTarget;
const rect = btn.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const span = document.createElement("span");
span.className = "rb-ripple";
span.style.width = span.style.height = `${size}px`;
span.style.left = `${e.clientX - rect.left - size / 2}px`;
span.style.top = `${e.clientY - rect.top - size / 2}px`;
btn.appendChild(span);
span.addEventListener("animationend", () => span.remove());
props.onClick?.(e);
}
return (
<button {...props} className="rb" onClick={onClick}>
{children}
</button>
);
}