Skip to content
← Components

Spotlight Card

Spotlight Card brings controlled expression to your interface.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/spotlight-card.json

Use it

Scroll horizontally for long lines
"use client";
import { SpotlightCard } from "@/components/jez-ui/ui/spotlight-card";
import { Plus } from "lucide-react";

export default function Example() {
  return (
    <>
      <SpotlightCard className="w-full max-w-sm border-0 bg-foreground p-8 text-background">
        <div className="flex min-h-64 flex-col justify-between">
          <Plus size={32} />
          <h3 className="text-4xl leading-tight tracking-tight">
            The detail
            <br />
            is the difference.
          </h3>
          <p className="text-xs opacity-70">
            Follow your curiosity. And the light.
          </p>
        </div>
      </SpotlightCard>
    </>
  );
}

Props & types

Scroll horizontally for long lines
Omit<
  React.ComponentProps<"div">,
  keyof {
    children: React.ReactNode;
    className?: string;
  }
> & {
  children: React.ReactNode;
  className?: string;
}

Source

View 2 source files

registry/ui/spotlight-card.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
export function SpotlightCard({
  children,
  className,
  ...rootProps
}: Omit<
  React.ComponentProps<"div">,
  keyof {
    children: React.ReactNode;
    className?: string;
  }
> & {
  children: React.ReactNode;
  className?: string;
}) {
  const [point, setPoint] = React.useState({ x: 50, y: 50 });
  return (
    <div
      {...rootProps}
      onPointerMove={(e) => {
        rootProps.onPointerMove?.(e);
        if (e.defaultPrevented) return;

        const r = e.currentTarget.getBoundingClientRect();
        setPoint({
          x: ((e.clientX - r.left) / r.width) * 100,
          y: ((e.clientY - r.top) / r.height) * 100,
        });
      }}
      className={cn(
        "relative overflow-hidden rounded-xl border border-border p-8",
        className,
      )}
    >
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 opacity-20"
        style={{
          background: `radial-gradient(circle at ${point.x}% ${point.y}%,var(--primary),transparent 65%)`,
        }}
      />
      <div className="relative">{children}</div>
    </div>
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

Dependencies

clsx@2.1.1 · tailwind-merge@3.5.0 · lucide-react@0.577.0

Accessibility & behaviour

Respect reduced-motion preferences. Decorative effects must not carry essential information. WebGL scenes include a static fallback and only render while visible.

Read the accessibility and performance guide →