---
title: Tilt
description: Card that tracks the pointer in subtle 3D.
group: Pointer
order: 30
new: true
---

A card that follows the pointer in light 3D. Tune the tilt, perspective, and glare.

## Card

```tsx
import { Tilt } from "@/components/motion/tilt"

export function Example() {
  return (
    <Tilt className="w-full max-w-sm rounded-xl">
      <div className="rounded-xl border border-border bg-background p-4">
        Content
      </div>
    </Tilt>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add tilt
```

## Usage

```tsx
import { Tilt } from "@/components/motion/tilt"
```

```tsx
<Tilt className="w-full max-w-sm rounded-xl">
  <div className="rounded-xl border border-border bg-background p-4">
    Content
  </div>
</Tilt>
```

## API Reference

### Tilt

| Prop | Type | Default |
| --- | --- | --- |
| maxTilt | number | 14 |
| perspective | number | 900 |
| glare | boolean | true |
| className | string | — |
| children | ReactNode | — |

Also accepts the other native `div` attributes.

## Source

```tsx
"use client";

import "@/lib/motion-styles";

import {
  useRef,
  useState,
  type ComponentProps,
  type CSSProperties,
  type ReactNode,
} from "react";
import { cn } from "@/lib/utils";

export type TiltProps = ComponentProps<"div"> & {
  /** Max tilt in degrees. */
  maxTilt?: number;
  /** Perspective distance in px. */
  perspective?: number;
  /** Soft light that follows the pointer. */
  glare?: boolean;
  children?: ReactNode;
};

function Tilt({
  className,
  maxTilt = 14,
  perspective = 900,
  glare = true,
  children,
  style,
  onMouseMove,
  onMouseLeave,
  ...props
}: TiltProps) {
  const ref = useRef<HTMLDivElement>(null);
  const [rotate, setRotate] = useState({ x: 0, y: 0 });
  const [glarePos, setGlarePos] = useState({ x: 50, y: 50 });
  const [active, setActive] = useState(false);
  const tilt = Math.min(Math.max(maxTilt, 0), 30);
  const depth = Math.max(perspective, 200);

  function handleMove(clientX: number, clientY: number) {
    const el = ref.current;
    if (!el) return;
    if (
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    ) {
      return;
    }

    const rect = el.getBoundingClientRect();
    const px = (clientX - rect.left) / Math.max(rect.width, 1);
    const py = (clientY - rect.top) / Math.max(rect.height, 1);
    setRotate({
      x: (0.5 - py) * 2 * tilt,
      y: (px - 0.5) * 2 * tilt,
    });
    setGlarePos({ x: px * 100, y: py * 100 });
    setActive(true);
  }

  function handleLeave() {
    setRotate({ x: 0, y: 0 });
    setGlarePos({ x: 50, y: 50 });
    setActive(false);
  }

  return (
    <div
      ref={ref}
      data-slot="tilt"
      data-active={active ? "true" : "false"}
      className={cn(
        "arctis-tilt relative overflow-hidden will-change-transform",
        className,
      )}
      style={
        {
          ...style,
          transform: `perspective(${depth}px) rotateX(${rotate.x}deg) rotateY(${rotate.y}deg)`,
        } as CSSProperties
      }
      {...props}
      onMouseMove={(event) => {
        onMouseMove?.(event);
        handleMove(event.clientX, event.clientY);
      }}
      onMouseLeave={(event) => {
        onMouseLeave?.(event);
        handleLeave();
      }}
    >
      {children}
      {glare ? (
        <span
          aria-hidden
          className="arctis-tilt-glare"
          data-active={active ? "true" : "false"}
          style={
            {
              background: `radial-gradient(circle at ${glarePos.x}% ${glarePos.y}%, rgb(255 255 255 / 0.4), transparent 55%)`,
            } as CSSProperties
          }
        />
      ) : null}
    </div>
  );
}

export { Tilt };
```
