# Tilt Card

> A 3D card that tilts toward the pointer with a glare highlight and eases back on leave.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/tilt-card
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/tilt-card.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/tilt-card-demo.tsx"
import { Sparkles } from "lucide-react"

import { TiltCard } from "@/components/tilt-card"

export default function TiltCardDemo() {
  return (
    <div className="flex w-full items-center justify-center py-6">
      <TiltCard className="w-72 rounded-3xl">
        <div className="overflow-hidden rounded-3xl border bg-card text-card-foreground shadow-xl">
          <div className="relative h-36 bg-linear-to-br from-orange-400 via-pink-500 to-violet-600">
            <div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgb(255_255_255/0.35),transparent_45%)]" />
            <span className="absolute bottom-3 left-4 inline-flex items-center gap-1.5 rounded-full bg-black/30 px-2.5 py-1 text-xs font-medium text-white backdrop-blur">
              <Sparkles className="size-3.5" /> Limited
            </span>
          </div>
          <div className="space-y-1.5 p-5">
            <h3 className="text-lg font-semibold">Holo Edition</h3>
            <p className="text-sm text-muted-foreground">Move your pointer across the card and watch the light follow.</p>
            <div className="flex items-center justify-between pt-3 text-sm">
              <span className="font-mono text-muted-foreground">#0042 / 100</span>
              <span className="font-semibold">$48</span>
            </div>
          </div>
        </div>
      </TiltCard>
    </div>
  )
}
```

## Source

```tsx title="components/tilt-card.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

export interface TiltCardProps extends React.ComponentProps<"div"> {
  /** Maximum rotation in degrees */
  maxTilt?: number
  /** CSS perspective distance in px */
  perspective?: number
  /** Scale while the pointer is over the card */
  scale?: number
  /** Show a glare highlight that follows the pointer */
  glare?: boolean
  /** Peak opacity of the glare, 0 to 1 */
  glareOpacity?: number
}

// Pointer position is turned into CSS variables on the element itself, so React
// never re-renders while the pointer moves.
export function TiltCard({
  maxTilt = 12,
  perspective = 900,
  scale = 1.03,
  glare = true,
  glareOpacity = 0.35,
  className,
  style,
  children,
  onPointerMove,
  onPointerLeave,
  ...props
}: TiltCardProps) {
  const handleMove = (e: React.PointerEvent<HTMLDivElement>) => {
    onPointerMove?.(e)
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
    const el = e.currentTarget
    const rect = el.getBoundingClientRect()
    const px = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1)
    const py = Math.min(Math.max((e.clientY - rect.top) / rect.height, 0), 1)
    el.style.setProperty("--tilt-x", `${((0.5 - py) * 2 * maxTilt).toFixed(2)}deg`)
    el.style.setProperty("--tilt-y", `${((px - 0.5) * 2 * maxTilt).toFixed(2)}deg`)
    el.style.setProperty("--tilt-scale", String(scale))
    el.style.setProperty("--glare-x", `${(px * 100).toFixed(1)}%`)
    el.style.setProperty("--glare-y", `${(py * 100).toFixed(1)}%`)
    el.style.setProperty("--glare-o", "1")
  }

  const handleLeave = (e: React.PointerEvent<HTMLDivElement>) => {
    onPointerLeave?.(e)
    const el = e.currentTarget
    el.style.setProperty("--tilt-x", "0deg")
    el.style.setProperty("--tilt-y", "0deg")
    el.style.setProperty("--tilt-scale", "1")
    el.style.setProperty("--glare-o", "0")
  }

  return (
    <div
      className={cn(
        "relative touch-pan-y transition-transform duration-200 ease-out will-change-transform motion-reduce:transition-none",
        className
      )}
      style={{
        transform: `perspective(${perspective}px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg)) scale(var(--tilt-scale, 1))`,
        ...style,
      }}
      onPointerMove={handleMove}
      onPointerLeave={handleLeave}
      {...props}
    >
      {children}
      {glare && (
        <div
          aria-hidden
          className="pointer-events-none absolute inset-0 rounded-[inherit] opacity-(--glare-o,0) transition-opacity duration-300"
          style={{
            background: `radial-gradient(circle at var(--glare-x, 50%) var(--glare-y, 50%), rgb(255 255 255 / ${glareOpacity}), transparent 55%)`,
          }}
        />
      )}
    </div>
  )
}
```
