Browse the library
Components / Cards
Spotlight card
FreeCard with a soft radial spotlight and a glowing border that both follow the cursor.
View as Markdownmotion
components/facade/spotlight-card.tsx
"use client";
import * as React from "react";
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
export interface SpotlightCardProps extends React.HTMLAttributes<HTMLDivElement> {
/** Spotlight radius in px. Defaults to 360. */
size?: number;
/** Border glow radius in px. Defaults to 220. */
borderSize?: number;
/** Spotlight strength as a percentage of brand color. Defaults to 14. */
intensity?: number;
className?: string;
children?: React.ReactNode;
}
export function SpotlightCard({
size = 360,
borderSize = 220,
intensity = 14,
className,
children,
onPointerMove,
onPointerEnter,
onPointerLeave,
...rest
}: SpotlightCardProps) {
const reduce = useReducedMotion();
const x = useMotionValue(-1000);
const y = useMotionValue(-1000);
const [active, setActive] = React.useState(false);
const spot = useMotionTemplate`radial-gradient(${size}px circle at ${x}px ${y}px, color-mix(in oklch, var(--brand) ${intensity}%, transparent), transparent 70%)`;
const ring = useMotionTemplate`radial-gradient(${borderSize}px circle at ${x}px ${y}px, color-mix(in oklch, var(--brand) 85%, transparent), transparent 65%)`;
return (
<div
{...rest}
onPointerMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
x.set(e.clientX - rect.left);
y.set(e.clientY - rect.top);
onPointerMove?.(e);
}}
onPointerEnter={(e) => {
setActive(true);
onPointerEnter?.(e);
}}
onPointerLeave={(e) => {
setActive(false);
onPointerLeave?.(e);
}}
className={cn(
"group/spotlight relative isolate overflow-hidden rounded-2xl border border-border bg-card text-card-foreground",
"shadow-[0_1px_2px_0_color-mix(in_oklch,var(--foreground)_5%,transparent)]",
className,
)}
>
{!reduce ? (
<>
{/* Border glow: a masked layer that only paints the 1px border ring. */}
<motion.div
aria-hidden
className="pointer-events-none absolute inset-0 z-10 rounded-[inherit] p-px transition-opacity duration-500"
style={{
background: ring,
opacity: active ? 1 : 0,
WebkitMask: "linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)",
mask: "linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)",
}}
/>
<motion.div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10 transition-opacity duration-500"
style={{ background: spot, opacity: active ? 1 : 0 }}
/>
</>
) : null}
{children}
</div>
);
}
export default SpotlightCard;app/page.tsx
import { ArrowUpRight, Calendar, Shield, Sparkles } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { SpotlightCard } from "@/components/facade/spotlight-card";
interface Feature {
icon: LucideIcon;
eyebrow: string;
title: string;
body: string;
meta: string;
}
const features: Feature[] = [
{
icon: Calendar,
eyebrow: "Same week",
title: "Book online in 60 seconds",
body: "Pick a time that works, including early mornings and Saturdays. Reminders by text.",
meta: "Open 7am to 7pm",
},
{
icon: Shield,
eyebrow: "No surprises",
title: "Upfront pricing, every visit",
body: "We confirm your insurance and share the exact cost before any treatment starts.",
meta: "In network with 20+ plans",
},
{
icon: Sparkles,
eyebrow: "Modern care",
title: "Whitening and Invisalign",
body: "Digital scans instead of molds, and a custom plan you can preview before you commit.",
meta: "Free smile consult",
},
];
export default function Demo() {
return (
<section className="w-full max-w-4xl">
<div className="mb-6 flex flex-wrap items-end justify-between gap-3">
<div>
<p className="font-mono text-xs uppercase tracking-[0.18em] text-brand">Bright Oak Dental, Austin</p>
<h3 className="mt-2 font-display text-2xl font-semibold tracking-tight sm:text-3xl">Dentistry without the dread</h3>
</div>
<a
href="#"
className="inline-flex items-center gap-1 rounded-md text-sm font-medium text-foreground underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
All services
<ArrowUpRight className="size-4" aria-hidden />
</a>
</div>
<div className="grid gap-4 md:grid-cols-3">
{features.map((f) => (
<SpotlightCard key={f.title} className="p-6">
<div className="flex items-center justify-between">
<span className="inline-flex size-10 items-center justify-center rounded-xl border border-border bg-background text-brand shadow-sm">
<f.icon className="size-[18px]" aria-hidden />
</span>
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">{f.eyebrow}</span>
</div>
<h4 className="mt-6 text-lg font-semibold tracking-tight">{f.title}</h4>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{f.body}</p>
<div className="mt-6 flex items-center gap-2 border-t border-border pt-4 text-xs text-muted-foreground">
<span className="size-1.5 rounded-full bg-success" aria-hidden />
{f.meta}
</div>
</SpotlightCard>
))}
</div>
</section>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/spotlight-card.jsonUsage
import { SpotlightCard } from "@/components/facade/spotlight-card";
export default function Page() {
return <SpotlightCard />;
}Props
SpotlightCardProps is exported, so you can extend or wrap it. Every prop has a default; <SpotlightCard /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| size | number | Spotlight radius in px. Defaults to 360. |
| borderSize | number | Border glow radius in px. Defaults to 220. |
| intensity | number | Spotlight strength as a percentage of brand color. Defaults to 14. |
| className | string | |
| children | React.ReactNode |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade spotlight card to this page and adapt the copy to our business.