← каталог
Glare Card
Карточка-стекло: наклоняется за курсором, по поверхности едет продолговатый блик как по глянцевой печати. Ни твинов, ни rAF — только CSS-переменные.
cssmouse-movehoverparallaxbasic~2 КБreduced-motion ✓
🎛 Настройки — крути и копируй
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| children? | ReactNode | — | |
| tilt? | number | 10 | Максимальный наклон, град. (формат «10»). |
| glare? | number | 0.35 | Сила блика 0–1 (формат «0.35»). |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/glare-card.jsonЧто нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
"use client";
import { useRef, type ReactNode } from "react";
export interface GlareCardProps {
children?: ReactNode;
/** Максимальный наклон, град. (формат «10»). */
tilt?: number;
/** Сила блика 0–1 (формат «0.35»). */
glare?: number;
}
/**
* Карточка-стекло: наклоняется за курсором, а по поверхности едет продолговатый блик,
* как по глянцевой печати. Наклон и блик считаются из ОДНОГО обработчика и уезжают в
* CSS-переменные — ни твинов, ни rAF-лупа; в покое элемент полностью статичен.
*/
export function GlareCard({ children, tilt = 10, glare = 0.35 }: GlareCardProps) {
const ref = useRef<HTMLDivElement>(null);
const frozen = () =>
typeof document !== "undefined" &&
(document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches);
const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
const el = ref.current;
if (!el || frozen()) return;
const r = el.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width;
const py = (e.clientY - r.top) / r.height;
el.style.setProperty("--glc-rx", `${(0.5 - py) * tilt * 2}deg`);
el.style.setProperty("--glc-ry", `${(px - 0.5) * tilt * 2}deg`);
el.style.setProperty("--glc-x", `${px * 100}%`);
el.style.setProperty("--glc-y", `${py * 100}%`);
el.style.setProperty("--glc-op", String(glare));
};
const reset = () => {
const el = ref.current;
if (!el) return;
el.style.setProperty("--glc-rx", "0deg");
el.style.setProperty("--glc-ry", "0deg");
el.style.setProperty("--glc-op", "0");
};
return (
<div className="glc-wrap">
<style>{`
.glc-wrap { perspective: 900px; }
.glc {
--glc-rx: 0deg; --glc-ry: 0deg; --glc-x: 50%; --glc-y: 50%; --glc-op: 0;
position: relative; overflow: hidden; isolation: isolate;
width: min(300px, 78vw); aspect-ratio: 4 / 5; border-radius: 18px; padding: 20px;
display: flex; flex-direction: column; justify-content: flex-end;
border: 1px solid var(--ds-border, oklch(0.34 0.03 280));
background: linear-gradient(160deg, oklch(0.28 0.06 290), oklch(0.16 0.03 275));
box-shadow: 0 22px 50px oklch(0 0 0 / 0.45);
transform: rotateX(var(--glc-rx)) rotateY(var(--glc-ry));
transition: transform 0.35s var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
will-change: transform;
}
/* продолговатый блик — как отражение лампы на глянце */
.glc::after {
content: ""; position: absolute; inset: -40%;
background: radial-gradient(40% 22% at var(--glc-x) var(--glc-y),
oklch(1 0 0 / 0.85), transparent 70%);
opacity: var(--glc-op); transition: opacity 0.3s; pointer-events: none;
mix-blend-mode: overlay;
}
.glc-title { font-size: 1.15rem; font-weight: 700; letter-spacing: -0.02em;
color: var(--ds-fg, oklch(0.97 0.01 265)); }
.glc-sub { margin-top: 5px; font-family: var(--ds-font-mono, ui-monospace, monospace); font-size: 0.72rem;
color: var(--ds-muted, oklch(0.75 0.03 280)); }
@media (prefers-reduced-motion: reduce) { .glc { transition: none; } }
`}</style>
<div ref={ref} className="glc" onPointerMove={onMove} onPointerLeave={reset}>
{children ?? (
<>
<div className="glc-title">Glare Card</div>
<div className="glc-sub">наведи — блик поедет за курсором</div>
</>
)}
</div>
</div>
);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0