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"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:
"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
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Card heading |
description | string | — | Subtitle text |
badge | string | — | Badge label (omit to hide) |
badgeVariant | "default" | "success" | "warning" | "default" | Badge colour scheme |
imageSrc | string | — | Floating image URL |
imageAlt | string | "" | Alt text for the image |
href | string | — | Wraps the card in an <a> tag |
tiltProps | Omit<TiltProps, "children" | "className"> | — | Props forwarded to the inner <Tilt> |
className | string | — | Extra classes on the card surface |