Roll Reveal Button
A crisp 3D perspective button. The label and icon each live on two stacked faces that roll in opposite directions on hover, revealing an identical copy of themselves via rotateX and perspective transforms.
ButtonReactTailwind CSSMotion
CSSTailwind
Manual
Create a file and paste the following code into it.
src/components/ui/roll-reveal-button.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
"use client";
import { motion, useReducedMotion, type HTMLMotionProps, type Transition, type Variants } from "motion/react";
import { ArrowRight } from "@phosphor-icons/react";
import { cn } from "@/lib/cn";
const SPRING: Transition = { type: "spring", stiffness: 380, damping: 30, mass: 0.8 };
function rollVariants({
restRotateX,
restY,
hoverRotateX,
hoverY,
}: {
restRotateX: number;
restY: number;
hoverRotateX: number;
hoverY: number;
}): Variants {
return {
rest: { opacity: restRotateX === 0 && restY === 0 ? 1 : 0, rotateX: restRotateX, y: restY },
hover: { opacity: hoverRotateX === 0 && hoverY === 0 ? 1 : 0, rotateX: hoverRotateX, y: hoverY },
};
}
const FACE_BASE =
"absolute inset-0 grid place-items-center bg-white text-[#171515] shadow-[0_22px_60px_rgba(31,24,21,0.1)] [backface-visibility:hidden] group-focus-visible:ring-2 group-focus-visible:ring-black/70 group-focus-visible:ring-offset-4 group-focus-visible:ring-offset-[#d4f1f4]";
const LABEL_FACE = "rounded-full px-12 text-5xl font-semibold leading-none tracking-normal max-sm:px-9 max-sm:text-4xl";
const ICON_FACE = "rounded-[10px] [&>svg]:h-12 [&>svg]:w-12 max-sm:[&>svg]:h-10 max-sm:[&>svg]:w-10";
const labelTop = rollVariants({ restRotateX: 0, restY: 0, hoverRotateX: -76, hoverY: -58 });
const labelBottom = rollVariants({ restRotateX: 76, restY: 58, hoverRotateX: 0, hoverY: 0 });
const iconTop = rollVariants({ restRotateX: 0, restY: 0, hoverRotateX: 70, hoverY: 56 });
const iconBottom = rollVariants({ restRotateX: -70, restY: -56, hoverRotateX: 0, hoverY: 0 });
function RollRevealButton({
children = "Button",
icon = <ArrowRight aria-hidden="true" weight="bold" />,
className,
type = "button",
...props
}: {
children?: React.ReactNode;
icon?: React.ReactNode;
className?: string;
} & Omit<HTMLMotionProps<"button">, "children" | "className">) {
const reduceMotion = useReducedMotion();
const hoverKey = reduceMotion ? undefined : "hover";
return (
<motion.button
type={type}
initial="rest"
whileHover={hoverKey}
whileFocus={hoverKey}
// Press feedback stays even under reduced-motion: a subtle scale-down is
// instant tactile feedback, not vestibular motion.
whileTap={{ scale: 0.985 }}
className={cn(
"group inline-flex items-center gap-2 border-0 bg-transparent p-0 outline-none disabled:pointer-events-none disabled:opacity-60",
"[perspective:760px]",
className
)}
{...props}
>
<span className="relative block h-24 w-72 [transform-style:preserve-3d] max-sm:h-20 max-sm:w-56">
<motion.span variants={labelTop} transition={SPRING} className={cn(FACE_BASE, LABEL_FACE, "[transform-origin:50%_0%]")}>
{children}
</motion.span>
<motion.span
variants={labelBottom}
transition={SPRING}
className={cn(FACE_BASE, LABEL_FACE, "[transform-origin:50%_100%]")}
aria-hidden="true"
>
{children}
</motion.span>
</span>
<span className="relative block h-24 w-24 [transform-style:preserve-3d] max-sm:h-20 max-sm:w-20">
<motion.span variants={iconTop} transition={SPRING} className={cn(FACE_BASE, ICON_FACE, "[transform-origin:50%_100%]")}>
{icon}
</motion.span>
<motion.span
variants={iconBottom}
transition={SPRING}
className={cn(FACE_BASE, ICON_FACE, "[transform-origin:50%_0%]")}
aria-hidden="true"
>
{icon}
</motion.span>
</span>
</motion.button>
);
}
export default function RollRevealButtonDemo() {
return (
<main className="grid h-dvh w-full place-items-center bg-[#d4f1f4] px-6">
<RollRevealButton />
</main>
);
}
Update the import paths to match your project setup.