<div id="wrapper">
<p class="lorem"><span class="hl">Lorem ipsum</span> dolor sit amet consectetur, adipisicing elit. Inventore sunt deleniti <span class="hl">quaerat earum ducimus minus. Libero voluptas sint</span>, ab fuga animi adipisci, tenetur nihil molestiae quisquam quae vel <span class="hl">aspernatur officiis!</span></p>
</div>
<div id="shade"></div>
#wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
font-size: clamp(14px, 2vmax, 24px);
}
.lorem {
width: clamp(200px, 20vw, 400px);
line-height: 1.5em;
text-align: justify;
}
#shade {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background-color: #3336;
backdrop-filter: blur(1px);
}
// The #shade element is the obscuring overlay
const $shade = document.getElementById('shade')
// Holes will be cut out from the overlay for each element with the "hl" class
const $hls = document.querySelectorAll('.hl')
// Extra "padding" to apply to boundaries of the holes on each side
const SPREAD = 2
// Reusable function for drawing the overlay
function drawKnockout () {
const w = window.innerWidth
const h = window.innerHeight
// Start by drawing the shade curtain over the entire viewport, moving
// clockwise between each corner
const coords = [
[0, 0], // NW
[w, 0], // NE
[w, h], // SE
[0, h], // SW
[0, 0] // NW
]
// Iterate over each "highlight" target for the holes
for (const $hl of $hls) {
// Collect all the bounding boxes for this element - inline elements (like
// word wrapped spans) can have multiple boxes
const boxes = $hl.getClientRects()
// Cut out a hole for each box, moving counter-clockwise this time
for (const box of boxes) {
const x1 = box.left - SPREAD
const x2 = box.right + SPREAD
const y1 = box.top - SPREAD
const y2 = box.bottom + SPREAD
coords.push(
[x1, y1], // NW
[x1, y2], // SW
[x2, y2], // SE
[x2, y1], // NE
[x1, y1], // NW
[0, 0]
)
}
}
// Apply the knockout as `clip-path` style rule on the #shade element
$shade.style.clipPath = `polygon(${coordsToPolygon(coords)})`
}
// Convert 2D array of coordinates to polygon-friendly string:
// [[0, 0], [20, 40]] -> "0px 0px, 20px 40px"
function coordsToPolygon (coords) {
return coords
.map((p) => p.map((c) => `${c}px`).join(' '))
.join(', ')
}
// Draw the knockout on load, and again if/when the viewport is resized
drawKnockout()
window.addEventListener('resize', drawKnockout)
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.