Mikkel Manniche
Dansk English Deutsch

Flächen

Tilt card

Eine Karte, die sich in 3D zur Maus neigt, mit Ebenen, die darüber schweben. Flach auf Touchscreens und bei reduzierter Bewegung.

Vorschau

Verwendung

tilt-card-demo.tsx
import { TiltCard } from '@/components/tilt-card'

export default function TiltCardDemo() {
  return (
    <TiltCard className="mx-auto max-w-sm">
      <div data-depth="1" className="h-36 rounded-xl bg-[linear-gradient(135deg,#f97316,#db2777_50%,#4f46e5)]" />
      <p data-depth="2" className="mt-4 font-medium">
        New website in two weeks
      </p>
      <p data-depth="1" className="mt-1 text-sm text-muted-foreground">
        Move the mouse over the card.
      </p>
    </TiltCard>
  )
}

Quellcode

tilt-card.tsx
import { useRef, type CSSProperties, type HTMLAttributes, type PointerEvent } from 'react'
import { cn } from '@/lib/utils'

export type TiltCardProps = HTMLAttributes<HTMLDivElement> & {
  /** The largest tilt in degrees, reached at the card's edges. */
  max?: number
  /** A soft glare that follows the pointer. */
  glare?: boolean
}

/**
 * A card that tilts towards the mouse in 3D. Children with `data-depth="1"` to `"3"` float above the card.
 * The angles are written straight to CSS variables, so React does not re-render on move.
 * On touch screens and under reduced motion it is a flat card.
 */
export function TiltCard({ max = 10, glare = true, className, children, onPointerMove, onPointerLeave, style, ...rest }: TiltCardProps) {
  const el = useRef<HTMLDivElement>(null)

  function move(e: PointerEvent<HTMLDivElement>) {
    onPointerMove?.(e)
    if (e.pointerType !== 'mouse' || !el.current) return
    const r = el.current.getBoundingClientRect()
    const x = (e.clientX - r.left) / r.width
    const y = (e.clientY - r.top) / r.height
    el.current.style.setProperty('--tilt-x', `${(0.5 - y) * 2 * max}deg`)
    el.current.style.setProperty('--tilt-y', `${(x - 0.5) * 2 * max}deg`)
    el.current.style.setProperty('--glare-x', `${x * 100}%`)
    el.current.style.setProperty('--glare-y', `${y * 100}%`)
  }

  function leave(e: PointerEvent<HTMLDivElement>) {
    onPointerLeave?.(e)
    el.current?.style.setProperty('--tilt-x', '0deg')
    el.current?.style.setProperty('--tilt-y', '0deg')
  }

  return (
    <div className="[perspective:900px]">
      <div
        ref={el}
        onPointerMove={move}
        onPointerLeave={leave}
        style={style as CSSProperties}
        className={cn(
          'group/tilt relative isolate rounded-2xl border bg-card p-6 text-card-foreground shadow-sm [transform-style:preserve-3d]',
          '[transform:rotateX(var(--tilt-x,0deg))_rotateY(var(--tilt-y,0deg))] transition-transform duration-200 ease-out',
          // The depth layers lift off the card only while it can tilt.
          'motion-safe:[&_[data-depth="1"]]:[transform:translateZ(20px)] motion-safe:[&_[data-depth="2"]]:[transform:translateZ(40px)] motion-safe:[&_[data-depth="3"]]:[transform:translateZ(60px)]',
          'motion-reduce:transform-none motion-reduce:transition-none',
          className,
        )}
        {...rest}
      >
        {glare && (
          <span
            aria-hidden
            className={cn(
              'pointer-events-none absolute inset-0 -z-10 rounded-[inherit] opacity-0 transition-opacity duration-300',
              'bg-[radial-gradient(circle_at_var(--glare-x,50%)_var(--glare-y,50%),rgb(255_255_255/0.14),transparent_60%)]',
              'group-hover/tilt:opacity-100 motion-reduce:hidden',
            )}
          />
        )}
        {children}
      </div>
    </div>
  )
}

Installieren

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/tilt-card.json

Mit eingetragener Registry (components.json)

npx shadcn@latest add @manniche/tilt-card

Abhängigkeiten

Dateien

  • tilt-card.tsx

Manniche Lab · Open Source

Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.