A card with a two-color light that follows the pointer along its border, with an optional soft glow inside, driven by CSS variables so moving never re-renders, and a steady light on keyboard focus.
Fast by default
Every component ships with the smallest runtime it can.
Composable
Small parts that fit together instead of one giant API.
Accessible
Keyboard and screen reader support is part of the code.
import { Gauge, Layers, ShieldCheck } from "lucide-react"
import { MagicCard } from "@/components/ballmac/magic-card"
const features = [
{ icon: Gauge, title: "Fast by default", body: "Every component ships with the smallest runtime it can." },
{ icon: Layers, title: "Composable", body: "Small parts that fit together instead of one giant API." },
{ icon: ShieldCheck, title: "Accessible", body: "Keyboard and screen reader support is part of the code." },
]
export default function MagicCardDemo() {
return (
<div className="grid w-full max-w-2xl gap-3 sm:grid-cols-3">
{features.map(({ icon: Icon, title, body }) => (
<MagicCard key={title} contentClassName="p-5" tabIndex={0}>
<Icon aria-hidden="true" className="size-5 text-foreground" />
<h3 className="mt-3 text-sm font-semibold">{title}</h3>
<p className="mt-1 text-[13px] leading-5 text-muted-foreground">{body}</p>
</MagicCard>
))}
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/magic-cardInstall the dependencies.
$ pnpm add motion@^12Copy the source into your project.
components/ballmac/magic-card.tsx// Ballmac UI: Magic Card. https://ui.ballmac.com/components/magic-card "use client" import * as React from "react" import { useReducedMotion } from "motion/react" import { cn } from "@/lib/utils" type MagicTone = "chart-1" | "chart-2" | "chart-3" | "chart-4" | "chart-5" | "primary" const VAR: Record<MagicTone, string> = { "chart-1": "var(--chart-1)", "chart-2": "var(--chart-2)", "chart-3": "var(--chart-3)", "chart-4": "var(--chart-4)", "chart-5": "var(--chart-5)", primary: "var(--primary)", } type MagicCardProps = React.ComponentProps<"div"> & { /** Radius of the light that follows the pointer, in pixels. */ size?: number /** First color of the border light. */ from?: MagicTone /** Second color of the border light. */ to?: MagicTone /** Also light the inside of the card faintly. */ glow?: boolean /** Classes for the inner surface. */ contentClassName?: string } function MagicCard({ size = 260, from = "chart-1", to = "chart-4", glow = true, className, contentClassName, children, onPointerMove, onPointerLeave, onFocus, onBlur, ...props }: MagicCardProps) { const reduce = useReducedMotion() const ref = React.useRef<HTMLDivElement>(null) // The pointer position goes straight into CSS variables, so moving never re-renders React. const place = React.useCallback((x: number, y: number, on: boolean) => { const el = ref.current if (!el) return el.style.setProperty("--mx", `${x}px`) el.style.setProperty("--my", `${y}px`) el.style.setProperty("--m-on", on ? "1" : "0") }, []) return ( <div ref={ref} data-slot="magic-card" onPointerMove={(e) => { onPointerMove?.(e) if (reduce) return const box = e.currentTarget.getBoundingClientRect() place(e.clientX - box.left, e.clientY - box.top, true) }} onPointerLeave={(e) => { onPointerLeave?.(e) place(0, 0, false) }} onFocus={(e) => { onFocus?.(e) const box = e.currentTarget.getBoundingClientRect() place(box.width / 2, box.height / 2, true) }} onBlur={(e) => { onBlur?.(e) place(0, 0, false) }} className={cn("group/magic relative isolate rounded-xl p-px [--m-on:0]", className)} style={ { "--from": VAR[from], "--to": VAR[to], "--size": `${size}px`, background: "radial-gradient(var(--size) circle at var(--mx, -999px) var(--my, -999px), var(--from), var(--to) 45%, transparent 75%), var(--border)", } as React.CSSProperties } {...props} > <div className={cn("relative h-full overflow-hidden rounded-[calc(var(--radius-xl)-1px)] bg-card text-card-foreground", contentClassName)}> {glow && ( <span aria-hidden="true" className="pointer-events-none absolute inset-0 opacity-[calc(var(--m-on)*0.5)] transition-opacity duration-300 motion-reduce:transition-none" style={{ background: "radial-gradient(var(--size) circle at var(--mx, -999px) var(--my, -999px), color-mix(in oklab, var(--from) 18%, transparent), transparent 70%)" }} /> )} <div className="relative">{children}</div> </div> </div> ) } export { MagicCard, type MagicCardProps, type MagicTone }Update the import paths to match your project setup.
Usage
import { MagicCard } from "@/components/ballmac/magic-card"The full example is in the Code tab above.
Examples
Color pairs
chart-1 → chart-4
Move over me
chart-2 → chart-1
Move over me
chart-3 → chart-5
Move over me
chart-4 → chart-3
Move over me
import { MagicCard, type MagicTone } from "@/components/ballmac/magic-card"
const pairs: [MagicTone, MagicTone][] = [
["chart-1", "chart-4"],
["chart-2", "chart-1"],
["chart-3", "chart-5"],
["chart-4", "chart-3"],
]
export default function MagicCardColors() {
return (
<div className="grid w-full max-w-md grid-cols-2 gap-3">
{pairs.map(([from, to]) => (
<MagicCard key={from + to} from={from} to={to} size={200} contentClassName="p-5" tabIndex={0}>
<p className="font-mono text-xs text-muted-foreground">
{from} → {to}
</p>
<p className="mt-1 text-sm font-medium">Move over me</p>
</MagicCard>
))}
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
sizeRadius of the light that follows the pointer, in pixels. | number | 260 |
fromFirst color of the border light. | MagicTone | "chart-1" |
toSecond color of the border light. | MagicTone | "chart-4" |
glowAlso light the inside of the card faintly. | boolean | true |
contentClassNameClasses for the inner surface. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Focus | A centered light shows when anything inside has focus |
| Reduced motion | No pointer tracking; a plain border remains |
Use with AI
<MagicCard from to size glow contentClassName>content</MagicCard>. from and to are chart tokens. Use spotlight-card for a single soft glow and this for a colored border light. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Magic Card (@ballmac/magic-card) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Feature and pricing cards on dark or light pages
- Any card grid that should feel responsive to the pointer
Not for
- Dense data UI
- Cards that are not interactive
Registry JSON: https://ui.ballmac.com/r/magic-card.json
Credits
Free to use in personal and commercial projects.
- npm
- motion@^12
- Registry
- shadcn/utils
Pairs well with
Spotlight Card
A card whose surface and border pick up a soft glow that follows the pointer, driven by CSS variables with no re-render per move. Keyboard focus shows a steady glow.
Tilt Card
A card that tilts in 3D toward the pointer with spring smoothing and a moving glare. TiltCardLayer children float at their own depth for parallax. Keyboard focus shows a gentle tilt.
Bento Grid
A responsive bento layout of feature cards that span columns and rows. Each BentoCard has a background visual slot, icon, title and description, and a link that slides up on hover or focus.
Animated Beam
An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.