[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-css-custom-highlight-api-no-dom-mutation":44,"search-suggestions":227,"related-css-custom-highlight-api-no-dom-mutation":265,"code:js:true:a4quhw":393,"code:js:true:tg4oxh":394,"code:css:true:pknx28":395,"code:js:true:b86y4o":396,"comments-01a080a9-801c-727a-ad92-866c15063710":397},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":210,"seo":211,"translationGroupId":214,"series":48,"podcastUrl":48,"verticalId":5,"thread":215,"assessments":217,"translations":223,"quiz":225},"01a080a9-801c-727a-ad92-866c15063710","css-custom-highlight-api-no-dom-mutation","The Search Highlight That Deletes Your Selection",null,"A search-highlight feature that rebuilds its DOM on every keystroke can wipe out a user's own text selection with no warning. The CSS Custom Highlight API fixes it by styling text ranges without ever touching a DOM node.","\u002Fmedia\u002Fcovers\u002Fcss-custom-highlight-api-no-dom-mutation.png",6,"2026-09-13T11:25:57.014Z",69,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"css","CSS",[64,66,69,72],{"slug":61,"name":65,"color":48},"Css",{"slug":67,"name":68,"color":48},"javascript","Javascript",{"slug":70,"name":71,"color":48},"webdev","Webdev",{"slug":73,"name":74,"color":48},"tutorial","Tutorial",{"assessments":76},1,{"slug":46,"title":78},"CSS Custom Highlight API — interactive playground",{"blocks":80,"version":76},[81,85,88,93,96,102,105,108,112,115,118,121,125,128,132,136,139,142,145,148,151,154,157,160,164,167,170,173,176,179,182,185,188,191,194,197,200,203],{"id":82,"html":83,"type":84},"b1","\u003Cp>A bug report lands: &quot;I selected a paragraph to quote it, hit copy, and pasted nothing.&quot; No screenshot that shows anything wrong, no repro steps that make sense. The reporter insists they didn&#39;t touch anything except drag-select some text and press ⌘C.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>They&#39;re right. They didn&#39;t touch anything else. But a few seconds earlier they&#39;d typed into the page&#39;s search box, and that box was still doing its job — highlighting every match in the paragraph they&#39;d just selected. It rebuilt that paragraph from scratch, the way search-highlight boxes have rebuilt paragraphs for years. Their selection wasn&#39;t corrupted. It was deleted, node by node, by code that had nothing to do with copying text at all.\u003C\u002Fp>",{"id":89,"html":90,"text":91,"type":92,"level":31},"b3","The two-line &quot;fix&quot; that is the bug","The two-line \"fix\" that is the bug","heading",{"id":94,"html":95,"type":84},"b4","\u003Cp>Here&#39;s the version almost everyone reaches for first — find matches with a regex, wrap them in \u003Ccode>&lt;mark&gt;\u003C\u002Fcode>, and shove the result back in with \u003Ccode>innerHTML\u003C\u002Fcode>:\u003C\u002Fp>",{"id":97,"code":98,"type":99,"language":100,"highlight":101},"b5","function highlight(container, rawText, query) {\n  if (!query) {\n    container.textContent = rawText;\n    return;\n  }\n  const escaped = query.replace(\u002F[.*+?^${}()|[\\]\\\\]\u002Fg, \"\\\\$&\");\n  const re = new RegExp(`(${escaped})`, \"gi\");\n  container.innerHTML = rawText.replace(re, \"\u003Cmark>$1\u003C\u002Fmark>\");\n}","code","js",[],{"id":103,"html":104,"type":84},"b6","\u003Cp>Wire it to an \u003Ccode>input\u003C\u002Fcode> event on a search box, and every keystroke re-runs it. It looks completely reasonable — and on dev.to it would even work, right up until you tried to select and copy something out of that paragraph while it was live.\u003C\u002Fp>",{"id":106,"html":107,"type":84},"b7","\u003Cp>\u003Cstrong>Guess before you scroll:\u003C\u002Fstrong> which line breaks the user&#39;s selection — and does it matter whether the selected text is one of the highlighted matches?\u003C\u002Fp>",{"id":109,"html":110,"text":111,"type":92,"level":31},"b8","Why \u003Ccode>innerHTML =\u003C\u002Fcode> breaks something it never mentions","Why innerHTML = breaks something it never mentions",{"id":113,"html":114,"type":84},"b9","\u003Cp>It&#39;s \u003Ccode>container.innerHTML = …\u003C\u002Fcode>. Not the regex, not the \u003Ccode>&lt;mark&gt;\u003C\u002Fcode> tag — the assignment itself. Setting \u003Ccode>innerHTML\u003C\u002Fcode> doesn&#39;t patch the existing children; it removes every one of them and parses a brand-new set from the string you gave it. There&#39;s no diffing, no reuse.\u003C\u002Fp>",{"id":116,"html":117,"type":84},"b10","\u003Cp>That matters because the browser&#39;s own text selection is a \u003Ccode>Range\u003C\u002Fcode> — two boundary points, each a reference to a specific node plus an offset into it. The moment the node a boundary point references is removed from the document, that reference has nothing left to point at, and the selection collapses to nothing. This happens \u003Cstrong>unconditionally\u003C\u002Fstrong>. It doesn&#39;t matter whether the text the user selected contains a search match, whether the match is at the start or the end of the paragraph, or whether the query is even one character long. Every child node is gone, so every Range anchored to those children is gone with it — including the one behind the highlight-yellow \u003Ccode>::selection\u003C\u002Fcode> the user is staring at.\u003C\u002Fp>",{"id":119,"html":120,"type":84},"b11","\u003Cp>The naive fix people try next — remember the selection&#39;s start\u002Fend offsets, rebuild, then reconstruct a new \u003Ccode>Range\u003C\u002Fcode> at the same offsets and reapply it with \u003Ccode>selection.addRange()\u003C\u002Fcode> — technically works, but now you&#39;re hand-rolling selection preservation on every keystroke, for a feature that&#39;s supposed to be \u003Cem>presentation only\u003C\u002Fem>. That&#39;s the tell that you&#39;re solving the problem on the wrong layer.\u003C\u002Fp>",{"id":122,"html":123,"text":124,"type":92,"level":31},"b12","The fix: style the text, don&#39;t touch the tree","The fix: style the text, don't touch the tree",{"id":126,"html":127,"type":84},"b13","\u003Cp>The actual bug isn&#39;t &quot;the wrong DOM surgery&quot; — it&#39;s that this needed DOM surgery at all. Highlighting a search match is a pure styling decision: which pixels get a background color. It never needed to change what node structure the paragraph has. The \u003Cstrong>CSS Custom Highlight API\u003C\u002Fstrong> does exactly that:\u003C\u002Fp>",{"id":129,"code":130,"type":99,"language":100,"highlight":131},"b14","function highlight(container, query) {\n  CSS.highlights.delete(\"search-results\");\n  if (!query) return;\n\n  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);\n  const q = query.toLowerCase();\n  const ranges = [];\n  let node;\n\n  while ((node = walker.nextNode())) {\n    const text = node.textContent.toLowerCase();\n    let i = 0;\n    while ((i = text.indexOf(q, i)) !== -1) {\n      const r = new Range();\n      r.setStart(node, i);\n      r.setEnd(node, i + q.length);\n      ranges.push(r);\n      i += q.length;\n    }\n  }\n\n  if (ranges.length) CSS.highlights.set(\"search-results\", new Highlight(...ranges));\n}",[],{"id":133,"code":134,"type":99,"language":61,"highlight":135},"b15","::highlight(search-results) {\n  background-color: gold;\n  color: #111;\n}",[],{"id":137,"html":138,"type":84},"b16","\u003Cp>Nothing here inserts, removes, or splits a single node. \u003Ccode>container.innerHTML\u003C\u002Fcode> is never touched — the paragraph&#39;s text nodes are exactly the ones the browser parsed on page load, forever. A \u003Ccode>Highlight\u003C\u002Fcode> is just a named set of \u003Ccode>Range\u003C\u002Fcode>s registered on \u003Ccode>CSS.highlights\u003C\u002Fcode>; the browser paints the styling as an overlay, the same way it paints native text selection. Whatever the user has selected with their mouse lives in its own untouched \u003Ccode>Range\u003C\u002Fcode>, on its own untouched nodes, completely unaware that a search feature exists.\u003C\u002Fp>",{"id":140,"html":141,"type":84},"b17","\u003C!-- playground:start -->",{"id":143,"html":144,"text":144,"type":92,"level":31},"b18","🎮 Try it yourself",{"id":146,"html":147,"type":84},"b19","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-custom-highlight-api-no-dom-mutation\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":149,"html":150,"type":84},"b20","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":152,"html":153,"type":84},"b21","\u003C!-- playground:end -->",{"id":155,"html":156,"text":156,"type":92,"level":31},"b22","Layering highlights without a turf war",{"id":158,"html":159,"type":84},"b23","\u003Cp>Real pages rarely have just one highlight. A code viewer might run syntax coloring \u003Cem>and\u003C\u002Fem> search-result highlighting over the same block, and they will overlap. \u003Ccode>Highlight\u003C\u002Fcode> objects have a \u003Ccode>priority\u003C\u002Fcode> property for exactly this: when two highlights cover the same character, the one with the higher \u003Ccode>priority\u003C\u002Fcode> paints on top, and for conflicting properties the styles apply in ascending priority order — last write wins. Leave \u003Ccode>priority\u003C\u002Fcode> unset and both default to \u003Ccode>0\u003C\u002Fcode>, so ties go to whichever \u003Ccode>Highlight\u003C\u002Fcode> was most recently registered, which is rarely what you want for &quot;search results should always sit above syntax coloring&quot;:\u003C\u002Fp>",{"id":161,"code":162,"type":99,"language":100,"highlight":163},"b24","CSS.highlights.set(\"syntax\", new Highlight(...syntaxRanges)); \u002F\u002F priority 0\nconst searchHl = new Highlight(...searchRanges);\nsearchHl.priority = 1; \u002F\u002F always wins the overlap\nCSS.highlights.set(\"search-results\", searchHl);",[],{"id":165,"html":166,"type":84},"b25","\u003Cp>One more layer sits above both, permanently: the browser&#39;s own built-in highlight pseudo-elements — \u003Ccode>::selection\u003C\u002Fcode>, \u003Ccode>::spelling-error\u003C\u002Fcode>, and friends — always paint on top of every custom highlight, regardless of \u003Ccode>priority\u003C\u002Fcode>. You can&#39;t outrank the user&#39;s own selection even if you tried, which is one more reason the old \u003Ccode>innerHTML\u003C\u002Fcode> approach was fighting the platform instead of using it.\u003C\u002Fp>",{"id":168,"html":169,"text":169,"type":92,"level":31},"b26","Where this actually stands today",{"id":171,"html":172,"type":84},"b27","\u003Cp>This isn&#39;t an experimental flag anymore. Chrome and Edge have shipped it since version 105 (2022), Safari added it in 17.2 (December 2023), and Firefox caught up in 140 (June 2025) — which is what pushed it to \u003Cstrong>Baseline: newly available\u003C\u002Fstrong> as of last year. One gap worth knowing before you ship: Firefox&#39;s implementation doesn&#39;t yet apply \u003Ccode>text-decoration\u003C\u002Fcode> or \u003Ccode>text-shadow\u003C\u002Fcode> inside \u003Ccode>::highlight()\u003C\u002Fcode>, though \u003Ccode>background-color\u003C\u002Fcode>, \u003Ccode>color\u003C\u002Fcode>, and \u003Ccode>font-weight\u003C\u002Fcode> all work everywhere. Stick to those three for anything cross-browser, and treat underlines or shadows as a Chromium\u002FSafari bonus, not a dependency.\u003C\u002Fp>",{"id":174,"html":175,"type":84},"b28","\u003C!-- quiz:start -->",{"id":177,"html":178,"text":178,"type":92,"level":31},"b29","🧠 Test yourself",{"id":180,"html":181,"type":84},"b30","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-custom-highlight-api-no-dom-mutation\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":183,"html":184,"type":84},"b31","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":186,"html":187,"type":84},"b32","\u003C!-- quiz:end -->",{"id":189,"html":190,"type":84},"b33","\u003Cp>The reporter&#39;s paste wasn&#39;t empty because of a rendering glitch or a browser quirk — it was empty because a completely unrelated feature had, as a side effect nobody designed on purpose, deleted the exact nodes their selection depended on. That&#39;s what happens when &quot;just restyle this text&quot; gets implemented as &quot;rebuild this text.&quot; If you&#39;re only changing how a range of text \u003Cem>looks\u003C\u002Fem>, there&#39;s now a browser API that never touches how it&#39;s \u003Cem>structured\u003C\u002Fem> — use it instead of \u003Ccode>innerHTML\u003C\u002Fcode>.\u003C\u002Fp>",{"id":192,"html":193,"type":84},"b34","\u003Cp>Go grep your own codebase for \u003Ccode>innerHTML =\u003C\u002Fcode> next to a regex and a \u003Ccode>&lt;mark&gt;\u003C\u002Fcode> tag. How many of those are one debounced keystroke away from eating someone&#39;s clipboard?\u003C\u002Fp>",{"id":195,"type":196},"b35","divider",{"id":198,"html":199,"type":84},"b36","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":201,"html":202,"type":84},"b37","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":204,"type":205,"items":206,"ordered":18},"b38","list",[207,208,209],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","A bug report lands: \"I selected a paragraph to quote it, hit copy, and pasted nothing.\" No screenshot that shows anything wrong, no repro steps that make sense. The reporter insists they didn't touch anything except drag-select some text and press ⌘C.\n\nThey're right. They didn't touch anything else. But a few seconds earlier they'd typed into the page's search box, and that box was still doing its job — highlighting every match in the paragraph they'd just selected. It rebuilt that paragraph from scratch, the way search-highlight boxes have rebuilt paragraphs for years. Their selection wasn't corrupted. It was deleted, node by node, by code that had nothing to do with copying text at all.\n\n## The two-line \"fix\" that is the bug\n\nHere's the version almost everyone reaches for first — find matches with a regex, wrap them in `\u003Cmark>`, and shove the result back in with `innerHTML`:\n\n```js\nfunction highlight(container, rawText, query) {\n  if (!query) {\n    container.textContent = rawText;\n    return;\n  }\n  const escaped = query.replace(\u002F[.*+?^${}()|[\\]\\\\]\u002Fg, \"\\\\$&\");\n  const re = new RegExp(`(${escaped})`, \"gi\");\n  container.innerHTML = rawText.replace(re, \"\u003Cmark>$1\u003C\u002Fmark>\");\n}\n```\n\nWire it to an `input` event on a search box, and every keystroke re-runs it. It looks completely reasonable — and on dev.to it would even work, right up until you tried to select and copy something out of that paragraph while it was live.\n\n**Guess before you scroll:** which line breaks the user's selection — and does it matter whether the selected text is one of the highlighted matches?\n\n## Why `innerHTML =` breaks something it never mentions\n\nIt's `container.innerHTML = …`. Not the regex, not the `\u003Cmark>` tag — the assignment itself. Setting `innerHTML` doesn't patch the existing children; it removes every one of them and parses a brand-new set from the string you gave it. There's no diffing, no reuse.\n\nThat matters because the browser's own text selection is a `Range` — two boundary points, each a reference to a specific node plus an offset into it. The moment the node a boundary point references is removed from the document, that reference has nothing left to point at, and the selection collapses to nothing. This happens **unconditionally**. It doesn't matter whether the text the user selected contains a search match, whether the match is at the start or the end of the paragraph, or whether the query is even one character long. Every child node is gone, so every Range anchored to those children is gone with it — including the one behind the highlight-yellow `::selection` the user is staring at.\n\nThe naive fix people try next — remember the selection's start\u002Fend offsets, rebuild, then reconstruct a new `Range` at the same offsets and reapply it with `selection.addRange()` — technically works, but now you're hand-rolling selection preservation on every keystroke, for a feature that's supposed to be *presentation only*. That's the tell that you're solving the problem on the wrong layer.\n\n## The fix: style the text, don't touch the tree\n\nThe actual bug isn't \"the wrong DOM surgery\" — it's that this needed DOM surgery at all. Highlighting a search match is a pure styling decision: which pixels get a background color. It never needed to change what node structure the paragraph has. The **CSS Custom Highlight API** does exactly that:\n\n```js\nfunction highlight(container, query) {\n  CSS.highlights.delete(\"search-results\");\n  if (!query) return;\n\n  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);\n  const q = query.toLowerCase();\n  const ranges = [];\n  let node;\n\n  while ((node = walker.nextNode())) {\n    const text = node.textContent.toLowerCase();\n    let i = 0;\n    while ((i = text.indexOf(q, i)) !== -1) {\n      const r = new Range();\n      r.setStart(node, i);\n      r.setEnd(node, i + q.length);\n      ranges.push(r);\n      i += q.length;\n    }\n  }\n\n  if (ranges.length) CSS.highlights.set(\"search-results\", new Highlight(...ranges));\n}\n```\n\n```css\n::highlight(search-results) {\n  background-color: gold;\n  color: #111;\n}\n```\n\nNothing here inserts, removes, or splits a single node. `container.innerHTML` is never touched — the paragraph's text nodes are exactly the ones the browser parsed on page load, forever. A `Highlight` is just a named set of `Range`s registered on `CSS.highlights`; the browser paints the styling as an overlay, the same way it paints native text selection. Whatever the user has selected with their mouse lives in its own untouched `Range`, on its own untouched nodes, completely unaware that a search feature exists.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-custom-highlight-api-no-dom-mutation\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Layering highlights without a turf war\n\nReal pages rarely have just one highlight. A code viewer might run syntax coloring *and* search-result highlighting over the same block, and they will overlap. `Highlight` objects have a `priority` property for exactly this: when two highlights cover the same character, the one with the higher `priority` paints on top, and for conflicting properties the styles apply in ascending priority order — last write wins. Leave `priority` unset and both default to `0`, so ties go to whichever `Highlight` was most recently registered, which is rarely what you want for \"search results should always sit above syntax coloring\":\n\n```js\nCSS.highlights.set(\"syntax\", new Highlight(...syntaxRanges)); \u002F\u002F priority 0\nconst searchHl = new Highlight(...searchRanges);\nsearchHl.priority = 1; \u002F\u002F always wins the overlap\nCSS.highlights.set(\"search-results\", searchHl);\n```\n\nOne more layer sits above both, permanently: the browser's own built-in highlight pseudo-elements — `::selection`, `::spelling-error`, and friends — always paint on top of every custom highlight, regardless of `priority`. You can't outrank the user's own selection even if you tried, which is one more reason the old `innerHTML` approach was fighting the platform instead of using it.\n\n## Where this actually stands today\n\nThis isn't an experimental flag anymore. Chrome and Edge have shipped it since version 105 (2022), Safari added it in 17.2 (December 2023), and Firefox caught up in 140 (June 2025) — which is what pushed it to **Baseline: newly available** as of last year. One gap worth knowing before you ship: Firefox's implementation doesn't yet apply `text-decoration` or `text-shadow` inside `::highlight()`, though `background-color`, `color`, and `font-weight` all work everywhere. Stick to those three for anything cross-browser, and treat underlines or shadows as a Chromium\u002FSafari bonus, not a dependency.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-custom-highlight-api-no-dom-mutation\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nThe reporter's paste wasn't empty because of a rendering glitch or a browser quirk — it was empty because a completely unrelated feature had, as a side effect nobody designed on purpose, deleted the exact nodes their selection depended on. That's what happens when \"just restyle this text\" gets implemented as \"rebuild this text.\" If you're only changing how a range of text *looks*, there's now a browser API that never touches how it's *structured* — use it instead of `innerHTML`.\n\nGo grep your own codebase for `innerHTML =` next to a regex and a `\u003Cmark>` tag. How many of those are one debounced keystroke away from eating someone's clipboard?\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":47,"canonical":212,"description":213},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-custom-highlight-api-no-dom-mutation","A search-highlight feature that rebuilds its DOM on every keystroke can wipe out a user's own text selection with no warning. The CSS Custom Highlight API fixes it by styling text ","01a080a9-801c-727a-ad92-8b3f69a35ea6",{"id":216,"locked":18},"01a080a9-804c-7197-8d76-75c150ca2b3a",[218],{"id":219,"slug":46,"title":220,"_count":221},"01a080a9-8074-709b-b471-8ae9fa943c81","CSS Custom Highlight API",{"questions":222},7,[224],{"locale":13,"slug":46},{"id":219,"slug":46,"title":220,"_count":226,"questionCount":222},{"questions":222},[228,230,232,236,238,240,244,248,252,256,259,262],{"slug":70,"name":71,"articles":229},93,{"slug":67,"name":68,"articles":231},78,{"slug":233,"name":234,"articles":235},"frontend","Frontend",70,{"slug":61,"name":65,"articles":237},32,{"slug":73,"name":74,"articles":239},24,{"slug":241,"name":242,"articles":243},"typescript","Typescript",14,{"slug":245,"name":246,"articles":247},"performance","Performance",12,{"slug":249,"name":250,"articles":251},"react","React",11,{"slug":253,"name":254,"articles":255},"browser","Browser",8,{"slug":257,"name":258,"articles":255},"node","Node",{"slug":260,"name":261,"articles":51},"grammar","Grammar",{"slug":263,"name":264,"articles":51},"programming","Programming",{"items":266,"meta":392},[267,278,299,320,338,356,374],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":235,"likeCount":19,"commentCount":19,"author":268,"vertical":269,"topic":270,"tags":271,"_count":276,"playground":277,"hasQuiz":17,"hasPlayground":17},{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[272,273,274,275],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":279,"slug":280,"title":281,"subtitle":48,"excerpt":282,"coverUrl":283,"locale":13,"readingMinutes":284,"publishedAt":285,"viewCount":286,"likeCount":19,"commentCount":19,"author":287,"vertical":288,"topic":289,"tags":290,"_count":297,"playground":298,"hasQuiz":17,"hasPlayground":17},"01a066ea-291a-77d8-aef4-d310318f61a9","css-user-invalid-native-form-validation-timing","Your Required Field Turns Red Before You Type Anything","A signup form's email input shows a red border the instant the page loads — before the visitor has typed a single character. The obvious CSS causes it, and the obvious fix is a native pseudo-class nobody reaches for first.","\u002Fmedia\u002Fcovers\u002Fcss-user-invalid-native-form-validation-timing.png",5,"2026-09-08T10:56:30.073Z",129,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[291,292,295,296],{"slug":61,"name":65,"color":48},{"slug":293,"name":294,"color":48},"forms","Forms",{"slug":70,"name":71,"color":48},{"slug":233,"name":234,"color":48},{"assessments":76},{"slug":280},{"id":300,"slug":301,"title":302,"subtitle":48,"excerpt":303,"coverUrl":304,"locale":13,"readingMinutes":305,"publishedAt":306,"viewCount":307,"likeCount":19,"commentCount":19,"author":308,"vertical":309,"topic":310,"tags":311,"_count":318,"playground":319,"hasQuiz":17,"hasPlayground":17},"01a061c4-985b-758b-86a4-21f6b2fd81f8","css-accent-color-native-form-control-tinting","One CSS Property Replaces Your Checkbox Hack","Recoloring a checkbox to match the brand usually means appearance: none, a hand-drawn checkmark, and a rebuilt focus ring. accent-color does the same job in one line — and knowing where it stops matters as much as knowing that it works.","\u002Fmedia\u002Fcovers\u002Fcss-accent-color-native-form-control-tinting.png",4,"2026-09-07T12:02:34.132Z",178,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[312,313,314,315],{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":233,"name":234,"color":48},{"slug":316,"name":317,"color":48},"accessibility","Accessibility",{"assessments":76},{"slug":301},{"id":321,"slug":322,"title":323,"subtitle":48,"excerpt":324,"coverUrl":325,"locale":13,"readingMinutes":51,"publishedAt":326,"viewCount":327,"likeCount":19,"commentCount":19,"author":328,"vertical":329,"topic":330,"tags":331,"_count":336,"playground":337,"hasQuiz":17,"hasPlayground":17},"01a037b3-f71a-755b-9820-9f2489daaebd","css-aspect-ratio-replace-padding-hack","Delete Your padding-bottom Aspect-Ratio Hack","For a decade the only way to keep a responsive box's shape was a padding percentage and an absolutely positioned wrapper. One CSS property replaces it — with one gotcha on real images nobody warns you about.","\u002Fmedia\u002Fcovers\u002Fcss-aspect-ratio-replace-padding-hack.png","2026-08-30T11:36:40.014Z",269,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[332,333,334,335],{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":233,"name":234,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":322},{"id":339,"slug":340,"title":341,"subtitle":48,"excerpt":342,"coverUrl":343,"locale":13,"readingMinutes":305,"publishedAt":344,"viewCount":345,"likeCount":19,"commentCount":19,"author":346,"vertical":347,"topic":348,"tags":349,"_count":354,"playground":355,"hasQuiz":17,"hasPlayground":17},"01a03297-a2d3-7318-bffe-5edb58824c5d","css-focus-visible-keyboard-focus-rings","The outline: none Debate Has Been Over Since 2020","A designer flags the ugly blue focus ring in review. An engineer deletes it with outline: none. Nobody notices the keyboard-only user who now can't tell where they are on the page.","\u002Fmedia\u002Fcovers\u002Fcss-focus-visible-keyboard-focus-rings.png","2026-08-29T12:32:09.792Z",252,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[350,351,352,353],{"slug":61,"name":65,"color":48},{"slug":316,"name":317,"color":48},{"slug":70,"name":71,"color":48},{"slug":233,"name":234,"color":48},{"assessments":76},{"slug":340},{"id":357,"slug":358,"title":359,"subtitle":48,"excerpt":360,"coverUrl":361,"locale":13,"readingMinutes":284,"publishedAt":362,"viewCount":363,"likeCount":19,"commentCount":19,"author":364,"vertical":365,"topic":366,"tags":367,"_count":372,"playground":373,"hasQuiz":17,"hasPlayground":17},"01a02d61-19cf-72cd-8490-a59e14f8839c","css-grid-auto-fit-vs-auto-fill","Your Grid Has Invisible Columns: auto-fill vs auto-fit","A responsive card grid that works perfectly with twelve items breaks with two — not from a bug, but from one word in a repeat() you copy-pasted without reading.","\u002Fmedia\u002Fcovers\u002Fcss-grid-auto-fit-vs-auto-fill.png","2026-08-28T17:00:06.666Z",414,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[368,369,370,371],{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":233,"name":234,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":358},{"id":375,"slug":376,"title":377,"subtitle":48,"excerpt":378,"coverUrl":379,"locale":13,"readingMinutes":284,"publishedAt":380,"viewCount":381,"likeCount":19,"commentCount":19,"author":382,"vertical":383,"topic":384,"tags":385,"_count":390,"playground":391,"hasQuiz":17,"hasPlayground":17},"01a02319-4485-7310-b494-dc94bdbfb49c","css-field-sizing-auto-grow-textarea","Delete Your Auto-Resize Textarea JS. CSS Does It Now.","The scrollHeight hack everyone copy-pastes for auto-growing textareas has a one-line CSS replacement — plus the one gotcha that bites if you skip max-height.","\u002Fmedia\u002Fcovers\u002Fcss-field-sizing-auto-grow-textarea.png","2026-08-26T06:56:38.974Z",311,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[386,387,388,389],{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":233,"name":234,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":376},{"page":76,"perPage":222,"total":237,"totalPages":284},"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">highlight\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>container\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> rawText\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> query\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>query\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    container\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>textContent \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> rawText;\n    \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> escaped \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> query\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">replace\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-num\">[.*+?^${}()|[\\]\\\\]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">g\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\\\\$&#38;\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> re \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">RegExp\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>escaped\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">)`\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"gi\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  container\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>innerHTML \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> rawText\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">replace\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>re\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"&lt;mark&gt;$1&lt;\u002Fmark&gt;\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003Cdiv>22\u003C\u002Fdiv>\u003Cdiv>23\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">highlight\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>container\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> query\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-num\">CSS\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>highlights\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">delete\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"search-results\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>query\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> walker \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> document\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">createTreeWalker\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>container\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-class\">NodeFilter\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-num\">SHOW_TEXT\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> q \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> query\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">toLowerCase\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> ranges \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> node;\n\n  \u003Cspan class=\"shj-kwd\">while\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>node \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> walker\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">nextNode\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> text \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> node\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>textContent\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">toLowerCase\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> i \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">while\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>i \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> text\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">indexOf\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>q\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> i\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">!==\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan>\u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> r \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Range\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n      r\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">setStart\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>node\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> i\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n      r\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">setEnd\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>node\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> i \u003Cspan class=\"shj-oper\">+\u003C\u002Fspan> q\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>length\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n      ranges\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">push\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>r\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n      i \u003Cspan class=\"shj-oper\">+=\u003C\u002Fspan> q\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>length;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>ranges\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>length\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-num\">CSS\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>highlights\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">set\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"search-results\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Highlight\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>ranges\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">::highlight\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">search-results\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">background-color\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">gold\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">color\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">#111\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-num\">CSS\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>highlights\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">set\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"syntax\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Highlight\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>syntaxRanges\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F priority 0\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> searchHl \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Highlight\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">...\u003C\u002Fspan>searchRanges\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\nsearchHl\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>priority \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F always wins the overlap\n\u003C\u002Fspan>\u003Cspan class=\"shj-num\">CSS\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>highlights\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">set\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"search-results\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> searchHl\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":398},[]]