{"id":10399,"date":"2026-07-15T18:38:50","date_gmt":"2026-07-15T23:38:50","guid":{"rendered":"https:\/\/master.dev\/blog\/?p=10399"},"modified":"2026-07-15T18:38:51","modified_gmt":"2026-07-15T23:38:51","slug":"lessons-learned-rewriting-a-sticky-detector","status":"publish","type":"post","link":"https:\/\/master.dev\/blog\/lessons-learned-rewriting-a-sticky-detector\/","title":{"rendered":"Lessons Learned Rewriting a Sticky Detector"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">I recently came across <a href=\"https:\/\/codepen.io\/editor\/chriscoyier\/pen\/0148624b-bda0-7450-9d80-69280682a1e5\">a demo I did in 2014<\/a> where, as you scroll down, a search input becomes stuck to the top of the page, and changes styles a bit to make room for a hamburger menu. I&#8217;ll just video that so it&#8217;s clear:<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player aligncenter wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 325px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe loading=\"lazy\" title=\"VideoPress Video Player\" aria-label=\"VideoPress Video Player\" width=\"495\" height=\"750\" src=\"https:\/\/videopress.com\/embed\/1EJJGkx2?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; presentation\"><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p class=\"wp-block-paragraph\">Perhaps you forget these long ago times, but we were <em>not<\/em> rocking <code>position: sticky;<\/code> back then. So this demo had to replicate that somehow. But I was noticing how the styles <em>change<\/em> as well, meaning even after we got <code>position: sticky;<\/code>, we&#8217;d need a way to detect when it&#8217;s stuck (through JavaScript or whatever) and update a class (through JavaScript or whatever).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In these cushy, simpler times, we can get this all done in CSS. But there are a couple of other lessons to be learned by converting this thing to 2026 magic, so let&#8217;s go. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Main Problem<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In the old demo, the main problem is&#8230; JavaScript. It&#8217;s not even that JavaScript is used at all, it&#8217;s just that how it&#8217;s implemented is wildly inefficient.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">var<\/span> wrap = $(<span class=\"hljs-string\">\"#wrap\"<\/span>);\n\nwrap.on(<span class=\"hljs-string\">\"scroll\"<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span>(<span class=\"hljs-params\">e<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">this<\/span>.scrollTop &gt; <span class=\"hljs-number\">147<\/span>) {\n    wrap.addClass(<span class=\"hljs-string\">\"fix-search\"<\/span>);\n  } <span class=\"hljs-keyword\">else<\/span> {\n    wrap.removeClass(<span class=\"hljs-string\">\"fix-search\"<\/span>);\n  }\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><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 class=\"wp-block-paragraph\">This listens to the <code>scroll<\/code> event and fires a callback on each. One little scroll flick downwards might fire this event (and thus do the callback and the DOM element access and math) <em>many hundreds<\/em> of times. Boooooo. <\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"860\" height=\"1024\" src=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.27.49-PM.png?resize=860%2C1024&#038;ssl=1\" alt=\"\" class=\"wp-image-10424\" style=\"aspect-ratio:0.8398556498101889;width:482px;height:auto\" srcset=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.27.49-PM.png?resize=860%2C1024&amp;ssl=1 860w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.27.49-PM.png?resize=252%2C300&amp;ssl=1 252w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.27.49-PM.png?resize=768%2C915&amp;ssl=1 768w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.27.49-PM.png?w=1026&amp;ssl=1 1026w\" sizes=\"auto, (max-width: 860px) 100vw, 860px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Plus, it&#8217;s jQuery 2.1.3 just because hey, it was 2014.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Gettin&#8217; Sticky<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The first thing we&#8217;re gonna do is use <code>position: sticky;<\/code> as that&#8217;s going to do the main thing we need (stick the search input to the header as it scrolls by), and <em>we can toss the old JavaScript entirely<\/em>. We&#8217;re gonna need a wrapper element that will be the element gettin&#8217; stuck. So we can do this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" 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\">\"search-area\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">form<\/span> <span class=\"hljs-attr\">action<\/span>=<span class=\"hljs-string\">\"\/search\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">for<\/span>=<span class=\"hljs-string\">\"s\"<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"visually-hidden\"<\/span>&gt;<\/span>Search<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"s\"<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"search\"<\/span> <span class=\"hljs-attr\">placeholder<\/span>=<span class=\"hljs-string\">\"Search...\"<\/span> \/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">form<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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\n<p class=\"wp-block-paragraph\">So even if this area isn&#8217;t at the top of the page to start, and ours isn&#8217;t, it&#8217;ll get stuck to the top when we scroll by if we do this:<\/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\">.search-area<\/span> {\n  <span class=\"hljs-attribute\">position<\/span>: sticky;\n  <span class=\"hljs-attribute\">top<\/span>: <span class=\"hljs-number\">0<\/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\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player aligncenter wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 405px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe loading=\"lazy\" title=\"VideoPress Video Player\" aria-label=\"VideoPress Video Player\" width=\"500\" height=\"412\" src=\"https:\/\/videopress.com\/embed\/Z8qLOm7C?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; presentation\"><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<h3 class=\"wp-block-heading\">Using <code>&lt;search&gt;<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Perhaps the <code>&lt;form&gt;<\/code> is a bit presumtive there. I might argue it&#8217;s a good idea since it&#8217;s the kind of thing that is the foundation of progressive enhancement and working without JavaScript. But this is literally a fake example that doesn&#8217;t search anything. We could replace that form with a <code>&lt;search&gt;<\/code> element or use <code>&lt;search&gt;<\/code> as the parent element. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" 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\">\"search-area\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">search<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">for<\/span>=<span class=\"hljs-string\">\"s\"<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"visually-hidden\"<\/span>&gt;<\/span>Search<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"s\"<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"search\"<\/span> <span class=\"hljs-attr\">placeholder<\/span>=<span class=\"hljs-string\">\"Search...\"<\/span> \/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">search<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><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\n<p class=\"wp-block-paragraph\">We basically <a href=\"https:\/\/www.scottohara.me\/blog\/2023\/03\/24\/search-element.html\">get a free ARIA landmark<\/a> for using it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Stuck State Query<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I also chucked that <code>&lt;search&gt;<\/code> in there for a sneaky second reason. We need an additional child element to style, since we&#8217;re utlimately going to use a container query, and you can&#8217;t write container styles on the container itself. Classic gotcha. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s officially make the wrapper element a container:<\/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\">.search-area<\/span> {\n  <span class=\"hljs-comment\">\/* `scroll-state` is a real keyword we need to make this specific kind of container *\/<\/span>\n  <span class=\"hljs-attribute\">container-type<\/span>: scroll-state;\n  <span class=\"hljs-comment\">\/* `sticky-search` is a name we just made up to name the container *\/<\/span>\n  <span class=\"hljs-attribute\">container-name<\/span>: sticky-search;\n  <span class=\"hljs-attribute\">position<\/span>: sticky;\n  <span class=\"hljs-attribute\">top<\/span>: <span class=\"hljs-number\">0<\/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 class=\"wp-block-paragraph\">Now we can write a <code>@container<\/code> query to detect the stuck state and use it. We could it like this, without needing the name 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\">.search-area<\/span> {\n  <span class=\"hljs-attribute\">container-type<\/span>: scroll-state;\n  <span class=\"hljs-attribute\">container-name<\/span>: sticky-search;\n  <span class=\"hljs-attribute\">position<\/span>: sticky;\n  <span class=\"hljs-attribute\">top<\/span>: <span class=\"hljs-number\">0<\/span>;\n\n  @container scroll-state(<span class=\"hljs-attribute\">stuck<\/span>: top) {\n    search {\n      <span class=\"hljs-comment\">\/* stuck styles for a child element *\/<\/span>\n    }\n  }\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 class=\"wp-block-paragraph\">Or do it elsewhere in the CSS and call it by name:<\/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\">...\n\n<span class=\"hljs-selector-tag\">search<\/span> {\n  ...\n\n  @container sticky-search scroll-state(<span class=\"hljs-attribute\">stuck<\/span>: top) {\n    <span class=\"hljs-comment\">\/* stuck styles for a child element *\/<\/span>\n    \n    input {\n      <span class=\"hljs-comment\">\/* keep styling other stuff *\/<\/span>\n    }\n  }\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 class=\"wp-block-paragraph\">In our demo, we add a bit of padding, change the background, and fiddle with the width of the input when it&#8217;s stuck (as that&#8217;s what the original demo did as well). <\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player aligncenter wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 442px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe loading=\"lazy\" title=\"VideoPress Video Player\" aria-label=\"VideoPress Video Player\" width=\"500\" height=\"426\" src=\"https:\/\/videopress.com\/embed\/9uB3IELH?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; presentation\"><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<h2 class=\"wp-block-heading\">Anchor the Burger<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is another somewhat contrived situation because of the nature of the demo, but it&#8217;s useful for learning anyway. See how we&#8217;ve kind of make a &#8220;fake&#8221; mobile layout by limiting the wrapper area to <code>280px<\/code>? It&#8217;s in that area specifically where we want our <code>position: fixed;<\/code> hamburger menu. But there is another classic gotcha here where you can&#8217;t <code>position: fixed;<\/code> and element relative to some random element on the page. You can&#8217;t just <code>position: relative;<\/code> a parent like you do with <code>position: absolute;<\/code>. There is trickery, like applying an unnecessary <code>transform<\/code> to scope things, but I find the tradeoffs unacceptable. Plus, there is a better way, anyway.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We can express &#8220;put this thing relative to this other element and stay there&#8221; through anchor positioning these days, without any of the weird constraints.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since we have an obvious parent element at play here (it might just be the <code>&lt;body&gt;<\/code> on a typical situation), we&#8217;ll make it the anchoring context like this:<\/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\">.wrap<\/span> {\n  ...\n\n  <span class=\"hljs-attribute\">anchor-name<\/span>: --wrap;\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 class=\"wp-block-paragraph\">Now we can still use <code>position: fixed;<\/code>, but place it perfectly relative to that anchor we just set up.<\/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\">.menu-icon<\/span> {\n  ...\n\n  <span class=\"hljs-attribute\">position<\/span>: fixed;\n  <span class=\"hljs-attribute\">position-anchor<\/span>: --wrap;\n  <span class=\"hljs-attribute\">top<\/span>: <span class=\"hljs-built_in\">calc<\/span>(anchor(top) + <span class=\"hljs-number\">12px<\/span>);\n  <span class=\"hljs-attribute\">right<\/span>: <span class=\"hljs-built_in\">calc<\/span>(anchor(right) + <span class=\"hljs-number\">25px<\/span>);\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<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"670\" src=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/CleanShot-2026-07-15-at-16.35.28%402x.png?resize=1024%2C670&#038;ssl=1\" alt=\"\" class=\"wp-image-10428\" srcset=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/CleanShot-2026-07-15-at-16.35.28%402x.png?resize=1024%2C670&amp;ssl=1 1024w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/CleanShot-2026-07-15-at-16.35.28%402x.png?resize=300%2C196&amp;ssl=1 300w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/CleanShot-2026-07-15-at-16.35.28%402x.png?resize=768%2C503&amp;ssl=1 768w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/CleanShot-2026-07-15-at-16.35.28%402x.png?w=1268&amp;ssl=1 1268w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">I love this so much. <\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Use SVG<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The old demo used \u2630 for the burger icon, which I&#8217;m just gonna say <a href=\"https:\/\/www.htmhell.dev\/11-the-trigram-for-heaven\/\">is a bad practice<\/a>. Using <code>&lt;svg&gt;<\/code> is just better, and allows for some normal markup. Probably something like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" 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\">button<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"menu-icon\"<\/span> <span class=\"hljs-attr\">aria-label<\/span>=<span class=\"hljs-string\">\"Toggle Menu\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">viewBox<\/span>=<span class=\"hljs-string\">\"0 0 100 100\"<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">\"12\"<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">\"12\"<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">\"true\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">path<\/span> <span class=\"hljs-attr\">d<\/span>=<span class=\"hljs-string\">\"...\"<\/span> \/&gt;<\/span>\n    <span class=\"hljs-comment\">&lt;!-- or maybe some &lt;line&gt; stuff --&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><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\n<p class=\"wp-block-paragraph\">I just grabbed one out of CodePen&#8217;s free assets:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"906\" src=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.37.41-PM.png?resize=1024%2C906&#038;ssl=1\" alt=\"\" class=\"wp-image-10429\" srcset=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.37.41-PM.png?resize=1024%2C906&amp;ssl=1 1024w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.37.41-PM.png?resize=300%2C265&amp;ssl=1 300w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.37.41-PM.png?resize=768%2C679&amp;ssl=1 768w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.37.41-PM.png?resize=1536%2C1358&amp;ssl=1 1536w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/Screenshot-2026-07-15-at-4.37.41-PM.png?resize=2048%2C1811&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Final Demo<\/h2>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019f5d94-74b8-7052-9db1-f653635a7ceb\/fabdf8101cbd5f7684ecb22c3856396b\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019f5d94-74b8-7052-9db1-f653635a7ceb\/fabdf8101cbd5f7684ecb22c3856396b?height=650&amp;theme-id=1&amp;slug-hash=019f5d94-74b8-7052-9db1-f653635a7ceb\/fabdf8101cbd5f7684ecb22c3856396b&amp;default-tab=result\" height=\"650\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019f5d94-74b8-7052-9db1-f653635a7ceb\/fabdf8101cbd5f7684ecb22c3856396b\" title=\"CodePen Embed 019f5d94-74b8-7052-9db1-f653635a7ceb\/fabdf8101cbd5f7684ecb22c3856396b\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A dozen years go by, it turns out we can do things a lot differently and a lot better. Here a `scroll` event is entirely replaced by HTML and CSS features and much better performance.<\/p>\n","protected":false},"author":1,"featured_media":10422,"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_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[7,397,514,166,419],"class_list":["post-10399","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-css","tag-menu","tag-scroll-state-2","tag-search","tag-sticky"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/fixed-header.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10399","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/comments?post=10399"}],"version-history":[{"count":13,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10399\/revisions"}],"predecessor-version":[{"id":10430,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10399\/revisions\/10430"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media\/10422"}],"wp:attachment":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media?parent=10399"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/categories?post=10399"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/tags?post=10399"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}