<?xml version="1.0" encoding="utf-8"?>
  <rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom"
    xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
    xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
    xmlns:georss="http://www.georss.org/georss"
    xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
  >
    <channel>
      <title>Piccalilli - CSS topic archive</title>
      <link>https://piccalil.li/</link>
      <atom:link href="https://piccalil.li/category/css.xml" rel="self" type="application/rss+xml" />
      <description>We are Piccalilli. A publication dedicated to providing high quality educational content to level up your front-end skills.</description>
      <language>en-GB</language>
      <copyright>Piccalilli - CSS topic archive 2026</copyright>
      <docs>https://www.rssboard.org/rss-specification</docs>
      <pubDate>Thu, 15 Oct 2026 10:02:42 GMT</pubDate>
      <lastBuildDate>Thu, 15 Oct 2026 10:02:42 GMT</lastBuildDate>

      
      <item>
        <title>Stop buttons triggering zoom when they’re double tapped</title>
        <link>https://piccalil.li/blog/stop-buttons-triggering-zoom-when-theyre-double-tapped/?ref=css-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Wed, 23 Sep 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/stop-buttons-triggering-zoom-when-theyre-double-tapped/?ref=css-category-rss-feed</guid>
        <description><![CDATA[<p>I stumbled across <a href="https://bsky.app/profile/renderg.host/post/3mvipol2rzk2m">this great project</a> by <a href="https://renderg.host/">Barry Prendergast</a>. It's a web-based radio player with a <em>lush</em> UI. On mobile I spotted that as I was rapidly flicking through the stations to find one that was on air, the browser was zooming in and out.</p>
<p>Here's a screen clip:</p>
<p></p><p>Watch the <a href="https://iframe.mediadelivery.net/embed/468647/040189f2-c87a-4fda-a3bf-c65bf2254edb?autoplay=false&amp;loop=false&amp;muted=false&amp;preload=true&amp;responsive=true">video</a>.</p><p></p>
<p>I thought this behaviour was related to their viewport meta tag setup, but they have that set up perfectly:</p>
<pre><code>&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
</code></pre>
<p>Now, you could prevent the zooming by adding <code>, maximum-scale=1.0, user-scalable=no</code> to the <code>content</code> attribute, but <strong>don't do that</strong> because it prevents zooming at all, which is a <a href="https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html">WCAG violation</a>.</p>
<p>What's actually happening here is the browser is responding to the double tap gesture, which in the case of Safari on iOS, zooms into the button element I'm tapping. Luckily there's a CSS one-liner for us that gets the job done.</p>
<pre><code>.button {
  touch-action: manipulation;
}
</code></pre>
<p>As per <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/touch-action">MDN</a>:</p>
<blockquote>
<p>Enable panning and pinch zoom gestures, but disable additional non-standard gestures such as double-tap to zoom. Disabling double-tap to zoom removes the need for browsers to delay the generation of <strong>click</strong> events when the user taps the screen. This is an alias for "<strong>pan-x pan-y pinch-zoom</strong>" (which, for compatibility, is itself still valid).</p>
</blockquote>
<p></p>
<p>Let's see it in action. First, here's a couple of buttons without <code>touch-action</code>. Try rapidly tapping on your phone/touch device:</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/emvBdae/c0ed51c7ebc182fe12fec6967a92c584">Buttons without touch-action applied</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p><a href="https://cdpn.io/pen/debug/emvBdae/c0ed51c7ebc182fe12fec6967a92c584">This link</a> has no CodePen framing to make that easier for you</p>
<p>Here are the same buttons <em>with</em> <code>touch-action</code> applied:</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/myWOrzY/1c43253e5ffc8a38d68dd7fc42cccf7a">Buttons with touch-action applied</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Again, <a href="https://cdpn.io/pen/debug/myWOrzY/1c43253e5ffc8a38d68dd7fc42cccf7a">here's a frameless version</a> for you</p>
<p>Handy!</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>A decent custom checkbox pattern for until ::checkmark is ready</title>
        <link>https://piccalil.li/blog/a-decent-custom-checkbox-for-until-checkmark-is-ready/?ref=css-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Thu, 17 Sep 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/a-decent-custom-checkbox-for-until-checkmark-is-ready/?ref=css-category-rss-feed</guid>
        <description><![CDATA[<p>Now that we can better customise <code>&lt;select&gt;</code> elements, it's only natural to side-eye other form <code>&lt;input&gt;</code> types that have caused us visual headaches.</p>
<p>Sure, we should be applying the <em>lightest</em> of touches to form elements, especially, but even with a bit of visual-massaging, checkboxes are limited, aside from a bit of <code>accent-color</code>.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/pvebLwo/01786b2d24fcc35066571a56df54c09f">Standard checkbox with accent colour</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>There is a brighter future incoming, if you're to read the <a href="https://www.w3.org/TR/css-forms-1/#checkmark">spec</a>:</p>
<blockquote>
<p>The ::checkmark pseudo-element represents an indicator of whether the item is checked, and is present on checkboxes, radios, and option elements.</p>
<p>— <a href="https://www.w3.org/TR/css-forms-1/#checkmark">W3C forms level 1</a></p>
</blockquote>
<p>Match that with <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/appearance#formal_syntax"><code>appearance: base</code></a>, which is also incoming, and we're looking at this sort of CSS:</p>
<pre><code>input[type="checkbox"] {
  appearance: base;
}

input[type="checkbox"]::checkmark {
  content: url("data:image/svg+xml,%3Csvg aria-hidden='true' focusable='false' width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m5 12l5 5L20 7' /%3E %3C/svg%3E");
}
</code></pre>
<p>We're miles off from that capability yet — it doesn't look like any browser is working on it —  so allow me to show you how to build a nice custom checkbox pattern for until we have the browser capabilities we're after.</p>
<p></p>
<h2>HTML first, always</h2>
<p>It's always right to start with some good quality markup:</p>
<pre><code>&lt;label for="custom-checkbox" class="checkbox"&gt;
  &lt;span class="checkbox__box"&gt;
    &lt;input type="checkbox" name="custom-checkbox" id="custom-checkbox" value="Some value that this control toggles"&gt;
    &lt;svg aria-hidden="true" focusable="false" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
      &lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m5 12l5 5L20 7" /&gt;
    &lt;/svg&gt;
  &lt;/span&gt;
  &lt;span&gt;A long label for this checkbox to make sure we get a nice wrapping behaviour&lt;/span&gt;
&lt;/label&gt;
</code></pre>
<p>The markup is pretty straightforward here. Inside the parent <code>&lt;label&gt;</code> — which is linked to the input both by being a parent <em>and</em> the <code>for</code>/<code>id</code> attributes — we have a container for the input and icon, along with a text label.</p>
<p>The reason I'm using <code>&lt;span&gt;</code> elements here is because aside from the input/SVG only <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Content_categories#phrasing_content">phrasing content</a> is permitted. I don't think a <code>&lt;div&gt;</code> would do any harm here, but it's best to do things right.</p>
<p>On the SVG checkmark element, there's an <code>aria-hidden="true"</code> attribute. This stops the SVG — a visual element — getting in the way for assistive technology. I've also added <code>focusable="false"</code>. This is actually a relic from the Internet Explorer days hell, but I keep it on visual only icons, just in case.</p>
<p>Right, we're in good shape. Let's make it look good.</p>
<p></p>
<h2>Some CSS</h2>
<p>The first thing to do is layout:</p>
<pre><code>.checkbox {
  display: flex;
  align-items: baseline;
  gap: 1em;
  text-wrap: balance;
}
</code></pre>
<p>Flex is more than capable here. I like to align on the baseline in this sort of context because as the viewport gets small and the text wraps, we don't want a vertically centered layout. It looks rubbish!</p>
<p>Speaking of balance, I'm using <code>text-wrap: balance</code> here for the same compressed viewport context and dealing with wrapping text. Keeping a consistent edge (rag) is extra important for small microcopy, such as labels.</p>
<p>Let's tackle the input itself.</p>
<pre><code>.checkbox input {
  margin: 0;
  width: 100%;
  height: 100%;
  appearance: none;
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 0.2em; /* This is so the focus ring has a matching radius to the visual box */
}
</code></pre>
<p>We've got to be <em>really</em> careful here because we don't want to mess up the focusability of our element. Combining <code>appearance: none</code> and absolute positioning, our element is still there, but its no longer in the way, <em>visually</em>. It can still receive focus and will present a focus ring, which is exactly what we need!</p>
<p>Let's tackle the "box" part, which is also this <code>&lt;input&gt;</code>'s parent.</p>
<pre><code>.checkbox__box {
  position: relative;
  background: transparent;
  color: currentcolor;
  border: 1px solid;
  width: 1.4em;
  height: 1.4em;
  transform: translateY(0.75ex);
  flex-shrink: 0;
  border-radius: 0.2em;
}
</code></pre>
<p>A lot of this is self explanatory but I'll pick up the key parts:</p>
<ol>
<li>I'm using <code>position: relative</code> so the <code>&lt;input&gt;</code> stays inside this box</li>
<li>The <code>transform</code> rule is a <em>bit</em> of a magic-number but because it's a relative <a href="https://every-layout.dev/rudiments/units/"><code>ex</code>
unit</a>, it scales quite nicely, regardless of parent font size. Most importantly the <code>ex</code> enhances that <code>baseline</code> alignment <em>and</em> fixes the initial alignment of the <code>&lt;input&gt;</code></li>
</ol>
<p>I really like how <a href="https://ishadeed.com/article/aligning-list-icons/">Ahmad Shadeed approaches this</a> too. That's the beauty of CSS: there's plenty of ways to do things well! </p>
<p>Let's deal with the SVG checkmark next:</p>
<pre><code>.checkbox__box svg {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  display: none;
  width: 1em;
  height: 1em;
}
</code></pre>
<p>I guess I could use <a href="https://piccalil.li/blog/getting-started-with-css-custom-properties/">logical properties</a> here, but we're positioning an icon in a box, so the "old" way works perfectly well.</p>
<p>The idea here is to visually hide the checkmark when the checkbox isn't checked and show it when it is. We'll deal with that CSS next.</p>
<div><h2>FYI</h2>
This is a situation that very few people will likely find themselves in. What I'd recommend is having a couple of SVG elements that get toggled in that context. 
</div>
<pre><code>.checkbox__box:has(input:checked) {
  background: white;
}

.checkbox__box input:checked + svg {
  display: block;
}
</code></pre>
<p>We're in checked state territory here. I'm setting a white background using <code>:has()</code> which is <em>yet another</em> <a href="https://piccalil.li/blog/some-little-ways-im-using-css-has-in-the-real-world/">useful use-case</a> for this endlessly handy addition to CSS.</p>
<p>The following block of CSS uses a traditional next sibling selector to show the SVG element when the input is checked. You could use <code>:has()</code> here too, if you're feeling fancy.</p>
<p>With all of that CSS in place, we're looking good.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/MYpeyGd/f1c611739fe7d2308ca6102877c53711">Custom checkbox</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p></p>
<h2>This approach could also work for radio buttons</h2>
<p>There's nothing stopping you using this approach for radio buttons. Check out this demo where I'm using a circle icon instead of a checkmark. It works well!</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/XJMKKep/edef02c55d0d972b66682ee3443858bb">Custom radio buttons</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<h2>The em units usage allows this whole component to scale</h2>
<p>The eagle eyed amongst us will have noticed that aside from the <code>transform</code> rules, I've consistently use <code>em</code> units. The reason for this is so our checkbox can scale with no other intervention.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/QwpEmZw/df33cad21fa4c40a91912bb559886c1b">Custom checkbox - massive edition</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>The only change here, versus the first demo is a <code>font-size</code> declaration on the <code>.checkbox</code> component.</p>
<p>A handy pattern, right?</p>
<hr />
<p>A big thanks to <a href="https://jakearchibald.com/">Jake Archibald</a> and <a href="https://heydonworks.com/">Heydon Pickering</a> for checking my homework.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Working with ::highlight() using progressive enhancement </title>
        <link>https://piccalil.li/blog/working-with-highlight-using-progressive-enhancement/?ref=css-category-rss-feed</link>
        <dc:creator><![CDATA[Sunkanmi Fafowora]]></dc:creator>
        <pubDate>Thu, 06 Aug 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/working-with-highlight-using-progressive-enhancement/?ref=css-category-rss-feed</guid>
        <description><![CDATA[<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-elements#highlight_pseudo-elements">Highlighting in CSS</a> has been beneficial for applying a highlight on specific text or text fragments during user selection, emphasizing a piece of information on a website, or visually emphasizing a text for the sake of branding. Particularly, this is pretty helpful when users want to scan your document from top to bottom because the <a href="https://www.nngroup.com/articles/concise-scannable-and-objective-how-to-write-for-the-web/">majority of people don’t read your document initially; they scan</a>.</p>
<p>On the web, text highlights are a good way to lay emphasis on text fragments through good ol’ CSS. From <code>::selection</code> for styling selected text to <code>::target-text</code> which styles highlighted text from Google searches, and in my opinion, CSS’ most powerful pseudo-element for highlighting: <code>::highlight()</code> which applies a custom highlight to a text fragment.</p>
<p>In this article, we will look into how the <code>::highlight()</code> pseudo-element works, the API behind this pseudo-element, and explore a fallback feature for this technology because it relies <strong>heavily on JavaScript (JS).</strong></p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/XJjwNap">::highlight() demo: pure text-shadows</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p></p>
<h1>The CSS Custom Highlight API</h1>
<p>Typically, a highlight or highlighted text is what you’d see during web searches or when you what to select a text fragment to copy, or even when you make a mistake in a word processor (the squiggly red underlines)<em>.</em> In CSS, you can achieve these through the highlight pseudo-elements like<code>::search-text</code>, <code>::selection</code>, <code>::spelling-error</code> , and <code>::grammar-error</code> . But, what about plain highlights like the demo above? That’s where the CSS Custom Highlight API comes in.</p>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a> is an API for text highlighting on a range of text using JavaScript and CSS. It extends the pseudo-elements for highlighting (<code>::search-text</code> , <code>::selection</code>, <code>::spelling-error</code> ) and lets you customize text fragments with <code>::highlight()</code> and JavaScript. <code>::highlight()</code> is what will be our focus for this article, and how we can programmatically highlight text and its fragments using JavaScript and CSS.</p>
<h1>How ::highlight() works</h1>
<p>To create highlighted text like in the initial example, you need to know 4 total steps, which include:</p>
<ol>
<li>Creating the highlight buckets by creating instances of the <code>Highlight()</code> class</li>
<li>Register each highlight instance into <code>CSS.highlights</code></li>
<li>Create <code>Range()</code> objects each with different points on the text for highlighting and add <code>Range()</code> objects to its highlight instances</li>
<li>Style with <code>::highlight()</code> pseudo-element</li>
</ol>
<p>Before we move on with an example, let me explain something because this is where it might get tricky. What we do around here is <strong>build with <a href="https://piccalil.li/blog/its-about-time-i-tried-to-explain-what-progressive-enhancement-actually-is/">progressive enhancement</a> in mind first</strong>. So the first question you should ask before anything is “what happens <a href="https://piccalil.li/blog/a-handful-of-reasons-javascript-wont-be-available/"><em>when</em> JS fails</a>?”. Well, I’m glad you asked.</p>
<p>Let’s say, for our example, we want to create two highlight objects and use that to style a simple poetic text that reads “<em>fire and ice live inside every word.</em>” Because we care about our users, we set each text for highlight with the <code>&lt;mark&gt;</code> HTML tag setting the <code>class</code> attribute to either <code>gold</code> or <code>ice</code> , depending on the highlighting style we wish the text to have. This acts as a fallback highlight style in case our JavaScript fails or the browser doesn’t support custom highlighting.</p>
<p>We also apply an <code>id</code> to them in case JS is available too for our CSS Custom Highlight API. With all that in mind, our HTML would look like this:</p>
<pre><code>&lt;main&gt;
  &lt;h1&gt;Two Highlights Demo&lt;/h1&gt;
  &lt;p id="line"&gt;
    &lt;mark id="fire" class="gold"&gt;fire&lt;/mark&gt; and
    &lt;mark id="ice" class="ice"&gt;ice&lt;/mark&gt; live inside 
    &lt;mark id="every" class="ice"&gt;every&lt;/mark&gt; 
    &lt;mark id="word" class="gold"&gt;word&lt;/mark&gt;
  &lt;/p&gt;
&lt;/main&gt;
</code></pre>
<p>Then, we proceed to query each tagged word in our JS applying the <code>firstChild</code> property to each of them of get the element’s first child node which we will use later:</p>
<pre><code>const fireNode = document.querySelector("#fire").firstChild;
const everyNode = document.querySelector("#every").firstChild;
const iceNode = document.querySelector("#ice").firstChild;
const wordNode = document.querySelector("#word").firstChild;
</code></pre>
<p>Finally, we can proceed with the steps on creating a custom highlight in CSS.</p>
<p></p>
<h2>Create instances of <code>Highlight()</code> class</h2>
<p>In order to create a custom highlight, the first step is to create an instance of the <code>Highlight()</code> class which will house the highlight styling we want a text or text fragment to have. For our demo, we’ll be creating two highlight objects named using the <code>Highlight()</code> class. One to give a golden color representing fire and the other to give a blue color representing ice:</p>
<pre><code>const goldHL = new Highlight();
const iceHL = new Highlight();
</code></pre>
<h2>Register each highlight instance in CSS.highlights</h2>
<p>Next, we register the created highlight instances in the <code>HighlightRegistry</code> via <code>CSS.highlights</code> <code>set()</code> method. We map a valid CSS identifier to the instance for CSS styling later.</p>
<pre><code>CSS.highlights.set("hl-gold", goldHL);
CSS.highlights.set("hl-ice", iceHL);
</code></pre>
<h2>Create Range() objects each with different points on the text for highlighting</h2>
<p>In this step, we will be creating a <code>Range()</code> object for each text fragment we queried earlier for highlighting. We will then apply the highlight we want on each selected text. For the first word “fire”, we create a <code>Range()</code> object called <code>r1</code> , and we set the start node to the first letter “f” using <code>setStart</code> on <code>r1</code> . <code>setStart()</code> accepts two values. It accepts the node we’re targeting (in our case for “fire”, its <code>fireNode</code> ) and the index of the text on the node.</p>
<p>Now, because we want to target the whole text “fire”, we have to also set where the range will stop. And this will be set using <code>setEnd()</code> . <code>setEnd()</code> accepts two values like <code>setStart()</code> on the range object (<code>r1</code>). It accepts the node we’re targeting (<code>fireNode</code> ) and the index of the end text “e” (as in the “e” in “fire”) using <code>fireNode.textContent.length</code> - 1 which gives us the last index of the text.</p>
<p>Finally, we add the range object into the set instance.</p>
<pre><code>const r1 = new Range();
r1.setStart(fireNode, 0);
r1.setEnd(fireNode, fireNode.textContent.length - 1);
goldHL.add(r1);
</code></pre>
<p>This step is repeated for <code>everyNode</code>, <code>iceNode</code>, and <code>wordNode</code>. Typically, you’d want to use a loop for this, but because this is really small, writing it in a specific manner will suffice, especially to help you understand how this works too.</p>
<pre><code>const r2 = new Range();
r2.setStart(wordNode, 0);
r2.setEnd(wordNode, wordNode.textContent.length - 1);
goldHL.add(r2);

const r3 = new Range();
r3.setStart(iceNode, 0);
r3.setEnd(iceNode, iceNode.textContent.length - 1);
iceHL.add(r3);

const r4 = new Range();
r4.setStart(everyNode, 0);
r4.setEnd(everyNode, everyNode.textContent.length - 1);
iceHL.add(r4);
</code></pre>
<h2>Style with ::highlight() pseudo-element</h2>
<p>Remember how we said we should <strong>think progressive enhancement first?</strong> Well, in order to achieve that for this demo in particular, we need to style the <code>&lt;mark&gt;</code>ed highlighted text first, then, we style the <code>::highlight()</code> pseudo-element. For that to work, we styled text marked with the <code>gold</code> class to be golden in <code>oklch()</code> with a glowy text shadow of similar color. We style text <code>&lt;mark&gt;</code> ed with the <code>ice</code> class to be blueish in <code>oklch()</code> with a glowy text shadow of similar color:</p>
<pre><code>mark {
  background: none;
}

mark.gold {
  color: oklch(88% 0.16 75);
  text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}

mark.ice {
  color: oklch(82% 0.1 215);
  text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/azBoBBo">::highlight() demo: two highlights without `::highlight()`</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Viola! (or how do they say it?) It looks amazing! 🤩</p>
<p>Even without the styled <code>::highlight()</code> , it works out pretty well. But, that’s not our only aim though. We still need to add the styling for both our highlight objects.</p>
<pre><code>::highlight(hl-gold) {
  color: oklch(88% 0.16 75);
  text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}

::highlight(hl-ice) {
  color: oklch(82% 0.1 215);
  text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}
</code></pre>
<p>Done! It’s pretty much the same styling we did for our <code>&lt;mark&gt;</code> tag classes and in case JS fails or the browser doesn’t support it, we wrap the <code>::higlight()</code> pseudo-elements in a <code>@supports</code> container:</p>
<pre><code>@supports selector(::highlight(h1-gold)) {
  ::highlight(hl-gold) {
    color: oklch(88% 0.16 75);
    text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
  }

  ::highlight(hl-ice) {
    color: oklch(82% 0.1 215);
    text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
  }
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/qEqWZYN">::highlight() demo: two highlights with fallback</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p></p>
<h2>Will you be using ::highlight()?</h2>
<p>I know for sure I will be using this feature in my work. This article explained highlighting in CSS, the CSS Custom Highlight API, and how <code>::higlight()</code> works in CSS with a fallback provision, for when JavaScript isn’t available or the browser doesn’t support it.</p>
<p>If you’re looking to read up more about the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a>, MDN has a good guide on the topic for you.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Navigating the age-old problem of checkmarks in UI with progressive enhancement</title>
        <link>https://piccalil.li/blog/navigating-the-age-old-problem-of-checkmarks-in-ui-with-progressive-enhancement/?ref=css-category-rss-feed</link>
        <dc:creator><![CDATA[Sunkanmi Fafowora]]></dc:creator>
        <pubDate>Thu, 28 May 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/navigating-the-age-old-problem-of-checkmarks-in-ui-with-progressive-enhancement/?ref=css-category-rss-feed</guid>
        <description><![CDATA[<p>The <code>::checkmark</code> pseudo-element was introduced in <a href="https://drafts.csswg.org/css-forms-1/"><strong>CSS Form Control Styling Module Level 1</strong></a> and it’s a powerful CSS feature to say the least. I even wrote about it as an <a href="https://css-tricks.com/almanac/pseudo-selectors/c/checkmark/">almanac entry for CSS-Tricks in 2025</a> to share what this pseudo-element can do.</p>
<p>The CSS <code>::checkmark</code> pseudo-element is used to style the checked state of input elements with checkers including the <code>&lt;select&gt;</code> dropdown, checkboxes, and radio buttons. There’s one problem: at the time of writing, <code>::checkmark</code> lacks browser support on two major browsers: Safari &amp; Firefox.</p>
<p>That’s not all.</p>
<p><a href="https://drafts.csswg.org/css-forms-1/#checkmark">According to the specification</a>, <code>::checkmark</code> is supposed to support styling of checkmarks present in checkboxes, radios, and option elements, but again, at the time of writing, it’s only supported for <code>&lt;option&gt;</code> elements in the <code>&lt;select&gt;</code> dropdown.</p>
<p>This means that <code>::checkmark</code> , as simple, powerful, and useful as it is, is still very limited, and that’s a problem. So, do we rely on the traditional solutions we used to use in the past to style checkmarks? Are they better? In this article, we will be exploring how the problem of customised checkmarks was solved in the past and compare those solutions with the modern <code>::checkmark</code> solution, providing recommendations as we go.</p>
<p></p>
<h2>How we’ve traditionally approached checkmarks in menus</h2>
<p>To create and style a traditional checkmark similar to the ones you see in <code>&lt;option&gt;</code> elements under <code>&lt;select&gt;</code> , first, we would create a custom dropdown to house the checkmark. To do that, we would have to consider a bunch of interactive states and rules. Your browser doesn’t provide these with non-semantic elements, so taking a few notes from <a href="https://css-tricks.com/striking-a-balance-between-native-and-custom-select-elements/">Sandrina’s article</a>, we need to have:</p>
<ul>
<li>A dropdown with the current selected option</li>
<li>Box toggling ability to toggle the visibility of the options</li>
<li>Clicking an option in the list updates the dropdown value. The dropdown text changes, and the list MUST be closed</li>
<li>Clicking outside closes the list</li>
<li>A customizable select dropdown icon to represent our picker</li>
<li>A customizable select checkmark icon to represent our checkmark for any selected option</li>
</ul>
<p>Here’s how our HTML would likely look:</p>
<pre><code>&lt;div id="cs"&gt;
    &lt;div id="cs-btn"&gt;
      &lt;span id="btn-lbl"&gt;::before&lt;/span&gt;
      &lt;span class="btn-arrow"&gt;▼&lt;/span&gt;
    &lt;/div&gt;
    
    /* list menu */
    &lt;div class="cs-menu"&gt;
      &lt;div class="item" data-sel data-v=":before"&gt;
        &lt;span class="checkmark"&gt;✓&lt;/span&gt;
        &lt;span class="label"&gt;::before&lt;/span&gt;
          
        &lt;/div&gt;
      &lt;div class="item" data-v="::after"&gt;
        &lt;span class="checkmark"&gt;✓&lt;/span&gt;
        &lt;span class="label"&gt;::after&lt;/span&gt; 
        &lt;/div&gt;
      &lt;div class="item" data-v="::marker"&gt;
        &lt;span class="checkmark"&gt;✓&lt;/span&gt;
        &lt;span class="label"&gt;::marker&lt;/span&gt;  
        &lt;/div&gt;
      &lt;div class="item" data-v="::selection"&gt;
        &lt;span class="checkmark"&gt;✓&lt;/span&gt;
        &lt;span class="label"&gt;::selection&lt;/span&gt;
        &lt;/div&gt;
        
      &lt;div class="item" data-v="::placeholder"&gt;
        &lt;span class="checkmark"&gt;✓&lt;/span&gt;
        &lt;span class="label"&gt;::placeholder&lt;/span&gt;
        &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
</code></pre>
<p>And our main CSS for the dropdown would look something like this:</p>
<pre><code>/* position property sets the list dropdown menu to be centered and non-clickable, just below the button that reveals it*/
.cs-menu {
  position: absolute;
  top: calc(100% + 0.4rem);
  left: 0;
  right: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  padding: 0.3rem;
  z-index: 20;
  box-shadow: 0 1.5rem 3rem oklch(0% 0 0 / 0.55);
  opacity: 0;
  transform: translateY(-0.4rem);
  pointer-events: none;
  transition: opacity 0.15s, transform 0.15s;
}

/* this reveals the menu list when the dropdown menu button is clicked if data-open attribute is present. It is hidden otherwise */ 
#cs[data-open] .cs-menu {
  opacity: 1;
  transform: translateY(0);
  pointer-events: all;
}

/* you can use flex here instead of grid. Just follow the pattern stated */
.item {
  display: grid;
  grid-template-columns: 1.2rem 1fr;
  align-items: center;
  gap: 0.65rem;
  padding: 0.6rem 0.75rem;
  border-radius: 0.4rem;
  cursor: pointer;
  transition: background 0.1s;
}
.item:hover {
  background: var(--hover);
}
.item[data-sel] {
  background: oklch(75% 0.18 55 / 0.08);
}

.label {
  font-size: 0.88rem;
  color: var(--muted);
  transition: color 0.12s;
}
.item[data-sel] .label {
  color: var(--accent);
}

/* checkmark over here 🙋‍♂️ */
.checkmark {
  justify-self: end;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--accent);
  visibility: hidden;
}
.item[data-sel] .checkmark {
  visibility: visible;
}

</code></pre>
<p>In essence, we’re using <code>position</code> absolute to remove the element from the normal document flow and hide it from the user. revealing the menu only when the dropdown is clicked. Then, we’re using <code>grid</code> to align and arrange the items in the dropdown menu.</p>
<p>Finally, we style the checkmark to be hidden by default on all list items, and only the selected item should reveal the checkmark, indicating it has been chosen!</p>
<p>Ah yes! That should be all, right?</p>
<p>All this code should make sure the dropdown and checkmark work as intended, fulfilling the requirements stated above. Surely, it works without JavaScript (JS). But, enough sarcastic talk, let’s see the result…</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/GgNqgym">The dilemma of custom select dropdown with checkmarks: no js, a problem</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Notice something not working? Yes, the dropdown is not working. Why? <strong>It’s because there’s no JS to provide the interactivity!</strong></p>
<p>That’s a <em>big</em> issue with solutions like this because JS is required for them to work, and <a href="https://piccalil.li/blog/a-handful-of-reasons-javascript-wont-be-available/">JS <em>will</em> fail for your users</a>. Warnings about best practice aside, let’s provide some interactivity to at least get this sample working.</p>
<pre><code>const cs = document.getElementById("cs");
const btn = document.getElementById("cs-btn");
const lbl = document.getElementById("btn-lbl");
const items = document.querySelectorAll(".item");

btn.addEventListener("click", () =&gt; cs.toggleAttribute("data-open"));

items.forEach((item) =&gt;
  item.addEventListener("click", () =&gt; {
    items.forEach((x) =&gt; x.removeAttribute("data-sel"));
    item.setAttribute("data-sel", "");
    lbl.textContent = item.dataset.v;
    cs.removeAttribute("data-open");
  })
);

document.addEventListener("click", (e) =&gt; {
  if (!cs.contains(e.target)) cs.removeAttribute("data-open");
});

</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/ogYjrZv">Custom dropdown using grid/flex with position property</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>We are missing two important points. What about accessibility and users who want to navigate via keyboard?</p>
<p></p>
<p>We will have to modify the HTML with some <code>aria-haspopup</code> , <code>aria-expanded</code>, <code>aria-selected</code>, <code>role</code>, <code>tab-index</code>, and <code>aria-label</code> . That would sort out our HTML accessibility dilemma. The code would look a lot like this. Setting it up this way from the start, although tedious, would ensure checkmarks in our dropdowns work as intended for <em>all</em> users:</p>
<pre><code>	&lt;div id="cs"&gt;
		&lt;!-- Button to open/close dropdown --&gt;
      &lt;button id="cs-btn" aria-haspopup="listbox" aria-expanded="false"&gt;
        &lt;span id="btn-lbl"&gt;::before&lt;/span&gt;
        &lt;span class="btn-arrow"&gt;▼&lt;/span&gt;
      &lt;/button&gt;
      
      &lt;!-- Menu itself --&gt;
      &lt;div class="cs-menu" 
	      id="cs-menu" 
	      role="listbox" 
	      aria-label="CSS pseudo-element"
      &gt;
        &lt;div class="item" 
	        role="option" 
	        data-sel 
	        aria-selected="true" 
	        data-v=":before"
	        tabindex="-1"
	        &gt;
          &lt;span class="checkmark"&gt;✓&lt;/span&gt;
          &lt;span class="label"&gt;::before&lt;/span&gt;
        &lt;/div&gt;
        
        &lt;div class="item" 
	        role="option" 
	        aria-selected="false" 
	        data-v="::after"
	        tabindex="-1"
	        &gt;
          &lt;span class="checkmark"&gt;✓&lt;/span&gt;
          &lt;span class="label"&gt;::after&lt;/span&gt;
        &lt;/div&gt;
        
        &lt;div class="item" 
	        role="option" 
		      aria-selected="false" 
		      data-v="::marker"
		      tabindex="-1"
		      &gt;
          &lt;span class="checkmark"&gt;✓&lt;/span&gt;
          &lt;span class="label"&gt;::marker&lt;/span&gt;
        &lt;/div&gt;
        
        &lt;div class="item" 
	        role="option" 
	        aria-selected="false" 
	        data-v="::selection"
	        tabindex="-1"
	        &gt;
          &lt;span class="checkmark"&gt;✓&lt;/span&gt;
          &lt;span class="label"&gt;::selection&lt;/span&gt;
        &lt;/div&gt;
        
        &lt;div class="item" 
	        role="option" 
	        aria-selected="false" 
	        data-v="::placeholder"
	        tabindex="-1"
	        &gt;
          &lt;span class="checkmark"&gt;✓&lt;/span&gt;
          &lt;span class="label"&gt;::placeholder&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
</code></pre>
<p>Then, for our JavaScript, that will be modified a bit from what you saw earlier. We would have to account for when the user uses the directional arrows <code>up</code> and <code>down</code>, and when they click <code>enter</code> and <code>esc</code> to navigate the dropdown and click on any option.</p>
<pre><code>const cs = document.getElementById("cs");
const btn = document.getElementById("cs-btn");
const lbl = document.getElementById("btn-lbl");
const items = [...document.querySelectorAll(".item")];

let selected = items.find((i) =&gt; i.hasAttribute("data-sel"));

function open() {
  cs.setAttribute("data-open", "");
  btn.setAttribute("aria-expanded", "true");
  (selected ?? items[0]).focus();
}

function close() {
  cs.removeAttribute("data-open");
  btn.setAttribute("aria-expanded", "false");
  btn.focus();
}

function select(item) {
  selected?.removeAttribute("data-sel");
  selected?.setAttribute("aria-selected", "false");
  item.setAttribute("data-sel", "");
  item.setAttribute("aria-selected", "true");
  selected = item;
  lbl.textContent = item.dataset.v;
}

btn.addEventListener("click", () =&gt;
  cs.hasAttribute("data-open") ? close() : open()
);

btn.addEventListener("keydown", (e) =&gt; {
  if (e.key === "ArrowDown") {
    e.preventDefault();
    open();
  }
});

/* accounting for keyboard up, down, enter and esc for the dropdown */
items.forEach((item) =&gt; {
  item.addEventListener("click", () =&gt; {
    select(item);
    close();
  });
  item.addEventListener("keydown", (e) =&gt; {
    const i = items.indexOf(item);
    if (e.key === "ArrowDown") {
      e.preventDefault();
      items[(i + 1) % items.length].focus();
    }
    if (e.key === "ArrowUp") {
      e.preventDefault();
      items[(i - 1 + items.length) % items.length].focus();
    }
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      select(item);
      close();
    }
    if (e.key === "Escape") close();
  });
});

document.addEventListener("click", (e) =&gt; {
  if (!cs.contains(e.target)) close();
});

</code></pre>
<p>That’s <em>a lot</em> of JavaScript! And essentially, what I’m doing is accounting for when a user clicks the <code>down</code> arrow button on their keyboard, which can be used to expand the dropdown and also browse downwards through the options. The up arrow key inside the dropdown can be used to navigate up, enter selects the item, and esc leaves the dropdown.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/NPbdVeY">custom dropdown using grid/flex with position property
</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Let’s also not forget the accessibility toggles that let the screen reader know that the dropdown has been expanded or that an item in the dropdown list has been selected.</p>
<p>This approach is how many developers have tackled the problem at hand before and here two key reasons why it is not the best solution:</p>
<ol>
<li>When JS fails, the whole solution is <strong>dead.</strong> Not even a glimmer of hope is here. No JavaScript means no interaction and the markup we added likely ends up creating even more problems for people using assistive technology</li>
<li>Every single browser behavior has to be unnecessarily implemented from scratch. From option focusing, to accounting for what happens when the user clicks the button to expand and close the dropdown and also clicks outside of the dropdown to close it, has to be implemented</li>
</ol>
<p>In essence, this solution is fragile and unreliable due to its over-reliance on JS. What’s more? Our checkmark styles are nonexistent if the dropdown cannot be revealed!</p>
<p>So, how do we solve these issues? Luckily, there’s a far less “heavy code” intensive solution that allows us to do style checkmarks anyhow we like.</p>
<p></p>
<h2>Modern day <code>::checkmark</code> styling</h2>
<p>The CSS <code>::checkmark</code> is a pseudo-element that allows us to apply styles to HTML elements that support a checkmark. It really helps remove the hassle of writing a bunch of HTML, CSS, and JS in order to make things work as they’re supposed to. Let’s take a custom dropdown and a checkmark designed using <code>::checkmark</code>.</p>
<p>Take for example, this <code>&lt;select&gt;</code>dropdown:</p>
<pre><code>&lt;select id="ls"&gt;
	&lt;option value="css"&gt;CSS&lt;/option&gt;
	&lt;option value="js"&gt;JavaScript&lt;/option&gt;
	&lt;option value="html"&gt;HTML&lt;/option&gt;
  &lt;option value="ts"&gt;TypeScript&lt;/option&gt;
  &lt;option value="rust"&gt;Rust&lt;/option&gt;
&lt;/select&gt;
</code></pre>
<p>The HTML is pretty concise when compared to the previous solution, right? It’s amazing what happens when we use semantic elements. This way, the browser handles everything for us. Accessibility, keyboard navigation, and every standard behavior we expect from our dropdown, without any hassle. Just define a dropdown using <code>&lt;select&gt;</code> with its <code>&lt;option&gt;</code>s and boom! We’re good to go.</p>
<p>Checkmarks even come out of the box with this native browser solution!</p>
<p>For our CSS, we would have something like this:</p>
<pre><code>/* required to make the checkmark stylable */ 
#ls,
::picker(select) {
  appearance: base-select;
}

#ls {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  padding: 0.7rem 1rem;
}

#ls option:checked {
  color: var(--accent);
  background: oklch(78% 0.22 145 / 0.1);
}

/* checkmark style here 🙋‍♂️*/
::checkmark {
  content: "🎨";
}

/* a dot checkmark for options not checked */
#ls option:not(:checked)::checkmark {
  visibility: visible;
  content: "·";
  color: var(--muted);
}
</code></pre>
<p>All you need to do is set <code>appearance</code> to <code>base-select</code> on the <code>&lt;select&gt;</code> dropdown and you’re set for styling your checkmarks using <code>::checkmark</code> and changing the default check using <code>content</code>!</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/Pwbzqqa">::checkmark demo (no JS required!)</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>No JS required!</p>
<p>A win for <a href="https://piccalil.li/blog/its-about-time-i-tried-to-explain-what-progressive-enhancement-actually-is/">progressive enhancement</a> because it essentially accounts for all the major issues associated with creating a custom dropdown and having a custom checkmark style. This solution, even when JS fails, will still work out fine.</p>
<p>This approach is clearly better than the previous approach, but there are a few issues with this that I think we should touch on.</p>
<h2>The problem with <code>::checkmark</code></h2>
<p>It’s amazing what this pseudo-element can do and what <em>it is intended to do</em>. It’s also amazing the amount of code it saves. Not only do I need to write less code to achieve the same result (efficiency), but the browser also handles the default behavior I expect when I click outside the dropdown (the dropdown closes automatically).</p>
<p>There are a few issues with <code>::checkmark</code> that I want to address though.</p>
<ol>
<li>There’s limited browser support. At the time of writing, <a href="https://caniuse.com/mdn-css_selectors_checkmark"><code>::checkmark</code> is only supported in 3 major browsers</a> and not across the board. That would suck for a user having to experience a feature in one browser and be absent on the next.</li>
<li>Lacking full feature support: According to the documentation, <a href="https://drafts.csswg.org/css-forms-1/#checkmark"><code>::checkmark</code> is supposed to work for checkboxes, radios, and</a> options. Guess what? In browsers that currently support this, <strong>only the options from <code>&lt;select&gt;</code> are supported.</strong>  So even if I’m using this pseudo-element, I’m not getting its full feature!</li>
</ol>
<p>Both need to be looked into by browser vendors, in my opinion.</p>
<h2>Wrapping up</h2>
<p>The modern day <code>::checkmark</code> solution with native browser dropdown allows us to favor all users without having to implement our own accessibility, keyboard navigation, and dropdown behavior. Even when JavaScript fails, the solution still works! Because we're using a native <code>&lt;select&gt;</code> element, we get the correct announcements from assistive technology too.</p>
<p>My only plea is that for our checkmarks to be styled correctly, major browser vendors may need to provide full support for this feature as soon as possible, so we can style our own custom checkmarks without having to rely on JS at all, ever again.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Building dynamic toggletips using anchored container queries</title>
        <link>https://piccalil.li/blog/building-dynamic-toggletips-using-anchored-container-queries/?ref=css-category-rss-feed</link>
        <dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
        <pubDate>Thu, 12 Mar 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/building-dynamic-toggletips-using-anchored-container-queries/?ref=css-category-rss-feed</guid>
        <description><![CDATA[<p>To add to the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries">container query options that we already have</a> (size queries, style queries, scroll-state queries), as well as the container query range syntax and many upgrades to all of the above, we can now use <em>anchored</em> container queries from Chrome 143.</p>
<p><a href="https://developer.chrome.com/blog/anchored-container-queries">Chrome’s announcement post</a> demonstrates how, if we were using anchor positioning, we could use anchored container queries to query the currently active fallback position (if any). They’ve provided a little demo in the post where a toggletip caret is anchored to the side of a toggletip, but if the toggletip needs to be anchored to a different side of the trigger due to lack of space, the caret is anchored to a different side as well. So in this case, anchored container queries are used to determine which position the toggletip is in, so that we can position the caret accordingly.</p>
<p>This is a pretty good use-case for anchored container queries, but also a great opportunity to look at how we might build toggletips in the (hopefully) near future. You’ll learn about popovers and anchor positioning, which are already baseline and will ensure that the toggletips at least work, as well as declarative anchors, ‘modern’ <code>attr()</code>, and <code>corner-shape</code>, progressive enhancements en<em>chant</em>ments that provide a range of magical benefits.</p>
<p>Here’s what we’ll be making:</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/gbrMGYx">Anchored container queries demo</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>In Chrome you’ll see toggletip carets that flip to the appropriate side depending on the amount of space available, but in other web browsers you’ll get the toggletips without carets.</p>
<p>Let’s get into it!</p>
<h2>Setting up the popover and anchor associations</h2>
<p>We’ll be using popovers to mark up the toggletips, as that’s the semantic way to do so these days, but before we begin it’s worth mentioning that they’re implicitly anchored. However, since you might want to use anchored container queries without popovers, I’ll show you how to set up anchor associations anyway.</p>
<p>To set up the popover and anchor associations in a declarative but progressively enhanced way, we’ll need the following HTML markup:</p>
<pre><code>&lt;div anchor="leftButton" id="--leftPopover" popover&gt;Toggletip&lt;/div&gt;
&lt;div anchor="centerButton" id="--centerPopover" popover&gt;Toggletip&lt;/div&gt;
&lt;div anchor="rightButton" id="--rightPopover" popover&gt;Toggletip&lt;/div&gt;

&lt;button id="leftButton" popovertarget="--leftPopover"&gt;Button&lt;/button&gt;
&lt;button id="centerButton" popovertarget="--centerPopover"&gt;Button&lt;/button&gt;
&lt;button id="rightButton" popovertarget="--rightPopover"&gt;Button&lt;/button&gt;
</code></pre>
<p>The most familiar part of this is probably the popover markup. We’ve given the toggletips the <code>popover</code> attribute, and then an <code>id</code> so that each trigger can reference a popover using the <code>popovertarget</code> attribute. The strange part is that we’ve used custom-ident values (e.g., <code>--leftPopover</code>), but I’ll explain why shortly. That’s the popover functionality set up.</p>
<p>For the anchor positioning we’ve also given <code>id</code>s to the triggers, and then referenced them from <code>anchor</code> attributes set on the toggletips. These values <em>don’t</em> have to be custom idents.</p>
<p>However, for web browsers that don’t support the <code>anchor</code> attribute, we’ll need to use CSS instead, and there’s a shorter and a longer way to do that depending on whether or not modern <code>attr()</code> is supported.</p>
<p>The shorter, modern <code>attr()</code> way:</p>
<pre><code>/* Modern attr() supported */
@supports (x: attr(x type(*))) {
  button {
    /* Reuse the name from popovertarget */
    anchor-name: attr(popovertarget type(&lt;custom-ident&gt;));
  }

  [popover] {
    /* Anchor to the relevant button */
    position-anchor: attr(id type(&lt;custom-ident&gt;));
  }
}
</code></pre>
<p>Basically, if modern <code>attr()</code> is supported, we reuse the <code>popovertarget</code> values of the buttons as anchor names while making it clear that we want to parse them as custom idents. After that, we match the <code>id</code> values of the popovers to said anchors, thus setting up the anchor positioning associations. The <code>anchor-name</code> and <code>position-anchor</code> CSS properties only accept custom idents (e.g. <code>--leftPopover</code>), so that’s why we're making a big fuss about them.</p>
<p>Next, if modern <code>attr()</code> <em>isn’t</em> supported, name each anchor manually:</p>
<pre><code>/* Modern attr() not supported */
@supports not (x: attr(x type(*))) {
  /* Assign anchor name */
  #leftButton {
    anchor-name: --leftButton;
  }

  /* Anchor to button */
  #leftPopover {
    position-anchor: --leftButton;
  }

  /* And so on... */
  #centerButton {
    anchor-name: --centerButton;
  }

  #centerPopover {
    position-anchor: --centerButton;
  }

  /* And so on... */
  #rightButton {
    anchor-name: --rightButton;
  }

  #rightPopover {
    position-anchor: --rightButton;
  }
}
</code></pre>
<p>Once modern <code>attr()</code> is <em>Baseline widely available</em>, we can delete this block, and then once the <code>anchor</code> attribute is <em>Baseline widely available</em>, we can delete <em>all</em> of this CSS. You won’t even need to use custom-ident values, so <code>--leftPopover</code> can become <code>leftPopover</code>, for example.</p>
<p>Note: if we’d rather that the toggletips be triggered on <code>:hover</code> (making them tooltips instead of toggletips), we can swap <code>popovertarget</code> for <code>interestfor</code>, but note that <a href="https://css-tricks.com/a-first-look-at-the-interest-invoker-api-for-hover-triggered-popovers/">interest invokers</a> are only supported in Chrome 142 and above. Keep them in mind for the future!</p>
<p></p>
<h2>Aligning and styling the toggletips</h2>
<p>Like last time, I’ll drop the code right here so you can skim through it before we look at it line-by-line:</p>
<pre><code>[popover] {
  position: fixed;

  /* Align to right/implied center */
  position-area: right;

  /* If no space, flip to other inline side */
  position-try: flip-inline;

  /*
    1rem of spacing between the button and
    toggletip that also flips side accordingly
  */
  margin-inline-start: 1rem;

  /* Anchor toggletip caret to this */
  anchor-name: --toggletip;

  /* Used to query this container’s fallbacks */
  container-type: anchored;

  /* Scooped corners, if supported */
  @supports (corner-shape: squircle) {
    border-radius: 3rem;
    corner-shape: squircle;
  }

  /* Normal corners, if not supported */
  @supports not (corner-shape: squircle) {
    border-radius: 1rem;
  }
}
</code></pre>
<p>First, we must declare <code>position: fixed</code> on the anchored element (in this case though, popovers already have it).</p>
<p>Next, we need to position the toggletip relative to the button (let’s say the center-right) using the <code>position-area</code> property. Now, you might think that <code>position-area: center right</code> is the correct declaration here, but here’s what that actually does:</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/anchor-toggle-tips-wrong.png" alt="An Inset-Modified Containing Block where an anchored toggletip is overflowing the center-right tile." /></p>
<p>As you can see, it’s overflowing the center-right tile, which is wrong but forgiving, since web browsers declare <code>align-self: anchor-center</code> under the hood to make it work anyway. This is declared on what’s called the Inset-Modified Containing Block (IMCB), which again is the center-right tile.</p>
<p>However, <a href="https://nerdy.dev/why-isnt-my-position-try-fallback-working-in-small-spaces">for reasons unknown, web browsers aren’t so forgiving when a fallback position is activated</a>. The trick is to use <code>position-area: right</code> instead. By omiting the <code>center</code> keyword, it resolves to <code>position-area: span-all right</code>, which does this:</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/anchor-toggle-tips-right.png" alt="An Inset-Modified Containing Block where an anchored toggletip is contained within the right-side tiles and then vertically centered." /></p>
<p>Now the toggletip <em>does</em> fit into the tile (IMCB). And again, it’s vertically aligned within the center of it due to <code>align-self: anchor-center</code>.</p>
<p>We can safely use <code>position-try: flip-inline</code>, which turns <code>right</code> into <code>left</code> whenever the toggletip overflows the viewport. Later on, anchored container queries will query whether this is happening or not.</p>
<p>After that, <code>margin-inline-start: 1rem</code> adds <code>1rem</code> of spacing between the button and the toggletip at the start of the inline axis, which also flips accordingly.</p>
<p></p>
<p><code>anchor-name: --toggletip</code> turns the popovers into anchors so that we can create toggletip carets and anchor them to said popovers. Normally this’d be a naming collision waiting to happen (because three different popovers are now called <code>--toggletip</code>, right?), but the toggletip carets will be nested inside the popovers, so they’re somewhat scoped.</p>
<p>And remember, we’ll be flipping the toggletip’s caret to the opposite side whenever the toggletip is flipped, and that’s what <code>container-type: anchored</code> is for — it’s a totally new type of container and we’ll be querying it’s fallback position to determine the correct side.</p>
<p>One more thing, though…</p>
<p>If <code>corner-shape: squircle</code> is supported (<code>@supports (corner-shape: squircle)</code>), then we use it with <code>border-radius: 3rem</code> to create corners that aren’t quite rounded but aren’t quite square either. If it’s <em>not</em> supported then we just use <code>border-radius: 1rem</code>. This is optional of course, but <code>corner-shape</code> will play a bigger role when we use it again in a moment:</p>
<pre><code>/* Scooped corners, if supported */
@supports (corner-shape: squircle) {
	border-radius: 3rem;
	corner-shape: squircle;
}

/* Normal corners, if not supported */
@supports not (corner-shape: squircle) {
	border-radius: 1rem;
}
</code></pre>
<h2>Creating and positioning the toggletip’s caret</h2>
<p>Similar to how we anchored the toggletips to the buttons, the following code anchors the toggletip carets to the toggletips:</p>
<pre><code>[popover] {
	/* Previous section code */

  /* Toggletip caret */
  &amp;::after {
    /*
      Only create carets if anchored
      container queries are supported
    */
    @supports (container-type: anchored) {
      /* Create caret */
      content: "";

      /* Anchor to toggletip */
      position: fixed;
      position-anchor: --toggletip;

      /* Scooped carets, if supported */
      @supports (corner-shape: scoop) {
        height: 1rem;
        aspect-ratio: 1/2;
        corner-shape: scoop;
        background: inherit;
      }

      /* Normal carets, if not supported */
      @supports not (corner-shape: scoop) {
        /* Hack to create a triangle */
        width: 0;
        height: 0;
        border-top: 0.5rem solid transparent;
        border-bottom: 0.5rem solid transparent;
      }

      /* If no fallback */
      @container anchored(fallback: none) {
        /* Position caret on the left */
        position-area: left;

        /* Needed for scooped carets */
        @supports (corner-shape: scoop) {
          border-top-left-radius: 100% 50%;
          border-bottom-left-radius: 100% 50%;
        }

        /* Needed for normal carets */
        @supports not (corner-shape: scoop) {
          /* Part of the triangle hack */
          border-right: 0.5rem solid var(--toggletip-color);
        }
      }

      /* If flip-inline fallback triggered */
      @container anchored(fallback: flip-inline) {
        position-area: right;

        @supports (corner-shape: scoop) {
          border-top-right-radius: 100% 50%;
          border-bottom-right-radius: 100% 50%;
        }

        @supports not (corner-shape: scoop) {
          border-left: 0.5rem solid var(--toggletip-color);
        }
      }
    }
  }
}
</code></pre>
<p>First of all, the toggletip carets are CSS-generated using the <code>::after</code> pseudo-element:</p>
<pre><code>[popover] {
  &amp;::after {
    /* Toggletip caret */
	}
}
</code></pre>
<p>Then, within that, we need to see if the web browser supports anchored container queries (if not, the toggletip carets simply aren’t generated, although the toggletips will still work):</p>
<pre><code>[popover] {
  &amp;::after {
		@supports (container-type: anchored) {
	    /*
	      Only create carets if anchored
	      container queries are supported
	    */
		}
	}
}
</code></pre>
<p>Within that, <code>content: ""</code> generates the toggletip carets (or, rather, it generates a pseudo-element with no content that we’ll style <em>into</em> carets).</p>
<p><code>position: fixed</code> and <code>position-anchor: --toggletip</code> anchors them to the toggletips.</p>
<p>After that, we again leverage <code>corner-shape</code> to create scooped toggletips. Or, if <code>corner-shape</code> isn’t supported, then we use the classic <a href="https://css-tricks.com/snippets/css/css-triangle/">border trick to create triangle-shaped carets</a>.</p>
<p>We also — finally — implement those anchored container queries to position the carets to the correct side of the toggletips. When a fallback isn’t active, <code>@container anchored(fallback: none)</code> will match, but when a fallback <em>is</em> active, <code>@container anchored(fallback: flip-inline)</code> will match. The value of <code>fallback</code> simply needs to match a value of <code>position-try</code>:</p>
<p><code>position-try: flip-inline</code>, in our case</p>
<pre><code>/* Scooped carets, if supported */
@supports (corner-shape: scoop) {
	height: 1rem;
	aspect-ratio: 1/2;
	corner-shape: scoop; /* (Border radius is set later) */
	background: inherit;
}

/* Normal carets, if not */
@supports not (corner-shape: scoop) {
	/* Hack to create a triangle */
	width: 0;
	height: 0;
	border-block: 0.5rem solid transparent; /* (Side is set later) */
}

/* If no fallback */
@container anchored(fallback: none) {
	/* Position caret on the left */
	position-area: left;

	/* Left border radii for scooped carets */
	@supports (corner-shape: scoop) {
		border-top-left-radius: 100% 50%;
		border-bottom-left-radius: 100% 50%;
	}

	/* ‘Left’ border for normal carets */
	@supports not (corner-shape: scoop) {
		border-right: 0.5rem solid var(--toggletip-color);
	}
}

/* If flip-inline fallback triggered */
@container anchored(fallback: flip-inline) {
	position-area: right;

	/* Right border radii for scooped carets */
	@supports (corner-shape: scoop) {
		border-top-right-radius: 100% 50%;
		border-bottom-right-radius: 100% 50%;
	}

	/* ‘Right’ border for normal carets */
	@supports not (corner-shape: scoop) {
		border-left: 0.5rem solid var(--toggletip-color);
	}
}
</code></pre>
<p>As a reminder, here’s the final demo:</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/gbrMGYx">Anchored container queries demo</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<h2>Wrapping up</h2>
<p>Container queries are evolving beautifully, especially with the newest addition, <em>anchored</em> container queries. And, thanks to other modern CSS features such as the <code>anchor</code> attribute, ‘modern’ <code>attr()</code>, <code>corner-shape</code>, and even interest invokers, we can include all kinds of progressive enhancements to create some really awesome — in this case — toggletips.</p>
<p>If you want to go ahead and implement this (full code in <a href="https://codepen.io/mrdanielschwarz/pen/gbrMGYx">the demo</a>), you totally can. If something isn’t supported it’ll simply revert to something that is, and then as new features become baseline, you can very easily remove the code that’s no longer needed.</p>
        
        ]]></description>
        
      </item>
    
    </channel>
  </rss>
