<main>
<header>
<h1>Custom Cursor Pop-up</h1>
</header>
<article>
<p>
Here, a <code>popup</code> displays a custom cursor. Using the top layer, we can put the cursor above everything else. If we need to display another pop-up, we can listen for the event and hide and reshow the cursor popup. Try showing a pop-up above it.
</p>
<div>
<button class="button ripple" popovertoggletarget="overlay-pop-up">Show Pop-up</button>
</div>
</article>
</main>
<div id="overlay-pop-up" popover>
<div class="card elevated">
You can still see the cursor above me and my <code>::backdrop</code>!
</div>
</div>
<canvas id="custom-cursor" class="custom-cursor" popover="manual"></canvas>
@layer demo {
#overlay-pop-up {
border: 0;
background: none;
max-width: calc(100% - (2 * var(--size-4)));
min-width: 0;
}
#overlay-pop-up::backdrop {
background: hsl(0 0% 0% / 0.25);
backdrop-filter: blur(4px);
}
#overlay-pop-up .card {
padding: var(--size-4);
display: block;
min-width: 0;
line-height: 1.5;
background: var(--surface-1);
color: var(--text-1);
}
}
@layer base {
*,
*:after,
*:before {
box-sizing: border-box;
/*cursor: none;*/
}
body {
min-height: 100vh;
background: var(--surface-1);
display: block;
font-family: "Google Sans", sans-serif, system-ui;
}
:where([popover]) {
margin: auto;
border-width: 0;
border-style: none;
background: transparent;
}
canvas {
height: 100vh;
width: 100vw;
pointer-events: none;
display: none;
position: fixed;
}
canvas:open {
display: block;
}
h1 {
margin: 0;
color: var(--gray-0);
}
.button {
/*cursor: none;*/
}
header {
height: 25vmin;
min-height: 200px;
background: var(--gradient-23);
display: grid;
place-items: center;
color: var(--gray-0);
padding: var(--size-4);
grid-template-columns: 1fr;
}
main {
margin: 0 auto;
}
article {
padding: var(--size-4);
display: flex;
flex-direction: column;
align-items: center;
}
article > * + * {
margin-top: var(--size-4);
}
}
const mapRange = (inputLower, inputUpper, outputLower, outputUpper) => {
const INPUT_RANGE = inputUpper - inputLower;
const OUTPUT_RANGE = outputUpper - outputLower;
return (value) =>
outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0);
};
const clamp = (min, max, value) => Math.min(Math.max(value, min), max);
let canvas;
let context;
const blocks = [];
const createBlock = ({ x, y, movementX, movementY }) => {
const inactiveBlocks = blocks.filter((block) => block.active === false);
const size = Math.floor(
mapRange(0, 100, 10, 50)(Math.max(Math.abs(movementX), Math.abs(movementY)))
);
if (inactiveBlocks.length !== 0) {
// Reuse a block
inactiveBlocks[0].active = true;
inactiveBlocks[0].size = size;
inactiveBlocks[0].x = x - size * 0.5;
inactiveBlocks[0].y = y - size * 0.5;
inactiveBlocks[0].hue = Math.floor(Math.random() * 359);
} else {
const block = {
active: true,
hue: Math.floor(Math.random() * 359),
x: x - size * 0.5,
y: y - size * 0.5,
size
};
blocks.push(block);
}
};
const drawBlocks = () => {
context.clearRect(0, 0, canvas.width, canvas.height);
for (const block of blocks.filter((block) => block.active)) {
context.strokeStyle = context.fillStyle = `hsl(${block.hue}, 80%, 80%)`;
context.beginPath();
context.arc(block.x, block.y, block.size * 0.5, 0, 2 * Math.PI);
context.stroke();
context.fill();
block.size -= 1;
if (block.size === 0) {
block.active = false;
}
}
requestAnimationFrame(drawBlocks);
};
const setUp = () => {
canvas = document.querySelector("canvas");
context = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.addEventListener("pointermove", createBlock);
// Listen for any pop-ups that are opened and make sure the canvas sits higher
document.body.addEventListener("beforetoggle", (e) => {
if (e.newState === "open" && e.target !== canvas) {
requestAnimationFrame(() => {
canvas.hidePopover();
canvas.showPopover();
});
}
});
requestAnimationFrame(drawBlocks);
};
setUp();
window.addEventListener("resize", () => {
if (canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
});
canvas.showPopover();