<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)

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.