← каталог
Flip Card
Карточка переворачивается в 3D по клику (spring), у лицевой и обратной стороны свой контент.
motionclickspringbasic~18 КБreduced-motion ✓
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| front? | ReactNode | — | |
| back? | ReactNode | — | |
| width? | number | 240 | |
| height? | number | 300 |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/flip-card.jsonзависимости: motion@^12.42.2
Что нужно в проекте до установки
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 { useState, type CSSProperties, type ReactNode } from "react";
import { motion } from "motion/react";
const face: CSSProperties = {
position: "absolute",
inset: 0,
borderRadius: 18,
border: "1px solid oklch(0.3 0.02 270)",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
gap: 8,
padding: "1.4rem",
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
};
export interface FlipCardProps {
front?: ReactNode;
back?: ReactNode;
width?: number;
height?: number;
}
/** Карточка переворачивается в 3D по клику (spring). Reduced-motion/data-shot — мгновенная смена стороны. */
export function FlipCard({ front, back, width = 240, height = 300 }: FlipCardProps) {
const [flipped, setFlipped] = useState(false);
const frozen =
typeof document !== "undefined" &&
(document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches);
return (
<button
onClick={() => setFlipped((v) => !v)}
aria-pressed={flipped}
style={{
width,
height,
perspective: 1000,
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
color: "oklch(0.92 0.01 265)",
}}
>
<motion.div
animate={{ rotateY: flipped ? 180 : 0 }}
transition={frozen ? { duration: 0 } : { type: "spring", stiffness: 260, damping: 22 }}
style={{ position: "relative", width: "100%", height: "100%", transformStyle: "preserve-3d" }}
>
<div style={{ ...face, background: "linear-gradient(160deg, oklch(0.3 0.06 290), oklch(0.17 0.02 270))" }}>
{front ?? (
<>
<span style={{ fontSize: "2rem" }}>◈</span>
<strong style={{ fontSize: "1.05rem" }}>Flip Card</strong>
<span style={{ fontSize: "0.8rem", color: "oklch(0.7 0.02 265)" }}>кликни — переверну</span>
</>
)}
</div>
<div
style={{
...face,
transform: "rotateY(180deg)",
background: "linear-gradient(160deg, oklch(0.32 0.08 200), oklch(0.17 0.02 270))",
}}
>
{back ?? (
<>
<strong style={{ fontSize: "1.05rem" }}>Обратная сторона</strong>
<span style={{ fontSize: "0.8rem", textAlign: "center", color: "oklch(0.7 0.02 265)" }}>
Тут детали, цена или CTA. Клик — обратно.
</span>
</>
)}
</div>
</motion.div>
</button>
);
}
import { animate } from "motion";
/**
* Flip Card — vanilla JS/Motion (без React). 3D-переворот по клику.
*
* <button id="card" style="perspective:1000px">
* <div data-inner style="position:relative;transform-style:preserve-3d">
* <div data-face style="backface-visibility:hidden">Лицо</div>
* <div data-face style="position:absolute;inset:0;transform:rotateY(180deg);backface-visibility:hidden">Тыл</div>
* </div>
* </button>
* <script type="module">
* import { initFlipCard } from './flip-card.vanilla.js';
* initFlipCard(document.querySelector('#card'));
* </script>
*
* @param {HTMLElement} card — контейнер с [data-inner] (перспективу задай на карточке)
* @returns {() => void} destroy
*/
export function initFlipCard(card) {
const inner = card.querySelector("[data-inner]");
if (!inner) return () => {};
let flipped = false;
const frozen = () =>
document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const toggle = () => {
flipped = !flipped;
card.setAttribute("aria-pressed", String(flipped));
const deg = flipped ? 180 : 0;
if (frozen()) {
inner.style.transform = `rotateY(${deg}deg)`;
} else {
animate(inner, { transform: `rotateY(${deg}deg)` }, { type: "spring", stiffness: 260, damping: 22 });
}
};
card.addEventListener("click", toggle);
return () => card.removeEventListener("click", toggle);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0