Tilt Card

A card with 3D spring tilt, optional floating image, badge variants and a radial shine on hover. Built on top of the Tilt primitive.

Made by Léo
Open in
demo-tilt-card.tsx
"use client";

import {
  TiltCard,
  type TiltCardProps,
} from "@/components/unlumen-ui/tilt-card";

type TiltCardDemoProps = Pick<TiltCardProps, "badgeVariant"> & {
  rotationFactor?: number;
};

export const TiltCardDemo = ({
  badgeVariant = "success",
  rotationFactor = 11,
}: TiltCardDemoProps) => {
  return (
    <div className="flex w-full flex-wrap gap-6 items-center justify-center p-10 max-w-xl">
      {/* Split badge + image */}
      <div className="w-full">
        <TiltCard
          title="Starter Kit"
          description="Everything you need to ship a modern SaaS product fast."
          price="Free"
          badgeLabel="Popular"
          badgeVariant={badgeVariant}
          imageSrc="https://images.unsplash.com/photo-1707978813846-dbf6c4dfbbe3?q=80&w=1065&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
          imageAlt="dashboard screenshot"
          href="#"
          tiltProps={{ rotationFactor }}
        />
      </div>

      {/* Price only badge + image */}
      <div className="w-full">
        <TiltCard
          title="Pro Template"
          description="Advanced features for power users and growing teams."
          price="$49"
          imageSrc="https://images.unsplash.com/photo-1759834857095-4e172a6d03f5?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
          imageAlt="analytics"
          href="#"
          tiltProps={{ rotationFactor }}
        />
      </div>

      {/* Split badge, warning, children */}
      <div className="w-full">
        <TiltCard
          title="Enterprise"
          description="SSO, audit logs, and dedicated infrastructure."
          price="$199"
          badgeLabel="Best value"
          badgeVariant="warning"
          href="#"
          tiltProps={{ rotationFactor }}
        >
          <ul className="text-xs text-muted-foreground space-y-1 mt-1">
            <li>✓ Unlimited seats</li>
            <li>✓ Priority support</li>
            <li>✓ Custom domain</li>
          </ul>
        </TiltCard>
      </div>
    </div>
  );
};

Installation

Install the following dependencies:

Install the following registry dependencies:

Copy and paste the following code into your project:

components/unlumen-ui/tilt-card.tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { ClippedCircle } from "@/components/unlumen-ui/clipped-circle";
import { Tilt, type TiltProps } from "@/components/unlumen-ui/tilt";

export interface TiltCardProps extends React.HTMLAttributes<HTMLDivElement> {
  title: string;
  description?: string;
  /** left half of the split badge pill; shown as a simple pill if `badgeLabel` is omitted */
  price?: string;
  /** right half of the split pill, coloured by `badgeVariant` */
  badgeLabel?: string;
  badgeVariant?: "success" | "warning";
  imageSrc?: string;
  imageAlt?: string;
  /** wraps the card in a plain `<a>` tag */
  href?: string;
  children?: React.ReactNode;
  tiltProps?: Omit<TiltProps, "children" | "className">;
}

const BADGE_LABEL_CLASSES: Record<
  NonNullable<TiltCardProps["badgeVariant"]>,
  string
> = {
  success: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
  warning: "bg-amber-500/20 text-amber-700 dark:text-amber-300",
};

export function TiltCard({
  title,
  description,
  price,
  badgeLabel,
  badgeVariant = "success",
  imageSrc,
  imageAlt = "",
  href,
  children,
  tiltProps,
  className,
  ...props
}: TiltCardProps) {
  const inner = (
    <Tilt
      rotationFactor={11}
      {...tiltProps}
      className={cn(
        "relative group overflow-hidden",
        "bg-background border border-border rounded-lg",
        "flex flex-col gap-4",
        "h-48 sm:h-52 md:h-56 w-full",
        "hover:shadow-lg hover:scale-105 transition-all duration-400 ease-out",
        className,
      )}
    >
      <div className="flex flex-row transition-all duration-200 justify-between px-4 sm:px-6 py-4 sm:py-5">
        <div className="flex flex-col gap-1 flex-1 mr-2">
          <h2 className="text-lg tracking-tight leading-tight font-medium">
            {title}
          </h2>
          {description && (
            <p className="text-foreground/50 text-sm">{description}</p>
          )}
          {children && <div className="mt-2">{children}</div>}
        </div>

        {price && badgeLabel ? (
          <div className="inline-flex h-fit items-center text-sm whitespace-nowrap shrink-0">
            <span className="rounded-l-full bg-secondary h-fit py-1 px-2 font-medium">
              {price}
            </span>
            <span
              className={cn(
                "rounded-r-full text-sm h-fit py-1 px-2 font-medium",
                BADGE_LABEL_CLASSES[badgeVariant],
              )}
            >
              {badgeLabel}
            </span>
          </div>
        ) : price ? (
          <span className="h-fit rounded-full bg-secondary px-3 py-1 text-sm font-medium whitespace-nowrap shrink-0">
            {price}
          </span>
        ) : null}
      </div>

      {imageSrc && (
        <img
          src={imageSrc}
          alt={imageAlt}
          width={288}
          height={224}
          loading="lazy"
          decoding="async"
          className={cn(
            "absolute z-10 top-27 w-72 -right-10",
            "rotate-[-5deg] border-border border rounded-md",
            "transition-transform duration-300 ease-out",
            "group-hover:-rotate-3 group-hover:-translate-y-1 group-hover:-translate-x-0.5",
          )}
        />
      )}

      <ClippedCircle circleClassName="bg-white" circleSize={800} />
    </Tilt>
  );

  if (href) {
    return (
      <a
        href={href}
        className="block cursor-pointer"
        {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
      >
        {inner}
      </a>
    );
  }

  return <div {...props}>{inner}</div>;
}

Update the import paths to match your project setup.

Usage

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

<TiltCard
  title="Starter Kit"
  description="Everything you need to ship fast."
  badge="Free"
  imageSrc="/preview.png"
  href="/templates/starter"
/>;

Custom children

Content passed as children is rendered below the description — useful for feature lists, CTAs, or meta info.

<TiltCard title="Pro" badge="$49" badgeVariant="warning">
  <ul className="text-sm text-muted-foreground space-y-1">
    <li>✓ Unlimited projects</li>
    <li>✓ Priority support</li>
  </ul>
</TiltCard>

Props

PropTypeDefaultDescription
titlestringCard heading
descriptionstringSubtitle text
badgestringBadge label (omit to hide)
badgeVariant"default" | "success" | "warning""default"Badge colour scheme
imageSrcstringFloating image URL
imageAltstring""Alt text for the image
hrefstringWraps the card in an <a> tag
tiltPropsOmit<TiltProps, "children" | "className">Props forwarded to the inner <Tilt>
classNamestringExtra classes on the card surface

Built by Léo from Unlumen :3

Last updated: 3/15/2026