卡片发光效果
HTML
<div id="app"></div>
CSS
:root {
--backdrop: hsl(0 0% 60% / 0.12);
--radius: 14;
--border: 3;
--backup-border: var(--backdrop);
--size: 200;
}
article:first-of-type {
--base: 80;
--spread: 500;
--outer: 1;
}
article:last-of-type {
--outer: 1;
--base: 220;
--spread: 200;
}
*,
*:after,
*:before {
box-sizing: border-box;
}
body {
display: grid;
place-items: center;
min-height: 100vh;
overflow: hidden;
background: hsl(0 0% 4%);
}
.wrapper {
position: relative;
}
article {
aspect-ratio: 3 / 4;
border-radius: calc(var(--radius) * 1px);
width: 260px;
position: relative;
grid-template-rows: 1fr auto;
box-shadow: 0 1rem 2rem -1rem black;
padding: 1rem;
display: grid;
border: 1px solid hsl(0 0% 100% / 0.15);
backdrop-filter: blur(calc(var(--cardblur, 5) * 1px));
/* For demo purposes. Means you get the effect on mobile */
touch-action: none;
}
main {
display: flex;
gap: 2rem;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 120ch;
max-width: calc(100vw - 2rem);
position: relative;
}
/* Glow specific styles */
[data-glow] {
--border-size: calc(var(--border, 2) * 1px);
--spotlight-size: calc(var(--size, 150) * 1px);
--hue: calc(var(--base) + (var(--xp, 0) * var(--spread, 0)));
background-image: radial-gradient(
var(--spotlight-size) var(--spotlight-size) at calc(var(--x, 0) * 1px) calc(
var(--y, 0) * 1px
),
hsl(
var(--hue, 210) calc(var(--saturation, 100) * 1%) calc(
var(--lightness, 70) * 1%
) / var(--bg-spot-opacity, 0.1)
),
transparent
);
background-color: var(--backdrop, transparent);
background-size: calc(100% + (2 * var(--border-size))) calc(
100% + (2 * var(--border-size))
);
background-position: 50% 50%;
background-attachment: fixed;
border: var(--border-size) solid var(--backup-border);
position: relative;
touch-action: none;
}
[data-glow]::before,
[data-glow]::after {
pointer-events: none;
content: "";
position: absolute;
inset: calc(var(--border-size) * -1);
border: var(--border-size) solid transparent;
border-radius: calc(var(--radius) * 1px);
background-attachment: fixed;
background-size: calc(100% + (2 * var(--border-size))) calc(
100% + (2 * var(--border-size))
);
background-repeat: no-repeat;
background-position: 50% 50%;
mask: linear-gradient(transparent, transparent), linear-gradient(white, white);
mask-clip: padding-box, border-box;
mask-composite: intersect;
}
/* This is the emphasis light */
[data-glow]::before {
background-image: radial-gradient(
calc(var(--spotlight-size) * 0.75) calc(var(--spotlight-size) * 0.75) at
calc(var(--x, 0) * 1px) calc(var(--y, 0) * 1px),
hsl(
var(--hue, 210) calc(var(--saturation, 100) * 1%) calc(
var(--lightness, 50) * 1%
) / var(--border-spot-opacity, 1)
),
transparent 100%
);
filter: brightness(2);
}
/* This is the spotlight */
[data-glow]::after {
background-image: radial-gradient(
calc(var(--spotlight-size) * 0.5) calc(var(--spotlight-size) * 0.5) at calc(
var(--x, 0) * 1px
) calc(var(--y, 0) * 1px),
hsl(0 100% 100% / var(--border-light-opacity, 1)),
transparent 100%
);
}
[data-glow] > [data-glow]:not(:is(a, button)) {
position: absolute;
inset: 0;
will-change: filter;
opacity: var(--outer, 1);
}
[data-glow] > [data-glow]:not(:is(a, button)) {
border-radius: calc(var(--radius) * 1px);
border-width: calc(var(--border-size) * 20);
filter: blur(calc(var(--border-size) * 10));
background: none;
pointer-events: none;
}
[data-glow] > [data-glow]:not(:is(a, button))::before {
inset: -10px;
border-width: 10px;
}
[data-glow] > [data-glow] {
border: none;
}
[data-glow] :is(a, button) {
border-radius: calc(var(--radius) * 1px);
border: var(--border-size) solid transparent;
}
[data-glow] :is(a, button) [data-glow] {
background: none;
}
[data-glow] :is(a, button) [data-glow]::before {
inset: calc(var(--border-size) * -1);
border-width: calc(var(--border-size) * 1);
}
article button {
padding: 0.75rem 2rem;
align-self: end;
color: hsl(0 0% 80%);
}
button[data-glow] span {
font-weight: bold;
background-image: radial-gradient(
var(--spotlight-size) var(--spotlight-size) at calc(var(--x, 0) * 1px) calc(
var(--y, 0) * 1px
),
hsl(
var(--hue, 210) calc(var(--saturation, 100) * 1%) calc(
var(--lightness, 70) * 1%
) / var(--bg-spot-opacity, 1)
),
transparent
);
background-color: var(--backdrop, transparent);
background-position: 50% 50%;
background-attachment: fixed;
background-clip: text;
filter: brightness(1.5);
color: transparent;
}
JavaScript
import React from "https://cdn.skypack.dev/react";
import { render } from "https://cdn.skypack.dev/react-dom";
const ROOT_NODE = document.querySelector("#app");
/**
* Tiny hook that you can use where you need it
*/
const usePointerGlow = () => {
const [status, setStatus] = React.useState(null);
React.useEffect(() => {
const syncPointer = ({ x: pointerX, y: pointerY }) => {
const x = pointerX.toFixed(2);
const y = pointerY.toFixed(2);
const xp = (pointerX / window.innerWidth).toFixed(2);
const yp = (pointerY / window.innerHeight).toFixed(2);
document.documentElement.style.setProperty("--x", x);
document.documentElement.style.setProperty("--xp", xp);
document.documentElement.style.setProperty("--y", y);
document.documentElement.style.setProperty("--yp", yp);
setStatus({ x, y, xp, yp });
};
document.body.addEventListener("pointermove", syncPointer);
return () => {
document.body.removeEventListener("pointermove", syncPointer);
};
}, []);
return [status];
};
const App = () => {
const [status] = usePointerGlow();
return (
<main>
<article data-glow>
<span data-glow />
<button data-glow>
<span>Glow Up</span>
</button>
</article>
<article data-glow>
<span data-glow />
<button data-glow>
<span>Glow Up</span>
</button>
</article>
<article data-glow>
<span data-glow />
<button data-glow>
<span>Glow Up</span>
</button>
</article>
</main>
);
};
render(<App />, ROOT_NODE);