UI Components Easy
Magic Card
Card with a radial gradient spotlight that follows the mouse cursor, creating an interactive lighting effect.
Open in Lab
MCP
css javascript svelte vue
Targets: TS JS HTML React Svelte Vue
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
padding: 2rem;
}
/* --- Magic Grid --- */
.magic-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
width: min(740px, calc(100vw - 2rem));
}
/* --- Magic Card --- */
.magic-card {
--mouse-x: 50%;
--mouse-y: 50%;
--magic-color: rgba(120, 120, 255, 0.15);
--magic-radius: 250px;
position: relative;
border-radius: 1rem;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
cursor: default;
transition: border-color 0.3s ease;
}
.magic-card:hover {
border-color: rgba(255, 255, 255, 0.15);
}
/* Spotlight overlay */
.magic-card__spotlight {
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
transition: opacity 0.3s ease;
background: radial-gradient(
var(--magic-radius) circle at var(--mouse-x) var(--mouse-y),
var(--magic-color),
transparent 100%
);
pointer-events: none;
z-index: 0;
}
.magic-card:hover .magic-card__spotlight {
opacity: 1;
}
/* Border glow on hover */
.magic-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
transition: opacity 0.3s ease;
background: radial-gradient(
600px circle at var(--mouse-x) var(--mouse-y),
rgba(120, 120, 255, 0.08),
transparent 100%
);
pointer-events: none;
z-index: 0;
}
.magic-card:hover::before {
opacity: 1;
}
/* Card content */
.magic-card__content {
position: relative;
z-index: 1;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.magic-icon {
font-size: 1.5rem;
color: #818cf8;
line-height: 1;
}
.magic-title {
font-size: 1.125rem;
font-weight: 700;
color: #f1f5f9;
letter-spacing: -0.01em;
}
.magic-body {
font-size: 0.875rem;
line-height: 1.6;
color: #94a3b8;
}// Magic Card — radial gradient spotlight follows the mouse cursor.
(function () {
"use strict";
const cards = document.querySelectorAll("[data-magic-card]");
cards.forEach(function (card) {
card.addEventListener("mousemove", function (e) {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
card.style.setProperty("--mouse-x", x + "px");
card.style.setProperty("--mouse-y", y + "px");
});
card.addEventListener("mouseleave", function () {
card.style.setProperty("--mouse-x", "50%");
card.style.setProperty("--mouse-y", "50%");
});
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Magic Card</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="magic-grid">
<div class="magic-card" data-magic-card>
<div class="magic-card__spotlight"></div>
<div class="magic-card__content">
<div class="magic-icon">◆</div>
<h3 class="magic-title">Interactive</h3>
<p class="magic-body">Move your mouse over this card to see the spotlight follow your cursor.</p>
</div>
</div>
<div class="magic-card" data-magic-card>
<div class="magic-card__spotlight"></div>
<div class="magic-card__content">
<div class="magic-icon">★</div>
<h3 class="magic-title">Responsive</h3>
<p class="magic-body">Each card tracks the cursor independently with its own radial gradient.</p>
</div>
</div>
<div class="magic-card" data-magic-card>
<div class="magic-card__spotlight"></div>
<div class="magic-card__content">
<div class="magic-icon">♦</div>
<h3 class="magic-title">Customizable</h3>
<p class="magic-body">Adjust colors, radius, and intensity using CSS custom properties.</p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useRef, useState, type CSSProperties, type ReactNode } from "react";
interface MagicCardProps {
children: ReactNode;
spotlightColor?: string;
spotlightRadius?: number;
className?: string;
}
export function MagicCard({
children,
spotlightColor = "rgba(120, 120, 255, 0.15)",
spotlightRadius = 250,
className = "",
}: MagicCardProps) {
const cardRef = useRef<HTMLDivElement>(null);
const [mousePos, setMousePos] = useState({ x: "50%", y: "50%" });
const [isHovered, setIsHovered] = useState(false);
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
const card = cardRef.current;
if (!card) return;
const rect = card.getBoundingClientRect();
setMousePos({
x: `${e.clientX - rect.left}px`,
y: `${e.clientY - rect.top}px`,
});
}
const cardStyle: CSSProperties = {
position: "relative",
borderRadius: "1rem",
overflow: "hidden",
border: `1px solid rgba(255, 255, 255, ${isHovered ? 0.15 : 0.08})`,
background: "rgba(255, 255, 255, 0.03)",
cursor: "default",
transition: "border-color 0.3s ease",
};
const spotlightStyle: CSSProperties = {
position: "absolute",
inset: 0,
borderRadius: "inherit",
opacity: isHovered ? 1 : 0,
transition: "opacity 0.3s ease",
background: `radial-gradient(${spotlightRadius}px circle at ${mousePos.x} ${mousePos.y}, ${spotlightColor}, transparent 100%)`,
pointerEvents: "none",
zIndex: 0,
};
return (
<div
ref={cardRef}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setIsHovered(false);
setMousePos({ x: "50%", y: "50%" });
}}
style={cardStyle}
className={className}
>
<div style={spotlightStyle} />
<div style={{ position: "relative", zIndex: 1 }}>{children}</div>
</div>
);
}
// Demo usage
export default function MagicCardDemo() {
const items = [
{
icon: "\u2666",
title: "Interactive",
body: "Move your mouse over this card to see the spotlight follow your cursor.",
},
{
icon: "\u2733",
title: "Responsive",
body: "Each card tracks the cursor independently with its own radial gradient.",
},
{
icon: "\u2726",
title: "Customizable",
body: "Adjust colors, radius, and intensity using simple props.",
},
];
return (
<div
style={{
minHeight: "100vh",
display: "grid",
placeItems: "center",
background: "#0a0a0a",
fontFamily: "system-ui, -apple-system, sans-serif",
padding: "2rem",
}}
>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
gap: "1.5rem",
width: "min(740px, calc(100vw - 2rem))",
}}
>
{items.map((item) => (
<MagicCard key={item.title}>
<div
style={{
padding: "2rem",
display: "flex",
flexDirection: "column",
gap: "0.75rem",
}}
>
<span style={{ fontSize: "1.5rem", color: "#818cf8", lineHeight: 1 }}>
{item.icon}
</span>
<h3
style={{
fontSize: "1.125rem",
fontWeight: 700,
color: "#f1f5f9",
letterSpacing: "-0.01em",
margin: 0,
}}
>
{item.title}
</h3>
<p
style={{
fontSize: "0.875rem",
lineHeight: 1.6,
color: "#94a3b8",
margin: 0,
}}
>
{item.body}
</p>
</div>
</MagicCard>
))}
</div>
</div>
);
}<script>
export let spotlightColor = "rgba(120, 120, 255, 0.15)";
export let spotlightRadius = 250;
let mouseX = "50%";
let mouseY = "50%";
let isHovered = false;
let cardEl;
function handleMouseMove(e) {
if (!cardEl) return;
const rect = cardEl.getBoundingClientRect();
mouseX = `${e.clientX - rect.left}px`;
mouseY = `${e.clientY - rect.top}px`;
}
function handleMouseEnter() {
isHovered = true;
}
function handleMouseLeave() {
isHovered = false;
mouseX = "50%";
mouseY = "50%";
}
$: spotlightStyle = `position: absolute; inset: 0; border-radius: inherit; opacity: ${isHovered ? 1 : 0}; transition: opacity 0.3s ease; background: radial-gradient(${spotlightRadius}px circle at ${mouseX} ${mouseY}, ${spotlightColor}, transparent 100%); pointer-events: none; z-index: 0;`;
$: cardBorder = isHovered ? "rgba(255, 255, 255, 0.15)" : "rgba(255, 255, 255, 0.08)";
const items = [
{
icon: "\u2666",
title: "Interactive",
body: "Move your mouse over this card to see the spotlight follow your cursor.",
},
{
icon: "\u2733",
title: "Responsive",
body: "Each card tracks the cursor independently with its own radial gradient.",
},
{
icon: "\u2726",
title: "Customizable",
body: "Adjust colors, radius, and intensity using simple props.",
},
];
</script>
<div
style="min-height: 100vh; display: grid; place-items: center; background: #0a0a0a; font-family: system-ui, -apple-system, sans-serif; padding: 2rem;"
>
<div
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.5rem; width: min(740px, calc(100vw - 2rem));"
>
{#each items as item}
<div
bind:this={cardEl}
on:mousemove={handleMouseMove}
on:mouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave}
style="position: relative; border-radius: 1rem; overflow: hidden; border: 1px solid {cardBorder}; background: rgba(255, 255, 255, 0.03); cursor: default; transition: border-color 0.3s ease;"
>
<div style={spotlightStyle}></div>
<div style="position: relative; z-index: 1;">
<div
style="padding: 2rem; display: flex; flex-direction: column; gap: 0.75rem;"
>
<span style="font-size: 1.5rem; color: #818cf8; line-height: 1;">
{item.icon}
</span>
<h3
style="font-size: 1.125rem; font-weight: 700; color: #f1f5f9; letter-spacing: -0.01em; margin: 0;"
>
{item.title}
</h3>
<p
style="font-size: 0.875rem; line-height: 1.6; color: #94a3b8; margin: 0;"
>
{item.body}
</p>
</div>
</div>
</div>
{/each}
</div>
</div><script setup>
import { ref, computed } from "vue";
const props = defineProps({
spotlightColor: { type: String, default: "rgba(120, 120, 255, 0.15)" },
spotlightRadius: { type: Number, default: 250 },
});
const items = [
{
icon: "\u2666",
title: "Interactive",
body: "Move your mouse over this card to see the spotlight follow your cursor.",
},
{
icon: "\u2733",
title: "Responsive",
body: "Each card tracks the cursor independently with its own radial gradient.",
},
{
icon: "\u2726",
title: "Customizable",
body: "Adjust colors, radius, and intensity using simple props.",
},
];
// Each card gets its own mouse state
const cardStates = ref(items.map(() => ({ x: "50%", y: "50%", hovered: false })));
function handleMouseMove(e, i) {
const rect = e.currentTarget.getBoundingClientRect();
cardStates.value[i].x = `${e.clientX - rect.left}px`;
cardStates.value[i].y = `${e.clientY - rect.top}px`;
}
function handleMouseEnter(i) {
cardStates.value[i].hovered = true;
}
function handleMouseLeave(i) {
cardStates.value[i].hovered = false;
cardStates.value[i].x = "50%";
cardStates.value[i].y = "50%";
}
</script>
<template>
<div class="demo-wrapper">
<div class="grid-container">
<div
v-for="(item, i) in items"
:key="item.title"
class="magic-card"
:style="{
borderColor: cardStates[i].hovered ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.08)',
}"
@mousemove="handleMouseMove($event, i)"
@mouseenter="handleMouseEnter(i)"
@mouseleave="handleMouseLeave(i)"
>
<div
class="spotlight"
:style="{
opacity: cardStates[i].hovered ? 1 : 0,
background: `radial-gradient(${props.spotlightRadius}px circle at ${cardStates[i].x} ${cardStates[i].y}, ${props.spotlightColor}, transparent 100%)`,
}"
></div>
<div style="position: relative; z-index: 1;">
<div class="card-content">
<span class="card-icon">{{ item.icon }}</span>
<h3 class="card-title">{{ item.title }}</h3>
<p class="card-body">{{ item.body }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.demo-wrapper {
min-height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
font-family: system-ui, -apple-system, sans-serif;
padding: 2rem;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
width: min(740px, calc(100vw - 2rem));
}
.magic-card {
position: relative;
border-radius: 1rem;
overflow: hidden;
border: 1px solid;
background: rgba(255, 255, 255, 0.03);
cursor: default;
transition: border-color 0.3s ease;
}
.spotlight {
position: absolute;
inset: 0;
border-radius: inherit;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 0;
}
.card-content {
padding: 2rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card-icon {
font-size: 1.5rem;
color: #818cf8;
line-height: 1;
}
.card-title {
font-size: 1.125rem;
font-weight: 700;
color: #f1f5f9;
letter-spacing: -0.01em;
margin: 0;
}
.card-body {
font-size: 0.875rem;
line-height: 1.6;
color: #94a3b8;
margin: 0;
}
</style>Magic Card
A card where a radial gradient spotlight follows the mouse cursor position, creating an interactive, magical lighting effect that responds to user movement.
How it works
- JavaScript listens for
mousemoveevents on the card - The cursor position relative to the card is calculated
- CSS custom properties
--mouse-xand--mouse-yare updated in real time - A
radial-gradientoverlay uses these properties to position the spotlight
Customization
- Change spotlight color with
--magic-color(default: rgba(120, 120, 255, 0.15)) - Adjust spotlight radius with
--magic-radius(default: 250px) - Multiple cards can be used in a grid — each tracks independently
When to use it
- Feature showcase grids
- Interactive card-based navigation
- Portfolio or project listing pages