Cards
ParallaxTile
Layers inside slide at different speeds with the pointer, so a flat card feels deep. Wrap each layer in ParallaxLayer.
Download
- React
- Tailwind CSS v4
- motion
components/ui/parallax-tile.tsx
"use client";
import type { CSSProperties, ReactNode } from "react";
import { useReducedMotion } from "motion/react";
type ParallaxTileProps = {
/** Put ParallaxLayer elements inside. */
children: ReactNode;
className?: string;
};
type ParallaxLayerProps = {
children?: ReactNode;
className?: string;
/** Pixels this layer moves at the tile edge. Bigger = feels closer. Negative moves the other way. */
depth?: number;
};
/* ParallaxTile: layers inside slide at different speeds with the pointer, so the card feels deep. */
export function ParallaxTile({
children,
className = "relative h-56 overflow-hidden rounded-2xl bg-[#100e1c] text-white",
}: ParallaxTileProps) {
const reduce = useReducedMotion();
const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (reduce) return;
const el = e.currentTarget;
const r = el.getBoundingClientRect();
el.style.setProperty("--px", `${((e.clientX - r.left) / r.width) * 2 - 1}`);
el.style.setProperty("--py", `${((e.clientY - r.top) / r.height) * 2 - 1}`);
};
const onLeave = (e: React.PointerEvent<HTMLDivElement>) => {
e.currentTarget.style.setProperty("--px", "0");
e.currentTarget.style.setProperty("--py", "0");
};
return (
<div
onPointerMove={onMove}
onPointerLeave={onLeave}
className={className}
style={{ "--px": 0, "--py": 0 } as CSSProperties}
>
{children}
</div>
);
}
export function ParallaxLayer({ children, className = "", depth = 20 }: ParallaxLayerProps) {
return (
<div
className={`transition-transform duration-300 ease-out motion-reduce:transition-none ${className}`}
style={{ transform: `translate3d(calc(var(--px) * ${depth}px), calc(var(--py) * ${depth}px), 0)` }}
>
{children}
</div>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/parallax-tile.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/parallax-tile-demo.tsx
import { ParallaxLayer, ParallaxTile } from "@/components/ui/parallax-tile"; export default function ParallaxTileExample() { return ( <div className="grid h-full place-items-center bg-[#0b0a14] p-6"> <ParallaxTile className="relative h-56 w-full max-w-[320px] overflow-hidden rounded-2xl bg-[#100e1c] text-white"> <ParallaxLayer depth={-10} className="absolute -inset-6"> <div className="absolute left-[10%] top-[15%] size-28 rounded-full bg-[#a78bfa]/40 blur-2xl" /> <div className="absolute bottom-[10%] right-[10%] size-32 rounded-full bg-[#38bdf8]/30 blur-2xl" /> </ParallaxLayer> <ParallaxLayer depth={14} className="absolute inset-0"> <div className="absolute right-8 top-8 size-12 rotate-12 rounded-xl border border-white/20 bg-white/5" /> <div className="absolute bottom-10 left-10 size-6 rounded-full bg-[#a78bfa]" /> </ParallaxLayer> <ParallaxLayer depth={26} className="absolute inset-0 grid place-content-center text-center"> <p className="text-2xl font-semibold">Layers of depth</p> <p className="mt-1 text-sm text-white/60">Move your pointer over the tile.</p> </ParallaxLayer> </ParallaxTile> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | — | ParallaxLayer elements. |
| className | string | dark tile, 14rem tall | Size and look of the tile. |
| ParallaxLayer depth | number | 20 | Pixels the layer moves at the edge. Bigger feels closer; negative moves the other way. |