Feature discovery, click through
by mnk
HTML
<div class="card" id="card">
<div class="hint">Click the button through the squircle hole</div>
<button id="underButton" class="under-button">Click me through the hole</button>
<svg id="overlaySvg" class="overlay-svg" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
<path id="overlayPath" d="" />
</svg>
</div>
CSS
:root{
--card-w: 100%;
--card-h: 260px;
}
body {
margin: 32px;
display: grid;
place-items: start center;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
background: #f3f4f6;
}
.card {
width: var(--card-w);
height: var(--card-h);
position: relative;
background: linear-gradient(135deg,#3b82f6,#06b6d4);
color: white;
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(2,6,23,0.25);
user-select: none;
}
/* Example element underneath the hole */
.under-button {
position: relative; /* keep above card background */
z-index: 0;
padding: 12px 18px;
font-size: 16px;
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.12);
color: white;
border-radius: 8px;
}
/* SVG overlay sits on top of the card */
.overlay-svg {
position: absolute;
inset: 0; /* fill the card exactly */
width: 100%;
height: 100%;
pointer-events: none; /* let the internal painted shape decide hit-testing */
}
/* The path is the painted black overlay (outer rect minus squircle). Only painted pixels receive pointer events */
#overlayPath {
fill: rgba(0,0,0,0.5);
fill-rule: evenodd; /* the inner squircle subpath will become a hole */
pointer-events: auto; /* painted pixels of this path catch pointer events; the hole does not */
}
/* small UI hint */
.hint {
position: absolute;
left: 12px;
top: 12px;
color: rgba(255,255,255,0.9);
font-size: 13px;
z-index: 2;
user-select: none;
pointer-events: none;
}
JavaScript
function superellipsePath(cx, cy, a, b, n, steps) {
let d = '';
for (let i = 0; i <= steps; i++) {
const t = (i / steps) * Math.PI * 2;
const c = Math.cos(t), s = Math.sin(t);
const x = cx + a * Math.sign(c) * Math.pow(Math.abs(c), 2 / n);
const y = cy + b * Math.sign(s) * Math.pow(Math.abs(s), 2 / n);
d += (i === 0 ? 'M ' : ' L ') + x.toFixed(3) + ' ' + y.toFixed(3);
}
d += ' Z';
return d;
}
const card = document.getElementById('card');
const svg = document.getElementById('overlaySvg');
const pathEl = document.getElementById('overlayPath');
function updateOverlay() {
const rect = card.getBoundingClientRect();
const w = Math.max(1, Math.round(rect.width));
const h = Math.max(1, Math.round(rect.height));
// Keep SVG coordinate space in pixels so path can be built in userSpaceOnUse.
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
svg.setAttribute('preserveAspectRatio', 'none');
// outer rectangle (clockwise)
const outer = `M 0 0 H ${w} V ${h} H 0 Z`;
// squircle params (change these to taste)
const cx = w / 2;
const cy = h / 2;
const holeDiameter = Math.min(w, h) * 0.36; // adjust percent for larger/smaller hole
const a = holeDiameter / 2;
const b = holeDiameter / 2;
const exponent = 4; // 4 -> typical squircle; 2 -> circle; >4 -> squarer
const steps = Math.max(64, Math.round(Math.max(a, b) * 1.2)); // adapt smoothness to size
const inner = superellipsePath(cx, cy, a, b, exponent, steps);
// combine: outer followed by inner. fill-rule="evenodd" on the path makes inner a hole.
pathEl.setAttribute('d', outer + ' ' + inner);
}
// On the overlay path, stop pointer events from bubbling to the card.
// Using pointer events covers mouse/touch/stylus in modern browsers.
function blockClicksOnOverlay() {
const pathEl = document.getElementById('overlayPath');
// Prevent pointer events from reaching the card/parent
...