{"id":5939,"date":"2025-05-26T10:02:35","date_gmt":"2025-05-26T15:02:35","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=5939"},"modified":"2025-05-26T10:02:36","modified_gmt":"2025-05-26T15:02:36","slug":"css-spotlight-effect","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/css-spotlight-effect\/","title":{"rendered":"CSS Spotlight Effect"},"content":{"rendered":"\n<p>I recently made <a href=\"https:\/\/codepen.io\/amit_sheen\/embed\/gbbzbeR\">an experiment<\/a> about&nbsp;<em>Proximity Reactions<\/em>. The idea was to create an interactive effect according to the mouse position relative to elements. Then I made a <em>less JavaScript, more CSS<\/em> version where the only thing JavaScript does is to pass the mouse position into a couple of CSS custom properties. That\u2019s it. All the heavy lifting happened inside the CSS itself, safely away from the JavaScript thread.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_gbbzbeR\" src=\"\/\/codepen.io\/anon\/embed\/gbbzbeR?height=450&amp;theme-id=47434&amp;slug-hash=gbbzbeR&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed gbbzbeR\" title=\"CodePen Embed gbbzbeR\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>That got me thinking: if we can get the mouse position in CSS so easily, what else can we build with that? I started tinkering, trying out different interaction patterns, and eventually got to this&nbsp;<strong>Spotlight Effect<\/strong> that&#8217;s easy to create, simple to customize, and looks surprisingly slick, all with just a few lines of CSS.<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='314' src='https:\/\/videopress.com\/embed\/bLO8upL7?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>Let\u2019s take a look at how it works and how you can make it your own, and hopefully you can pick up a few new CSS tricks along the way. \ud83d\ude42<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-setup\">The Setup<\/h2>\n\n\n\n<p>To create a spotlight effect that responds to the mouse position, we need to set up two small things before diving into the CSS. <\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>We need a dedicated&nbsp;<em>spotlight<\/em>&nbsp;element in the DOM. This is usually placed near the end of the markup so it can sit on top of everything else when needed. <\/li>\n\n\n\n<li>We need just a few lines of JavaScript to pass the mouse coordinates into CSS custom properties.<\/li>\n<\/ol>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"spotlight\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-built_in\">document<\/span>.body.addEventListener(<span class=\"hljs-string\">'mousemove'<\/span>, (e) =&gt; {\n  <span class=\"hljs-built_in\">document<\/span>.body.style.setProperty(<span class=\"hljs-string\">'--clientX'<\/span>, e.clientX + <span class=\"hljs-string\">'px'<\/span>);\n  <span class=\"hljs-built_in\">document<\/span>.body.style.setProperty(<span class=\"hljs-string\">'--clientY'<\/span>, e.clientY + <span class=\"hljs-string\">'px'<\/span>);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That is all. No fancy libraries, no event throttling, just raw coordinates handed over to CSS, where the real magic happens.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"basic-follow\">Basic follow<\/h2>\n\n\n\n<p>Now that the setup is in place, we can start writing some CSS. We will begin with a very basic version of the spotlight effect: a simple transparent circle that follows the mouse movements. There are many ways to implement this kind of effect. Using&nbsp;<code>transform<\/code>&nbsp;is a common and often more precise approach in some cases. But for our example, we are going to tap into the power of&nbsp;<code>background-image<\/code>. This gives us a lot of creative flexibility, especially when we\u2019ll start creating patterns with gradients later on.<\/p>\n\n\n\n<p>Here is the CSS for our initial spotlight:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.spotlight<\/span> {\n  <span class=\"hljs-attribute\">position<\/span>: fixed;\n  <span class=\"hljs-attribute\">inset<\/span>: <span class=\"hljs-number\">0<\/span>;\n  <span class=\"hljs-attribute\">background-image<\/span>: <span class=\"hljs-built_in\">radial-gradient<\/span>(circle at var(--clientX, <span class=\"hljs-number\">50%<\/span>) <span class=\"hljs-built_in\">var<\/span>(--clientY, <span class=\"hljs-number\">50%<\/span>), transparent <span class=\"hljs-number\">6em<\/span>, black <span class=\"hljs-number\">8em<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Notice that we set&nbsp;<code>position: fixed<\/code>&nbsp;and&nbsp;<code>inset: 0<\/code>, this ensures that it fills the entire viewport, anchoring it to the edges of the body, and stays in place when the user scroll down the page. With that in place, we can position the transparent circle (made with a simple&nbsp;<code>radial-gradient<\/code>) using the CSS custom properties that our JavaScript sets. It really is that simple.<\/p>\n\n\n\n<p>I&#8217;m using\u00a0<code>em<\/code>\u00a0units for sizing. This makes everything scale relative to the font size, and it makes it very easy to adjust the size of the entire effect just by changing the font size on this element.<\/p>\n\n\n\n<p>Here is the result:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_azzaVzv\/338dde68a37a2f1b3bd3d95d80bfcbcc\" src=\"\/\/codepen.io\/anon\/embed\/azzaVzv\/338dde68a37a2f1b3bd3d95d80bfcbcc?height=450&amp;theme-id=47434&amp;slug-hash=azzaVzv\/338dde68a37a2f1b3bd3d95d80bfcbcc&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed azzaVzv\/338dde68a37a2f1b3bd3d95d80bfcbcc\" title=\"CodePen Embed azzaVzv\/338dde68a37a2f1b3bd3d95d80bfcbcc\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>To make the effect feel a bit lighter, I also added a touch of&nbsp;<code>opacity<\/code>. I think it creates a more layered and subtle look. More importantly, I set&nbsp;<code>pointer-events: none<\/code>&nbsp;on the&nbsp;<code>.spotlight<\/code>&nbsp;element. Since this layer sits above everything else in the DOM, we want to make sure it does not block any user interaction with the elements below it. Without this, buttons, links, and other interactive parts of the page would become unresponsive.<\/p>\n\n\n\n<p>I&#8217;m\u00a0<strong>not<\/strong>\u00a0using <code>cursor: none;<\/code> here. While it might seem like an good choice for effects like this, hiding the mouse cursor can lead to accessibility issues and negatively impact the user experience. It&#8217;s generally best to avoid it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"making-it-interesting\">Making It Interesting<\/h2>\n\n\n\n<p>This is where things start to get fun. Instead of a simple circle, we can turn our spotlight into a dynamic, interactive effect that responds to the mouse movement in playful ways. The technique we will use involves layering gradients in the <code>background-image<\/code> and combining them in a gooey visual style. The result is a smooth, organic animation that feels alive under the cursor.<\/p>\n\n\n\n<p>To achieve the gooey effect, we rely on the&nbsp;<code>filter<\/code>&nbsp;property, specifically a combination of&nbsp;<code>blur<\/code>&nbsp;and&nbsp;<code>contrast<\/code>. The blur softens the edges of the shapes, and the high contrast causes overlapping areas to merge into blobs. However, applying contrast on a transparent background does nothing. To fix that, we give the element a solid white&nbsp;<code>background-color<\/code>. Then, to make the white areas effectively transparent against the page, we use&nbsp;<code>mix-blend-mode: darken<\/code>.<\/p>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p>1) Start with a basic spotlight<\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"321\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/ZNXB782.png?resize=320%2C321&#038;ssl=1\" alt=\"\" class=\"wp-image-5945\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/ZNXB782.png?w=320&amp;ssl=1 320w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/ZNXB782.png?resize=300%2C300&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/ZNXB782.png?resize=150%2C150&amp;ssl=1 150w\" sizes=\"auto, (max-width: 320px) 100vw, 320px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p>Add a pattern using the&nbsp;<code>background-image<\/code><\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"321\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/Zqil84_4.png?resize=320%2C321&#038;ssl=1\" alt=\"\" class=\"wp-image-5946\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/Zqil84_4.png?w=320&amp;ssl=1 320w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/Zqil84_4.png?resize=300%2C300&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/Zqil84_4.png?resize=150%2C150&amp;ssl=1 150w\" sizes=\"auto, (max-width: 320px) 100vw, 320px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p>Set the&nbsp;<code>background-color<\/code>&nbsp;to white<\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"321\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/3.png?resize=320%2C321&#038;ssl=1\" alt=\"\" class=\"wp-image-5947\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/3.png?w=320&amp;ssl=1 320w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/3.png?resize=300%2C300&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/3.png?resize=150%2C150&amp;ssl=1 150w\" sizes=\"auto, (max-width: 320px) 100vw, 320px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p>Apply the&nbsp;<code>filter<\/code>&nbsp;for the gooey effect<\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"321\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/4.png?resize=320%2C321&#038;ssl=1\" alt=\"\" class=\"wp-image-5948\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/4.png?w=320&amp;ssl=1 320w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/4.png?resize=300%2C300&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/4.png?resize=150%2C150&amp;ssl=1 150w\" sizes=\"auto, (max-width: 320px) 100vw, 320px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p>Remove the white parts using&nbsp;<code>mix-blend-mode<\/code><\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"321\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/5.png?resize=320%2C321&#038;ssl=1\" alt=\"\" class=\"wp-image-5949\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/5.png?w=320&amp;ssl=1 320w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/5.png?resize=300%2C300&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/5.png?resize=150%2C150&amp;ssl=1 150w\" sizes=\"auto, (max-width: 320px) 100vw, 320px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n\n<p>And here is the code that sets up this visual base:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.spotlight<\/span> {\n  <span class=\"hljs-attribute\">filter<\/span>: <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">1em<\/span>) <span class=\"hljs-built_in\">contrast<\/span>(<span class=\"hljs-number\">100<\/span>);\n  <span class=\"hljs-attribute\">mix-blend-mode<\/span>: darken;\n  <span class=\"hljs-attribute\">background-color<\/span>: white;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now that we have this setup, we can start layering more shapes, play with gradients, and watch the gooey interactions evolve as the mouse moves.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-blob-light\">The Blob Light<\/h2>\n\n\n\n<p>With the gooey base in place, we can use gradients to build more playful visual behaviors. Since <code>background-image<\/code> can accept a comma-separated list of layers, we can stack several gradients with varying styles, sizes, and positions. These layers blend together through the blur and contrast filters, resulting in a smooth, organic effect.<\/p>\n\n\n\n<p>To create a blob-style spotlight, I made the main circle a bit larger and softer, and stacked two repeating linear gradients to form a diagonal grid pattern.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.spotlight<\/span> {\n  <span class=\"hljs-attribute\">background-image<\/span>:\n    <span class=\"hljs-built_in\">radial-gradient<\/span>(circle at var(--clientX, <span class=\"hljs-number\">50%<\/span>) <span class=\"hljs-built_in\">var<\/span>(--clientY, <span class=\"hljs-number\">50%<\/span>), transparent, black <span class=\"hljs-number\">20em<\/span>),\n    <span class=\"hljs-built_in\">repeating-linear-gradient<\/span>(<span class=\"hljs-number\">45deg<\/span>, black <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0.4em<\/span>, transparent <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">3em<\/span>),\n    <span class=\"hljs-built_in\">repeating-linear-gradient<\/span>(-<span class=\"hljs-number\">45deg<\/span>, black <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0.4em<\/span>, transparent <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">3em<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This is how the&nbsp;<code>background-image<\/code>&nbsp;looks like&nbsp;<strong>without<\/strong>&nbsp;the gooey setup:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"321\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/AuEuovV8.png?resize=320%2C321&#038;ssl=1\" alt=\"\" class=\"wp-image-5950\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/AuEuovV8.png?w=320&amp;ssl=1 320w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/AuEuovV8.png?resize=300%2C300&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/AuEuovV8.png?resize=150%2C150&amp;ssl=1 150w\" sizes=\"auto, (max-width: 320px) 100vw, 320px\" \/><\/figure>\n<\/div>\n\n\n<p>And the full blob effect:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_GggXOMG\/e9cd518a64ae8d9a83b715a2a6641cb9\" src=\"\/\/codepen.io\/anon\/embed\/GggXOMG\/e9cd518a64ae8d9a83b715a2a6641cb9?height=450&amp;theme-id=47434&amp;slug-hash=GggXOMG\/e9cd518a64ae8d9a83b715a2a6641cb9&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed GggXOMG\/e9cd518a64ae8d9a83b715a2a6641cb9\" title=\"CodePen Embed GggXOMG\/e9cd518a64ae8d9a83b715a2a6641cb9\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fixing-the-fuzzy-edges\">Fixing the Fuzzy Edges<\/h2>\n\n\n\n<p>You may have noticed in the previous example that the edges of the&nbsp;<code>.spotlight<\/code>&nbsp;element appear fuzzy, subtly revealing the content behind it. This is a side effect of the&nbsp;<code>blur<\/code>&nbsp;filter. When there&#8217;s nothing beyond the blurred edge for the&nbsp;<code>contrast<\/code>&nbsp;filter to respond to, the gradient just fades out softly. Visually, that results in blurry borders that break the clean feel of the effect.<\/p>\n\n\n\n<p>There are a few ways to deal with this. Like scaling up the element, applying a negative&nbsp;<code>inset<\/code>, or manually setting a larger width and height. But all of these approaches introduce extra complexity, especially since you\u2019d also have to compensate for the mouse coordinates shifting relative to the larger area.<\/p>\n\n\n\n<p>A simpler and more robust fix is to add an&nbsp;<code>outline<\/code>. Just make sure it&#8217;s larger than the blur radius and matches the background color. That way, the fuzzy edges get hidden cleanly without affecting the positioning logic at all.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.spotlight<\/span> {\n  <span class=\"hljs-attribute\">outline<\/span>: <span class=\"hljs-number\">2em<\/span> solid white;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We&#8217;ll include this&nbsp;<code>outline<\/code>&nbsp;fix in all the following examples to keep things clean and crisp.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"dotted-reveal\">Dotted Reveal<\/h2>\n\n\n\n<p>The reason the blob in the previous example appears to morph as the mouse moves is that, while the main circle follows the cursor, the grid pattern remains fixed on the screen. The interaction between these two layers creates the illusion of motion and shifting shapes within the spotlight.<\/p>\n\n\n\n<p>Following the same principle, we can build a dotted effect. This time, instead of diagonal lines, we&#8217;ll use two radial gradients, and set a&nbsp;<code>background-size<\/code>&nbsp;to create a repeating pattern:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.spotlight<\/span> {\n  <span class=\"hljs-attribute\">background-image<\/span>:\n    <span class=\"hljs-built_in\">radial-gradient<\/span>(circle at var(--clientX, <span class=\"hljs-number\">50%<\/span>) <span class=\"hljs-built_in\">var<\/span>(--clientY, <span class=\"hljs-number\">50%<\/span>), transparent <span class=\"hljs-number\">6em<\/span>, black <span class=\"hljs-number\">10em<\/span>),\n    <span class=\"hljs-built_in\">radial-gradient<\/span>(circle, black <span class=\"hljs-number\">0.2em<\/span>, transparent <span class=\"hljs-number\">1em<\/span>),\n    <span class=\"hljs-built_in\">radial-gradient<\/span>(circle, black <span class=\"hljs-number\">0.2em<\/span>, transparent <span class=\"hljs-number\">1em<\/span>);\n  <span class=\"hljs-attribute\">background-size<\/span>: <span class=\"hljs-number\">100%<\/span> <span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">2em<\/span> <span class=\"hljs-number\">3em<\/span>, <span class=\"hljs-number\">2em<\/span> <span class=\"hljs-number\">3em<\/span>;\n  <span class=\"hljs-attribute\">background-position<\/span>: <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">1em<\/span> <span class=\"hljs-number\">1.5em<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The first layer defines the moving mask (just like before), and the next two layers form the repeating dot pattern. By adjusting&nbsp;<code>background-position<\/code>, we offset the second dot layer to create the alternating effect. The result is a playful dotted texture that dynamically follows the mouse.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_azzaXgz\/32590da0e381f17b3d3a2080c183dd8f\" src=\"\/\/codepen.io\/anon\/embed\/azzaXgz\/32590da0e381f17b3d3a2080c183dd8f?height=450&amp;theme-id=47434&amp;slug-hash=azzaXgz\/32590da0e381f17b3d3a2080c183dd8f&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed azzaXgz\/32590da0e381f17b3d3a2080c183dd8f\" title=\"CodePen Embed azzaXgz\/32590da0e381f17b3d3a2080c183dd8f\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>All of the values in the last two examples (color stops, gradient sizes and positions, blur and contrast settings, and more) can be tweaked to create&nbsp;<strong>wildly<\/strong>&nbsp;different effects. I spent a lot of time experimenting before landing on these particular numbers, and I encourage you to do the same. Go ahead and fork one of the demos, adjust the gradients, play with the filter values, and see where your creativity takes you. And if you discover something cool, don\u2019t forget to send it my way.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"movement-interaction\">Movement Interaction<\/h2>\n\n\n\n<p>In the previous examples, only the main circle responded to the cursor movement, but those same CSS variables can drive other visual elements as well. Here is an example that lays out a grid of squares using a&nbsp;<code>conic-gradient<\/code>. By offsetting its position by a fraction of the cursor coordinates (a factor of negative 0.25 in this case) we achieve a subtle parallax effect.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.spotlight<\/span> {\n  <span class=\"hljs-attribute\">background-image<\/span>:\n    <span class=\"hljs-built_in\">radial-gradient<\/span>(circle at var(--clientX, <span class=\"hljs-number\">50%<\/span>) <span class=\"hljs-built_in\">var<\/span>(--clientY, <span class=\"hljs-number\">50%<\/span>), transparent, black <span class=\"hljs-number\">14em<\/span>),\n    <span class=\"hljs-built_in\">conic-gradient<\/span>(from <span class=\"hljs-number\">270deg<\/span> at <span class=\"hljs-number\">1em<\/span> <span class=\"hljs-number\">1em<\/span>, #aaa <span class=\"hljs-number\">90deg<\/span>, transparent <span class=\"hljs-number\">0<\/span>);\n  <span class=\"hljs-attribute\">background-size<\/span>: <span class=\"hljs-number\">100%<\/span> <span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">3em<\/span> <span class=\"hljs-number\">3em<\/span>;\n  <span class=\"hljs-attribute\">background-position<\/span>: \n    <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span>, \n    <span class=\"hljs-built_in\">calc<\/span>(var(--clientX, <span class=\"hljs-number\">50%<\/span>) * -<span class=\"hljs-number\">0.25<\/span>) <span class=\"hljs-built_in\">calc<\/span>(var(--clientY, <span class=\"hljs-number\">50%<\/span>) * -<span class=\"hljs-number\">0.25<\/span>); <span class=\"hljs-comment\">\/* only the conic layer moves *\/<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>You can comment out the&nbsp;<code>background-position<\/code>&nbsp;to see its affect, and feel free to tweak the offset factor and see how the motion transforms.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_LEEgVyw\/470ffa62dab979cdee8e1ffda8c32397\" src=\"\/\/codepen.io\/anon\/embed\/LEEgVyw\/470ffa62dab979cdee8e1ffda8c32397?height=450&amp;theme-id=47434&amp;slug-hash=LEEgVyw\/470ffa62dab979cdee8e1ffda8c32397&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed LEEgVyw\/470ffa62dab979cdee8e1ffda8c32397\" title=\"CodePen Embed LEEgVyw\/470ffa62dab979cdee8e1ffda8c32397\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"learn-more\">Tip: try adding a transition on the background\u2019s position to add even more motion. <code>.spotlight { transition: background-position 0.5s ease-out; }<\/code><\/p>\n\n\n\n<p>Remember these values can be used for anything. They\u2019re just variables, and that means you can plug them into any CSS property that accepts dynamic values. For example, here\u2019s an example where the mouse\u2019s X position controls the amount of blur, and the Y position determines the size of the central circle.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_myyzJoW\/d74d410a17ad36ed510c2d817a68cd18\" src=\"\/\/codepen.io\/anon\/embed\/myyzJoW\/d74d410a17ad36ed510c2d817a68cd18?height=450&amp;theme-id=47434&amp;slug-hash=myyzJoW\/d74d410a17ad36ed510c2d817a68cd18&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed myyzJoW\/d74d410a17ad36ed510c2d817a68cd18\" title=\"CodePen Embed myyzJoW\/d74d410a17ad36ed510c2d817a68cd18\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>With just two custom properties, you&#8217;re suddenly controlling not only movement, but also style and intensity. You could just as easily hook the mouse into opacity, gradient angles, or any part of the effect you want to feel dynamic. What would you change in your effect?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-full-reveal\">The Full Reveal<\/h2>\n\n\n\n<p>Up until now, we&#8217;ve been revealing only what\u2019s inside the spotlight, with everything else hidden behind the dark blur. But what if we want to fully reveal the page in certain cases? For example, when hovering over a specific element, we might want to turn the effect off entirely and let the full content show.<\/p>\n\n\n\n<p>Surprisingly, you don\u2019t need any JavaScript to do this. With one clever CSS selector, we can \u2018listen\u2019 for a hover on elements with a specific class and adjust the effect accordingly.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.spotlight<\/span> {\n  <span class=\"hljs-attribute\">transition<\/span>: opacity <span class=\"hljs-number\">1s<\/span>, background-color <span class=\"hljs-number\">1s<\/span>;\n\n  <span class=\"hljs-attribute\">body<\/span>:<span class=\"hljs-built_in\">has<\/span>(.reveal:hover) &amp; {\n    opacity: <span class=\"hljs-number\">0<\/span>;\n    <span class=\"hljs-attribute\">background-color<\/span>: black;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now, any element with&nbsp;<code>class=\"reveal\"<\/code>&nbsp;will temporarily disable the spotlight effect when hovered.<\/p>\n\n\n\n<p>In terms of styling, there are a few ways to disable the effect. You could scale the gradient out, reduce the blur, or even hide the entire&nbsp;<code>.spotlight<\/code>&nbsp;element. In this case, I went with a combination of lowering the opacity and changing the background color. This gave me a subtle fade effect both in and out.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_gbbBaeK\/9ab9b755eb99b3fba5be9edaeb546b72\" src=\"\/\/codepen.io\/anon\/embed\/gbbBaeK\/9ab9b755eb99b3fba5be9edaeb546b72?height=450&amp;theme-id=47434&amp;slug-hash=gbbBaeK\/9ab9b755eb99b3fba5be9edaeb546b72&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed gbbBaeK\/9ab9b755eb99b3fba5be9edaeb546b72\" title=\"CodePen Embed gbbBaeK\/9ab9b755eb99b3fba5be9edaeb546b72\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-light-spotlight\">The Light Spotlight<\/h2>\n\n\n\n<p>Until now, the hidden part of the page has been covered in black, creating a dark spotlight effect. But what if your design calls for a light version, with white as the cover color?<\/p>\n\n\n\n<p>Turns out it&#8217;s pretty straightforward. All we need to do is invert the colors in our&nbsp;<code>.spotlight<\/code>&nbsp;element\u2019s styles. Anything that was black becomes white, anything that was white becomes black (transparent stays as-is). And just as important, make sure to change the&nbsp;<code>mix-blend-mode<\/code> from <code>darken<\/code> to <code>lighten<\/code> so that the blending works correctly with the inverted color scheme.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_raaQRWP\/1dc87e15c36346f3996ee8b45e760d45\" src=\"\/\/codepen.io\/anon\/embed\/raaQRWP\/1dc87e15c36346f3996ee8b45e760d45?height=450&amp;theme-id=47434&amp;slug-hash=raaQRWP\/1dc87e15c36346f3996ee8b45e760d45&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed raaQRWP\/1dc87e15c36346f3996ee8b45e760d45\" title=\"CodePen Embed raaQRWP\/1dc87e15c36346f3996ee8b45e760d45\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Of course, these values don\u2019t have to be hard coded. You can define the colors and blend mode using CSS custom properties, giving you full control over the theme. Better yet, we can respond to user preferences using the&nbsp;<code>light-dark()<\/code> function and the&nbsp;<code>prefers-color-scheme<\/code>&nbsp;query to decide whether to use a light or dark spotlight effect.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-pseudo\">:root<\/span> {\n  <span class=\"hljs-attribute\">color-scheme<\/span>: light dark;\n\n  <span class=\"hljs-attribute\">--spotlight-cover<\/span>: <span class=\"hljs-built_in\">light-dark<\/span>(white, black);\n  <span class=\"hljs-attribute\">--spotlight-reveal<\/span>: <span class=\"hljs-built_in\">light-dark<\/span>(black, white);\n\n  @media (<span class=\"hljs-attribute\">prefers-color-scheme<\/span>: dark) {\n    --spotlight-blend-mode: darken;\n  }\n  \n  <span class=\"hljs-keyword\">@media<\/span> (<span class=\"hljs-attribute\">prefers-color-scheme:<\/span> light) {\n    <span class=\"hljs-selector-tag\">--spotlight-blend-mode<\/span>: <span class=\"hljs-selector-tag\">lighten<\/span>;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This approach not only makes your spotlight more flexible, but also keeps it aligned with accessibility and user experience best practices.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_jEEQoRG\/7a82b024cf0a2ceffd51f48409f9e648\" src=\"\/\/codepen.io\/anon\/embed\/jEEQoRG\/7a82b024cf0a2ceffd51f48409f9e648?height=450&amp;theme-id=47434&amp;slug-hash=jEEQoRG\/7a82b024cf0a2ceffd51f48409f9e648&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed jEEQoRG\/7a82b024cf0a2ceffd51f48409f9e648\" title=\"CodePen Embed jEEQoRG\/7a82b024cf0a2ceffd51f48409f9e648\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"adding-colors\">Adding Colors<\/h2>\n\n\n\n<p>So what about colors beyond just black or white? Black and white are great for creating strong contrast, but what if you want something a bit more&#8230; purple?<\/p>\n\n\n\n<p>Well, at this point, we need to slightly rethink our approach. The gooey technique we&#8217;ve used so far works beautifully with monochrome because of the way&nbsp;<code>mix-blend-mode<\/code>&nbsp;interacts with light and dark. As soon as you start introducing color, things get trickier. The blend mode can dramatically shift the look and feel depending on how your chosen colors interact with the background and with each other.<\/p>\n\n\n\n<p>You&nbsp;<em>can<\/em>&nbsp;try changing the colors to something like purple or teal, but it will alter the nature of the effect, sometimes in surprising ways, so I encourage you to experiment. And how knows, you might land on exactly the vibe you&#8217;re looking for.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ZYYVZPW\/c735a1a047274552bc40b599b2fe5b39\" src=\"\/\/codepen.io\/anon\/embed\/ZYYVZPW\/c735a1a047274552bc40b599b2fe5b39?height=450&amp;theme-id=47434&amp;slug-hash=ZYYVZPW\/c735a1a047274552bc40b599b2fe5b39&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ZYYVZPW\/c735a1a047274552bc40b599b2fe5b39\" title=\"CodePen Embed ZYYVZPW\/c735a1a047274552bc40b599b2fe5b39\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"mobile-support\">Mobile Support<\/h2>\n\n\n\n<p>This entire effect relies on mouse movement, so what happens when there&#8217;s no mouse? Rather than hiding content on touch devices, we\u2019ll simply disable the effect altogether when we detect a mobile or touch-based screen. That way, users still see everything, just without the fancy spotlight interaction.<\/p>\n\n\n\n<p>We can ensures that a device support hover interactions using the&nbsp;<code>hover<\/code>&nbsp;media query, which is supported on all major browsers. By wrapping the spotlight styles in a&nbsp;<code>@media (hover: hover)<\/code>&nbsp;we can apply the effect only on hover supported devices.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-keyword\">@media<\/span> (<span class=\"hljs-attribute\">hover:<\/span> hover) {\n  <span class=\"hljs-selector-class\">.spotlight<\/span> {\n    <span class=\"hljs-comment\">\/* spotlight styles *\/<\/span>\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This media query works well for most cases, but some devices support both touch and mouse input. Think touchscreen laptops or tablets with external mice. In those cases, the effect might kick in when it shouldn&#8217;t.<\/p>\n\n\n\n<p>To handle this more gracefully, we can back up our CSS with a small JavaScript snippet. It listens for a touch event and disables the effect as soon as a user interacts via touch. That way, the spotlight effect is removed dynamically if the device leans toward touch input.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> mouseMoveHandler = <span class=\"hljs-function\">(<span class=\"hljs-params\">e<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-built_in\">document<\/span>.body.style.setProperty(<span class=\"hljs-string\">'--clientX'<\/span>, e.clientX + <span class=\"hljs-string\">'px'<\/span>);\n  <span class=\"hljs-built_in\">document<\/span>.body.style.setProperty(<span class=\"hljs-string\">'--clientY'<\/span>, e.clientY + <span class=\"hljs-string\">'px'<\/span>);\n};\n\n<span class=\"hljs-built_in\">document<\/span>.body.addEventListener(<span class=\"hljs-string\">'mousemove'<\/span>, mouseMoveHandler);\n\n<span class=\"hljs-built_in\">document<\/span>.body.addEventListener(<span class=\"hljs-string\">'touchstart'<\/span>, () =&gt; {\n  <span class=\"hljs-built_in\">document<\/span>.body.classList.add(<span class=\"hljs-string\">'reveal'<\/span>);\n  <span class=\"hljs-built_in\">document<\/span>.body.removeEventListener(<span class=\"hljs-string\">'mousemove'<\/span>, mouseMoveHandler);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And one last thing on this topic: we should also account for basic keyboard navigation. We do not want users tabbing into elements that are visually hidden by the effect, so we will also disable it in that case. This can be detected using&nbsp;<code>body:has(:focus-visible)<\/code>, which tells us when one of our elements is focused. You can combine this selector with your&nbsp;<code>.reveal<\/code>&nbsp;logic to ensure the effect is turned off when keyboard navigation kicks in.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-keyword\">@media<\/span> (<span class=\"hljs-attribute\">hover:<\/span> hover) {\n  <span class=\"hljs-selector-class\">.spotlight<\/span> {\n    <span class=\"hljs-comment\">\/* spotlight styles *\/<\/span>\n\n    <span class=\"hljs-attribute\">body<\/span>:<span class=\"hljs-built_in\">has<\/span>(.reveal:hover, :focus-visible) &amp; {\n      opacity: <span class=\"hljs-number\">0<\/span>;\n      <span class=\"hljs-attribute\">background-color<\/span>: black;\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>With this setup, the effect behaves just right: it kicks in only when it makes sense and stays out of the way when it doesn&#8217;t. Mobile users still get the full content, and hybrid devices adapt in real time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-ultimate-spot\">The Ultimate Spot<\/h2>\n\n\n\n<p>Before we wrap up, here is a quick demo that brings together most of what we explored. A spotlight with a blob gooey effect, crisp edges, theme switching, and full mobile and keyboard navigation support. All within scrollable content, with areas that disable the effect on hover.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_wBBNrYa\/b3489c3f48ffc53c13246e25839ee2d6\" src=\"\/\/codepen.io\/anon\/embed\/wBBNrYa\/b3489c3f48ffc53c13246e25839ee2d6?height=450&amp;theme-id=47434&amp;slug-hash=wBBNrYa\/b3489c3f48ffc53c13246e25839ee2d6&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed wBBNrYa\/b3489c3f48ffc53c13246e25839ee2d6\" title=\"CodePen Embed wBBNrYa\/b3489c3f48ffc53c13246e25839ee2d6\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"taking-it-further\">Taking It Further<\/h2>\n\n\n\n<p>All of the ideas in this article are just starting points. Now it\u2019s your turn to run with them. You can play with gradient backgrounds and tweak their sizes and positions. You can experiment with filter settings or try different blend mode options to see what new moods emerge. You might also pull extra data from JavaScript (like the cursor angle relative to an element or the speed of movement) and feed that into your styles for even richer effects.<\/p>\n\n\n\n<p>In this article, I\u2019ve used a single <code>&lt;div&gt;<\/code> for the&nbsp;<code>.spotlight<\/code>&nbsp;element, but feel free to layer in additional elements, icons, text, or graphic shapes within the reveal area. Apply the same technique to multiple elements with their own custom settings. The possibilities are endless, so let your imagination guide you and discover what unique interactions you can build.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>We can pass the mouse position from JavaScript to CSS and use it to make unusual and playful effects.<\/p>\n","protected":false},"author":27,"featured_media":5988,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[7,180,262,3],"class_list":["post-5939","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-css","tag-custom-properties","tag-filter","tag-javascript"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/CSS-Spotlight-Effect.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5939","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/27"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=5939"}],"version-history":[{"count":7,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5939\/revisions"}],"predecessor-version":[{"id":5989,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5939\/revisions\/5989"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/5988"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=5939"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=5939"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=5939"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}