<?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 - Front-End Challenges Club topic archive</title>
      <link>https://piccalil.li/</link>
      <atom:link href="https://piccalil.li/category/front-end-challenges-club.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 - Front-End Challenges Club topic archive 2026</copyright>
      <docs>https://www.rssboard.org/rss-specification</docs>
      <pubDate>Mon, 08 Jun 2026 23:11:32 GMT</pubDate>
      <lastBuildDate>Mon, 08 Jun 2026 23:11:32 GMT</lastBuildDate>

      
      <item>
        <title>Front-End solution: Eyebrow heading dots</title>
        <link>https://piccalil.li/blog/front-end-solution-eyebrow-heading-dots/?ref=front-end-challenges-club-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Wed, 10 Jul 2024 12:20:49 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/front-end-solution-eyebrow-heading-dots/?ref=front-end-challenges-club-category-rss-feed</guid>
        <description><![CDATA[<p>In <a href="https://piccalil.li/blog/front-end-challenge-eyebrow-heading-dots">the challenge</a> I was stuck with a suboptimal solution to little decorative dots. If you haven’t already read that, do that first. I’ll wait.</p>
<p>Right, you’re up to speed, let’s dig in.</p>
<h2>A call to arms</h2>
<p>I was really impressed with the efforts folks contributed to this challenge, so I’ll list a couple of my favourites.</p>
<ul>
<li><a href="https://codepen.io/kevinpowell/pres/YzbRmEJ">Kevin had a good effort using grid and <code>fit-content</code></a></li>
<li><a href="https://jsbin.com/gulehulilu/edit?css,output">Ryan had a go with an alternative grid approach</a></li>
<li><a href="https://codepen.io/garybyrne1/pen/NWVzOWL/988ba80693d87566793afb5c91fbdecc">Gary tried getting absolute position to work out</a></li>
</ul>
<p>Thank you, <em>as always</em>, to the folks who contributed.</p>
<h2>The secret is in the newest CSS</h2>
<p><a href="https://front-end.social/@kizu/112678869220207364">Roma</a> linked me up with an <a href="https://kizu.dev/shrinkwrap-problem/">article about the shrink-wrap problem</a> they wrote. The solution was right there, <a href="https://kizu.dev/shrinkwrap-problem/#even-more-heading-decorations">in these further examples</a>.</p>
<p>It’s a really clever solution because the anchor positioning system allows us to stick the start or the end of the dots to each side of the text, providing that much sought-after effect that I was after in the original challenge.</p>
<p>I’ve not read up much anchor position stuff quite yet because in my mind, it was a popover thing. How wrong was I?!</p>
<h2>I riffed off Roma’s approach for the solution to this Front-End Challenges Club</h2>
<p>We’re using slightly different HTML this time. I wanted no extra elements, but that seems like an impossibility at this point. That’s cool because you gotta let go of these things to progress. It’s better to be flexible when working with the web.</p>
<pre><code>&lt;h2 class="eyebrow"&gt;
  &lt;span&gt;Some reasonably long text goes here&lt;/span&gt;
&lt;/h2&gt;
</code></pre>
<p>One less <code>&lt;span&gt;</code> at least and most importantly, no empty elements, which I did have in my last attempt in the challenge.</p>
<p>Let’s now move on to CSS and start on the <code>.eyebrow</code> block.</p>
<pre><code>.eyebrow {
  --eyebrow-decoration-gutter: 1ch;

  font-size: var(--size-step-0);
  font-weight: 500;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
  font-feature-settings: "cpsp" on;
  max-width: unset;

  /* This is the relative parent for the dots */
  position: relative;
}
</code></pre>
<p>We start by determining what the gap between dots and text should be. I’ve opted for a nice <code>1ch</code> but it can be whatever you like.</p>
<p>Skipping past the visual typography settings, we set the <code>position</code> as <code>relative</code>. The dots will use this as their relative parent where anchor support is available later.</p>
<pre><code>.eyebrow &gt; span {
  position: relative;
  display: inline-block;
}
</code></pre>
<p>To handle browsers with no anchor support, we are building with <a href="https://piccalil.li/blog/its-about-time-i-tried-to-explain-what-progressive-enhancement-actually-is">progressive enhancement</a>. By setting the <code>&lt;span&gt;</code> as a relative parent, we can fake anchoring with absolute positioning later.</p>
<pre><code>.eyebrow &gt; span::before,
.eyebrow &gt; span::after {
  content: "";
  display: block;
  aspect-ratio: 1;
  height: 0.7ex;
  background-color: currentColor;
  border-radius: 100%;
  position: absolute;

  /* Attach to the anchor */
  position-anchor: --eyebrow-target;
  
  inset-block-start: 50%;
  transform: translateY(-50%);
}
</code></pre>
<p>This is very similar to my original shot at this pattern. We’re creating dots using aspect ratio and border radius, making them the same colour as the rendered text.</p>
<p>The next part, we create an anchor for our dots. We’re not doing this in a <code>@supports</code> because there’s no point. <a href="https://piccalil.li/blog/css-inheritance/">The browser will ignore and carry on</a> because CSS is a declarative programming language.</p>
<p>The last bit I’m not overly enthused about. I wish <code>translate</code> had some logical versions because this pattern would work regardless of writing mode. A refactoring target, I guess.</p>
<pre><code>.eyebrow &gt; span::before {
  inset-inline-start: calc(var(--eyebrow-decoration-gutter) * -2);
}

.eyebrow &gt; span::after {
  inset-inline-end: calc(var(--eyebrow-decoration-gutter) * -2);
}
</code></pre>
<p>Again, for browsers with no anchor support, we’re using only absolute positioning to position our anchors. There’s not enough space with the <code>--eyebrow-decoration-gutter</code>, so I’ve multiplied it by minus two to get a double negative version of our custom property. I can live with that, but you might instead prefer to create an <code>--eyebrow-fallback-decoration-gutter</code> or something instead.</p>
<pre><code>@supports (position-anchor: --eyebrow-target) {
  /* Not a relative parent, but an anchoring parent instead */
  .eyebrow &gt; span {
    anchor-name: --eyebrow-target;

    /* Undo the default experience */
    display: revert;
    position: revert;
  }

  /* Anchor the end of the dot to the anchor. Margin inline provides the gutter */
  .eyebrow &gt; span::before {
    inset-inline-start: auto;
    inset-inline-end: anchor(start);
    margin-inline-end: var(--eyebrow-decoration-gutter);
  }

  /* Anchor the start of the dot to the anchor. Margin inline provides the gutter */
  .eyebrow &gt; span::after {
    inset-inline-end: auto;
    inset-inline-start: anchor(end);
    margin-inline-start: var(--eyebrow-decoration-gutter);
  }
}
</code></pre>
<p>This is where the fun starts. We first check to see if anchor positioning is available to us. A quick note on <code>@supports</code>: I like to test the actual value we are trying to apply. Feels like a good idea and improves readability as I see it.</p>
<p>The next thing we do is target the span and attach it to the anchor we created earlier with <code>anchor-name: --eyebrow-target;</code>. Following this, we <a href="https://piccalil.li/blog/css-inheritance/#revert">revert the positioning and display rules to the user agent stylesheet value</a>.</p>
<p><em>Finally</em>, because we have anchor support, we can now utilise it. For the <code>::before</code> element, we change the positioning values to <strong>stitch the end</strong> of the dot to the <strong>start of the anchor</strong>.  We do the opposite for the <code>::after</code> element and <strong>stitch the start</strong> to the <strong>end of the anchor</strong>. We re-apply the gutter value with <code>margin</code> too, as it was set with positioning before.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/KKLOEod/10d3eaf9aaaa42e921c96b5edbf42eac">My final solution to this challenge</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>It’s been a bit of a different challenge this time around on Front-End Challenges Club. I hope you’ve enjoyed it! Next time, I <em>promise</em> it’ll be a simpler one.</p>
<p>Again, thank you to everyone who had a go at it!</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Front-End challenge: Eyebrow heading dots</title>
        <link>https://piccalil.li/blog/front-end-challenge-eyebrow-heading-dots/?ref=front-end-challenges-club-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Thu, 27 Jun 2024 11:19:18 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/front-end-challenge-eyebrow-heading-dots/?ref=front-end-challenges-club-category-rss-feed</guid>
        <description><![CDATA[<p>I’m going to do this thing a little differently and start with <em>my solution</em> and ask for folks to come up with a better solution.</p>
<p>The context is a little “eyebrow” heading, but the complexity is there are decorative dots each side. I’m close — with the help of magic numbers — but I want something extremely scalable and intrinsic, ideally.</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/fecc/010-ideal-state.jpg" alt="Two figma artboards: one @min (narrow) and one @max (wide). There are two eyebrows per artboard. One is short and the other is medium to show text wrapping on to two lines" title="&lt;a href='/images/blog/fecc/010-ideal-state.jpg'&gt;View full size image&lt;/a&gt;" /></p>
<p>When the content breaks on to two lines, I want those dots to sit right up to edges of the text with a nice gap to cushion that.</p>
<h2>Starting with a pretty standard grid layout</h2>
<p>My first idea was to allow the content to naturally wrap, only specifying the size of the decorative columns as <code>1fr</code> each. It works great on large viewports, but on small viewports, those dots were pushed out to the edges.</p>
<pre><code>.eyebrow {
  font-size: var(--size-step-0);
  font-weight: 500;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
  max-width: unset; 
  font-feature-settings: "cpsp" on;
  display: grid;
  align-items: center;
  grid-template-columns: 1fr auto 1fr;
  gap: 0 var(--space-s);
}

.eyebrow span::before {
  content: "";
  display: block;
  aspect-ratio: 1;
  height: 0.7ex;
  background-color: currentColor;
  border-radius: 100%;
}

.eyebrow span:first-child::before {
  margin-inline-start: auto;
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/ZENrVaG/5b2e3f7d2bab544df9c4d5178ad8acc5">Eyebrow: standard grid</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>As far as I can tell, using <code>min-content</code>, <code>max-content</code> and <code>fit-content</code> to size the content isn’t going to do the job because where space allows the text needs to flow nicely on a single line. But, <code>fit-content</code> with some control in place works <em>ok</em>.</p>
<pre><code>.eyebrow {
  font-size: var(--size-step-0);
  font-weight: 500;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
  max-width: unset;
  font-feature-settings: "cpsp" on;
  display: grid;
  align-items: center;
  grid-template-columns: 1fr fit-content(30ch) 1fr;
  gap: 0 var(--space-s);
}

.eyebrow span::before {
  content: "";
  display: block;
  aspect-ratio: 1;
  height: 0.7ex;
  background-color: currentColor;
  border-radius: 100%;
}

.eyebrow span:first-child::before {
  margin-inline-start: auto;
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/XWwZoVd/0664d358f8844941204adfa7645ce556">Eyebrow: fit content</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<h2>Where I’ve ended</h2>
<p>I’m using <code>minmax</code> to size the decorative columns based on either a viewport width or <code>1fr</code>. This allows the decorative columns to nicely fill space where there is space to fill, but on small screens, push everything in enough to give the effect we’re after.</p>
<pre><code>.eyebrow {
  font-size: var(--size-step-0);
  font-weight: 500;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
  max-width: unset; 
  font-feature-settings: "cpsp" on;
  display: grid;
  align-items: center;
  grid-template-columns: minmax(13vi, 1fr) auto minmax(13vi, 1fr);
  gap: 0 var(--space-s);
}

.eyebrow span::before {
  content: "";
  display: block;
  aspect-ratio: 1;
  height: 0.7ex;
  background-color: currentColor;
  border-radius: 100%;
}

.eyebrow span:first-child::before {
  margin-inline-start: auto;
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/RwmQEGj">Eyebrow: my approach</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<h2>What I’d love to achieve</h2>
<ol>
<li>No magic numbers</li>
<li>No extra elements in the heading</li>
</ol>
<p>I’m <em>sure</em> it’s possible to do the above and based on previous editions of <a href="https://piccalil.li/category/front-end-challenges-club">Front-End Challenges Club</a>, there’s some seriously smart people giving them a go.</p>
<p>Fancy your chances with this little challenge? Go ahead and <a href="https://codepen.io/piccalilli/pen/RwmQEGj">fork this CodePen</a> (or grab the code and do what you want) and let us know how you did it! You can <a href="https://andy-bell.co.uk/links/">catch me at the following locations too</a>.</p>
<p>For the solution to this challenge, I’ll pick my favourite attempt, apply it in the actual context and show you all how it does, while hopefully breaking it down into something easy for you all to understand.</p>
<p>A closing note: it doesn’t have to use <code>grid</code> either. That’s just the route I chose 🙂</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Front-End solution: progress indicator</title>
        <link>https://piccalil.li/blog/solution-009-progress-indicator/?ref=front-end-challenges-club-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Wed, 13 Mar 2024 08:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/solution-009-progress-indicator/?ref=front-end-challenges-club-category-rss-feed</guid>
        <description><![CDATA[<p>This is the solution to <a href="https://piccalil.li/blog/challenge-009-progress-indicator/">Front-End Challenge #009</a>, so check that out first if you haven’t already.</p>
<p>I knew when I set this challenge, there were multiple ways to solve it and I was <em>not</em> disappointed by the diversity in approaches that folks kindly sent to me.</p>
<p>Here’s how I did it.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/mdogpWK">The finished 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>Starting with markup</h2>
<p>I’ve opted for a little web component that progressively enhances a loading statement, so most of the markup lives in that component. But, it’s important that the default experience is acceptable and informative.</p>
<pre><code>&lt;progress-indicator progress="0" stroke="8" viewbox="130"&gt;
  &lt;div role="alert" aria-live="polite"&gt;
    &lt;p&gt;Loading, please wait…&lt;/p&gt;
  &lt;/div&gt;
&lt;/progress-indicator&gt;
</code></pre>
<p>We’re calling on a <code>&lt;progress-indicator&gt;</code> custom element and inside that, there’s some default markup. It’s a statement that lets the user know that something is loading. Using the <code>role="alert"</code> attribute along with <code>aria-live="polite"</code> gives a screen reader the right message too without interrupting their flow.</p>
<h2>The web component</h2>
<p>The first thing to do is create the component and add it to the registered custom elements:</p>
<pre><code>class ProgressIndicator extends HTMLElement {
}

customElements.define("progress-indicator", ProgressIndicator);
</code></pre>
<p>What’s going on here is we’re creating a class that extends the base <code>HTMLElement</code> object and then assigning that to any <code>&lt;progress-indicator&gt;</code> element that is found on a page.</p>
<div><h2>FYI</h2>
<p>Everything I add in this section is going to be a method of the <code>ProgressIndicator</code> class, so if you’re coding along, keep that in mind!</p>
</div>
<p>Next, let’s add our constructor method. This method runs as soon as the element is registered. I use <code>constructor</code> instead of <code>connectedCallback()</code> because I want to replace that default HTML that we added earlier as quickly as possible.</p>
<pre><code>constructor() {
  super();

  // Calculate the circle radius and the normalised version which is radius minus the stroke width
  const radius = this.viewBox / 2;
  const normalisedRadius = radius - this.stroke;
  this.calculatedCircumference = normalisedRadius * 2 * Math.PI;

  // Set the custom property viewbox value for our CSS to latch on to
  this.style.setProperty("--progress-indicator-viewbox", `${this.viewBox}px`);

  // Set the default aria role states
  this.setAttribute("aria-label", this.label);
  this.setAttribute("role", "progressbar");
  this.setAttribute("aria-valuemax", "100");

  // Render the component with all the data ready
  this.innerHTML = `
  &lt;div class="progress-indicator"&gt;
    &lt;div class="progress-indicator__visual"&gt;
      &lt;div data-progress-count class="progress-indicator__count"&gt;&lt;/div&gt;
      &lt;svg 
        fill="none" 
        viewBox="0 0 ${this.viewBox} ${this.viewBox}"
        width="${this.viewBox}"
        height="${this.viewBox}"
        focusable="false"
        class="progress-indicator__circle"
      &gt;
        &lt;circle 
          r="${normalisedRadius}"
          cx="${radius}"
          cy="${radius}"
          stroke-width="${this.stroke}"
          class="progress-indicator__background-circle"
        /&gt;
        &lt;circle 
          r="${normalisedRadius}"
          cx="${radius}"
          cy="${radius}"
          stroke-dasharray="${this.calculatedCircumference} ${this.calculatedCircumference}"
          stroke-width="${this.stroke}"
          class="progress-indicator__progress-circle"
          data-progress-circle
        /&gt;
      &lt;/svg&gt;
      &lt;svg 
        class="progress-indicator__check"
        focusable="false" 
        viewBox="0 0 20 20" 
        fill="none"
      &gt;
        &lt;path d="m8.335 12.643 7.66-7.66 1.179 1.178L8.334 15 3.032 9.697 4.21 8.518l4.125 4.125Z" fill="currentColor"/&gt;
      &lt;/svg&gt;
    &lt;/div&gt;
  &lt;/div&gt;
`;
}
</code></pre>
<p>Let’s break down what’s going on here. First up, we’re running <code>super();</code>. This instructs the parent class — <code>HTMLElement</code> — to construct. This is required.</p>
<div><h2>FYI</h2>
<p>Before I move on, let me just touch on a couple of bits. We’re rendering an SVG which has a couple of circles and a check icon. What we’re doing next in the <code>constructor</code> is calculating some properties for those SVG elements.</p>
</div>
<p>The first thing to do is calculate a <code>radius</code>, which is the <code>viewBox</code> property, halved. We get that data later in the component code.</p>
<p>The <code>normalisedRadius</code> is that <code>radius</code> value, minus the width of the stroke — also covered later in the component. This means the circles wont exceed the bounds of the <code>viewBox</code>.</p>
<p>The next bit is where the heat increases a touch — especially for a professional rectangle drawer that happens to code, in my case 😅. For displaying the progress, visually, we need to calculate the circumference of the circle using PI — y’know, from maths class at school. Unfortunately, I did not like maths at school, so I can’t really teach you how it works. What I can do though is tell you we calculate that circumference by doubling the normalised radius, then multiplying <em>that</em> by PI.</p>
<p>Next up, it’s time to set up some attributes on our <code>&lt;progress-indicator&gt;</code> component, and we start by setting a CSS Custom Property — <code>--progress-indicator-viewbox</code> — which we’ll hook on to later in our CSS. Following that, we need to make sure this isn’t a exclusionary, visual-only component, so we’ve got some aria roles to set:</p>
<ol>
<li><code>aria-label</code> allows us to provide a descriptive label, which is populated via a property of the web component</li>
<li>The <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/progressbar_role"><code>progressbar</code> role</a> gives the right type of information to assistive tech</li>
<li>The <code>aria-valuemax</code> works alongside the <code>progressbar</code> role to let an assistive tech user know what the max progress value will be</li>
</ol>
<p>Last up in the constructor, we’re populating a template literal by stitching markup with all of the data we gathered and calculated. A lot of it is self explanatory, but I want to point out the bit that shows progress.</p>
<p>Now, stand back as I attempt to explain the <code>stroke-dasharray</code> attribute…</p>
<p>The idea is to add a series of numbers like this: <code>stroke-dasharray="10 5"</code>. These numbers specify the length of alternating dashes and gaps between those dashes. So, for our example, the dashes will be 10 units long with a gap of 5 units. The units represent the length of the stroke, which in our case is the circumference of the circle.</p>
<p>We set our attribute as the following: <code>stroke-dasharray="${this.calculatedCircumference} ${this.calculatedCircumference}"</code>. This means each dash is the circumference of our circle and so is each gap. This allows us to reveal portions of this using <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dashoffset"><code>stroke-dashoffset</code></a>. We do that in the next part.</p>
<h2>Monitoring and displaying progress</h2>
<p>The core of our component is in, so now let’s add a method that updates the progress state, both visually and auditorily.</p>
<pre><code>setProgress(percent) {
  // Always make sure the percentage passed never exceeds the max
  if (percent &gt; 100) {
    percent = 100;
  }

  // Set the aria role value for screen readers
  this.setAttribute("aria-valuenow", percent);

  const circle = this.querySelector("[data-progress-circle]");
  const progressCount = this.querySelector("[data-progress-count]");

  // Calculate a dash offset value based on the calculated circumference and the current percentage
  circle.style.strokeDashoffset =
    this.calculatedCircumference -
    (percent / 100) * this.calculatedCircumference;

  // A human readable version for the text label
  progressCount.innerText = `${percent}%`;

  // Set a complete or pending state based on progress
  if (percent == 100) {
    this.setAttribute("data-progress-state", "complete");
  } else {
    this.setAttribute("data-progress-state", "pending");
  }
}
</code></pre>
<p>What’s happening here in a nutshell is we’re first checking that the progress doesn’t exceed 100%. If that’s all good, we move on to setting the <code>aria-valuenow</code> value to the current percentage value.</p>
<p>After grabbing the correct circle and text label elements, we populate the number value on the text label and then, the dash array magic happens. By multiplying the circumference by the point value of the percentage, we get a dash offset value that reveals only the right amount of stroke to visually show progress on the indicator.</p>
<p>If they had shown me stuff like this in school, I might have actually paid attention in maths lessons…</p>
<p>Lastly, we set some data attributes that we can hook on to CSS. Using the <a href="https://cube.fyi/exception.html">CUBE exception</a> approach, we’re setting either a progress or complete state on the <code>data-progress-state</code> attribute.</p>
<h2>Getters and observed attributes</h2>
<p>Right, these are the last few parts of our web component before we can move on to the fun part: CSS.</p>
<p>The first thing to do is observe the <code>progress</code> attribute on our custom element with the following snippet:</p>
<pre><code>static get observedAttributes() {
  return ["progress"];
}
</code></pre>
<p>This is a property <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#responding_to_attribute_changes">that’s available on all web components</a> and it does exactly what it says on the tin. You can observe as many attributes as you like. All you’ve got to do is pop them in the return array.</p>
<p>Let’s hook on to those attribute changes now.</p>
<pre><code>attributeChangedCallback(name, oldValue, newValue) {
  if (name === "progress") {
    this.setProgress(newValue);
  }
}
</code></pre>
<p>It’s pretty straightforward, really. We’re checking if the progress attribute is the one that triggered this <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks">lifecycle callback</a> then popping the new value in our <code>setProgress</code> method. Now, any outside JS can update this progress indicator by changing the <code>progress</code> attribute value.</p>
<p>This is the last part of the JS now, we need to set our getters for the various bits of data we need from the component’s attributes:</p>
<pre><code>get viewBox() {
  return this.getAttribute("viewbox") || 100;
}

get stroke() {
  return this.getAttribute("stroke") || 5;
}

get label() {
  return this.getAttribute("label") || "Current progress";
}
</code></pre>
<p>All we’re doing here is attempting to grab data from those attributes and if there’s nothing, setting some sensible defaults. Lovely.</p>
<h2>Styling it up</h2>
<p><em>Finally</em>, it’s time to get into some CSS. First up, I’ve modified and added to the Custom Properties I provided in <a href="https://piccalil.li/blog/challenge-009-progress-indicator/">the challenge</a>:</p>
<pre><code>:root {
  --font-base: "Space Mono", monospace;
  --transition: 200ms linear;
  --color-dark: #1f1a38;
  --color-dark-glare: #989ea9;
  --color-success: #76f7bf;

  --progress-indicator-color-complete: var(--color-success);
  --progress-indicator-progress-stroke: var(--color-dark);
  --progress-indicator-bg-stroke: var(--color-dark-glare);

</code></pre>
<p>All I’ve done is add some more specific theming properties that hook on to the more constant design tokens.</p>
<pre><code>.progress-indicator {
  font-family: var(--font-base);
  line-height: 1.1;
  color: var(--color-dark);
  container-type: inline-size;
  width: var(--progress-indicator-viewbox);
  height: auto;
}
</code></pre>
<p>This is the start of our component (<a href="https://cube.fyi/block.html">block</a>) styles. The two bits I want to touch on are the following:</p>
<ol>
<li><code>container-type</code> is being set to <code>inline-size</code> because we’re going to be using container units to size the text and icon</li>
<li>The <code>--progress-indicator-viewbox</code> property — if you remember — is set by our web component’s code. This is where our CSS hooks on to it by setting the width</li>
</ol>
<pre><code>.progress-indicator__progress-circle {
  stroke: var(--progress-indicator-progress-stroke, currentColor);
  transition: stroke-dashoffset var(--transition);
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}
</code></pre>
<p>This is the circle that holds our progress indicator stroke. The important part to touch on here is that the dash array starts from 90 degrees on our circle, so we need to transform it back to the top by rotating <code>-90deg</code>. The transform origin is from the center too.</p>
<pre><code>.progress-indicator__background-circle {
  stroke: var(--progress-indicator-bg-stroke, grey);
}
</code></pre>
<p>This part is pretty straightforward. If there’s a Custom Property value for the background circle’s stroke — the grey one — we use that. Otherwise, we default to the <code>grey</code> (or <code>gray</code> if you’re American) system colour.</p>
<pre><code>.progress-indicator__check {
  width: var(--progress-indicator-check-size, 60cqw);
  height: auto;
  display: none;
}
</code></pre>
<p>This part is our little check icon which shows up when <code>progress</code> reaches 100%. The reason we set the <code>.progress-indicator</code> to be a container earlier is because we’re using <code>cqw</code> units to size the checkbox, relative to the component’s width. Pretty handy, right?</p>
<pre><code>.progress-indicator__count {
  font-size: var(--progress-indicator-count-size, max(25cqw, 1rem));
  z-index: 1;
}
</code></pre>
<p>Sticking with that theme, we’re doing the same for the text too. The difference in our fallback value is we’re making sure the text is at least <code>1rem</code> with <code>max()</code>. Please make sure when you’re using container or viewport units to size text that it doesn’t fail the <a href="https://www.w3.org/WAI/WCAG20/Understanding/resize-text">WCAG 1.4.4 guideline: Resize text</a>.</p>
<p>Right, we need all the inner parts of the indicator to stack on top of each other nicely. Let me teach you a CSS grid party trick:</p>
<pre><code>.progress-indicator__visual {
  display: grid;
  grid-template-areas: "stack";
  align-items: center;
  place-items: center;
}

.progress-indicator__visual &gt; * {
  grid-area: stack;
}
</code></pre>
<p>The <code>.progress-indicator__visual</code> element houses both the text label and the icon, so what’s happening here is we’ve got a grid layout with one named area: <code>stack</code>. By putting both icon and label in the same area, they stack on top of each other. We don’t need to worry about z-index because they’re never shown together. Cool right?</p>
<p>Finally, let’s add some state exceptions to wrap this thing up.  Remember us modifying <code>data-progress-state</code> in our JS code? We’re going to respond to those changes now:</p>
<pre><code>[data-progress-state="complete"] .progress-indicator__progress-circle {
  fill: var(--progress-indicator-color-complete);
}

[data-progress-state="complete"] .progress-indicator__count {
  display: none;
}

[data-progress-state="complete"] .progress-indicator__check {
  display: revert;
}
</code></pre>
<p>The first rule changes the fill colour of our circle to the complete colour. There’s no fallback here because the icon is demonstrating completeness too.</p>
<p>The second rule hides the text label, followed by the third rule which reverts the display state of the icon, allowing its default to shine through, showing it visually.</p>
<p>And with all of that done, that’s a wrap!</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/mdogpWK">The finished 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>I was <em>blown away</em> by how many of you attempted this challenge. I honestly thought no one would be interested in <a href="https://piccalil.li/category/front-end-challenges-club/">Front-End Challenges Club</a> with it being idle for years at this point, but I’m glad I was completely wrong in that assumption.</p>
<p>My favourite attempt was by <a href="https://codepen.io/noleli/pen/GRLRoQq">Noah</a>, because they also <a href="https://noahliebman.net/2024/02/front-end-challenge-progress-indicator/">shared some good notes about their build too</a>. I’m a sucker for a blog post!</p>
<p>I deliberately didn’t add a rounded line cap to the indicator’s stroke because I wanted to free up people to use more CSS-based approaches. Props to Noah and folks like <a href="https://codepen.io/hi_mayank/pen/MWxNLWN">Mayank</a> who really pushed the boat out in that sense.</p>
<p>Finally, <a href="https://www.youtube.com/live/QhHW22zkH64?si%253D9rIY-ymHgUFg0IAY">Amit</a> and <a href="https://youtu.be/MXWP56LUI3g">Kevin</a> both recorded videos of their solutions. I love to see that and you should also check them out. There’s many ways to build this component and I’m really happy that the front-end community stepped up to demonstrate that 🙌</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Front-End challenge: progress indicator</title>
        <link>https://piccalil.li/blog/challenge-009-progress-indicator/?ref=front-end-challenges-club-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Mon, 26 Feb 2024 08:23:09 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/challenge-009-progress-indicator/?ref=front-end-challenges-club-category-rss-feed</guid>
        <description><![CDATA[<p>Ooof it’s been a while since there was a <a href="https://piccalil.li/category/front-end-challenges-club/">Front-End Challenges Club</a>. Just <em>[checks notes]</em> 3 and a half years…</p>
<p>This is a handy little component that visually shows progress on a circular path. On the surface, it looks simple, but there’s lots to consider with state <em>and</em> how assistive technology can understand it.</p>
<p>Here’s the default and completed state:</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/fecc/fe-challenge-09.jpg" alt="On the left, a progress indicator with 25% progress. On the right, a completed one where the text has been replaced with a checkmark" /></p>
<h2>Points to consider</h2>
<ul>
<li>What is the experience going to be like when someone doesn’t have JavaScript enabled?</li>
<li>What technology is best suited for this component?</li>
<li>How can JavaScript and CSS communicate well?</li>
</ul>
<h2>Assets</h2>
<p>I create a Figma composition for this challenge, which you can view either with a free account or anonymously. Here’s some links:</p>
<ul>
<li><a href="https://www.figma.com/file/fqQzPwsoFqaoD8NVTcH3sX/Progress-Indicator?type=design&amp;node-id=0%3A1&amp;mode=design&amp;t=Ry6nqolAqOczXonK-1">Figma composition</a></li>
<li><a href="https://fonts.google.com/specimen/Space+Mono">Space Mono font</a></li>
</ul>
<p>Here’s some CSS Custom Properties you might find useful too:</p>
<pre><code>:root {
  --font-base: 'Space Mono', monospace;
  --color-dark: #1f1a38;
  --color-dark-glare: #989ea9;
  --color-success: #76f7bf;
}
</code></pre>
<h2>Wrapping up</h2>
<p>I’ll publish my solution to this challenge soon. Until then, have fun building it!</p>
<p>If you share your version, make sure you <a href="https://piccalil.li/contact">let me know</a> and as always, I’ll pick my favourite and share it with you in my solution post.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Solution: Progress Stepper</title>
        <link>https://piccalil.li/blog/solution-008-progress-stepper/?ref=front-end-challenges-club-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Thu, 29 Oct 2020 00:00:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/solution-008-progress-stepper/?ref=front-end-challenges-club-category-rss-feed</guid>
        <description><![CDATA[<p>This is the solution for <a href="https://piccalil.li/blog/challenge-008-progress-stepper/">Challenge #008</a>.</p>
<p>You’d be forgiven for thinking that this challenge is going to require a lot of HTML elements to get it working, because when you scratch under the surface, there’s a lot going on and plenty to consider. Those of you that are not new to <a href="https://piccalil.li/category/Front-End%20Challenges%20Club/">Front-End Challenges Club</a> will be all too aware of this theme by now.</p>
<p>Luckily, we can actually build this challenge with a pretty darn slim <em>and</em> semantic HTML base. The CSS does admittedly take the brunt of the work, though, but let’s dive in and learn some cool stuff regardless.</p>
<p>This solution has the following code files:</p>
<ul>
<li><code>index.html</code></li>
<li><code>css/global.css</code></li>
</ul>
<p>You can <a href="https://fecc-challenge-008.netlify.app">see a live demo</a> or <a href="https://github.com/piccalil-li/fecc-solution-008-progress-stepper/archive/main.zip">download a complete version of what we’re making in this solution, here</a>.</p>
<h2>HTML</h2>
<p>As always, let’s start with a nice HTML shell. Open up your <code>index.html</code> file and add the following:</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="ie=edge" /&gt;
    &lt;title&gt;Front-End Challenges Club #008&lt;/title&gt;
    &lt;link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/modern-css-reset/dist/reset.min.css"
    /&gt;
    &lt;link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Asap:wght@700&amp;family=Saira+Condensed:wght@400;900&amp;display=swap"
    /&gt;
    &lt;link rel="stylesheet" href="/css/global.css" /&gt;
  &lt;/head&gt;
  &lt;body&gt;&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>This is our core structure and as you can see, it’s pretty simple. We’re pulling in our CSS, along with a CDN version of <a href="https://hankchizljaw.com/wrote/a-modern-css-reset/">my modern reset that I use</a>. We’re also pulling in some fonts directly from <a href="https://fonts.google.com">Google Fonts</a>.</p>
<p>Now we have our HTML shell, let’s add our main stepper component markup. We’ll add it all in one chunk and go through it afterwards. Open up <code>index.html</code> and add the following to it, between the opening and closing <code>&lt;body&gt;</code> tags:</p>
<pre><code>&lt;div class="stepper"&gt;
  &lt;ol role="list" aria-label="Purchase steps"&gt;
    &lt;li data-status="complete"&gt;
      &lt;span aria-hidden="true"&gt;Step one&lt;/span&gt;
      &lt;strong&gt;Your basket&lt;/strong&gt;
    &lt;/li&gt;
    &lt;li data-status="complete"&gt;
      &lt;span aria-hidden="true"&gt;Step two&lt;/span&gt;
      &lt;strong&gt;Your details&lt;/strong&gt;
    &lt;/li&gt;
    &lt;li aria-current="step"&gt;
      &lt;span aria-hidden="true"&gt;Step three&lt;/span&gt;
      &lt;strong&gt;Payment&lt;/strong&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;span aria-hidden="true"&gt;Step four&lt;/span&gt;
      &lt;strong&gt;Order complete&lt;/strong&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</code></pre>
<p>Is that it?? Hell yeh it is. Let’s break it down:</p>
<ol>
<li>We use an ordered list, because this stepper has specifically ordered content</li>
<li>We add an <code>aria-label</code> to describe what this list is. We only do this because of what we implement in the next point, so it’s helpful to provide some more context to screen readers.</li>
<li>Inside each list, we hide <code>Step X</code> from screen readers with <code>aria-hidden="true"</code>. Because we use an <code>&lt;ol&gt;</code>, we get that numerical announcement already. The <code>Step X</code> bits are purely for providing visual sugar</li>
<li>We’re using <code>aria-current="step"</code> which <a href="https://tink.uk/using-the-aria-current-attribute/">tells assistive technology, such as screen readers that this item is the current step</a>. We’re using <code>data-status</code> to hook onto finite state in our CSS too, which we will get into more detail with later on</li>
</ol>
<p>That’s our lot for HTML. Importantly, if no CSS manages to load, this list will work perfectly well as our stepper. Good ol’ progressive enhancement in action.</p>
<h2>CSS</h2>
<p>Let’s start off by extending the root custom properties that I provided in the <a href="https://piccalil.li/blog/challenge-008-progress-stepper/">challenge post</a>.</p>
<p>Add the following to <code>css/global.css</code>:</p>
<pre><code>:root {
  --color-light: #fdfdfd;
  --color-dark: #27474e;
  --color-mid: #496970;
  --color-primary: #f3344a;
  --color-primary-glare: #f4d7da;
  --color-secondary: #678c94;
  --color-secondary-glare: #ebf0f1;
  --font-condensed: 'Saira Condensed', sans-serif;
  --font-sans: 'Asap', sans-serif;
  --shadow: 0px 0px 40px rgba(39, 71, 78, 0.1);
  --size-300: 0.88rem;
  --size-400: 1rem;
  --size-500: 1.44rem;
  --size-600: 2rem;
  --size-700: 2.5rem;
}
</code></pre>
<p>The colours and treatments are the same. All I added here is some fonts and a low-key size scale for keeping things consistent.</p>
<p>Let’s add the following global styles to our CSS next:</p>
<pre><code>body {
  font-family: sans-serif;
  background: var(--color-light);
  color: var(--color-dark);
  display: grid;
  place-items: center;
}
</code></pre>
<p>There’s not much to talk about here as it’s all straightforward. We’re using the ol’ <code>place-items</code> CSS Grid trick purely for demo purposes to frame the finished stepper nicely.</p>
<p>Now, let’s start on the stepper component itself. Add the following to your CSS:</p>
<pre><code>.stepper {
  --stepper-y-space: var(--size-500);
  --stepper-x-space: var(--size-700);
  --stepper-modifier: 1.5ex;

  border: 1px solid var(--color-secondary-glare);
  padding: var(--size-600);
  box-shadow: var(--shadow);
  border-radius: var(--size-400);
  min-width: 20rem;
  counter-reset: steps;
}
</code></pre>
<p>The first thing to note is that we are setting some specific custom properties for this component. These will make spacing and positioning nice and consistent, without having magic numbers in our calculations.</p>
<p>The <code>--stepper-modifier</code> is a handy property that we are going to use for more fine-grained control. We use an <code>ex</code> unit, which is the height of the <code>x</code> character (<a href="https://www.typotheque.com/help/specific_font_questions/x-height">x-height</a>) in your chosen font and size. This helps us position things flush with our text with a unit that’s designed for exactly that.</p>
<p>Lastly, we’re using CSS counters in this solution. We’re resetting the <code>steps</code> counter which means that each instance of <code>.stepper</code> on a page starts at <code>1</code>.</p>
<p>Next up: list styles. Add the following to your CSS:</p>
<pre><code>.stepper [role='list'] {
  font-family: var(--font-condensed);
  line-height: 1.1;
  text-transform: uppercase;
  margin: calc(var(--stepper-y-space) * -1) 0 0 0;
  padding: 0;
  list-style: none;
}

.stepper li {
  padding-left: var(--stepper-x-space);
  padding-top: var(--stepper-y-space);
  position: relative;
  counter-increment: steps;
}
</code></pre>
<p>You might notice that we are using <code>[role="list"]</code> as a selector. <a href="https://www.scottohara.me/blog/2019/01/12/lists-and-safari.html">Something I was made aware of recently</a> is that if you remove list styles from a list, VoiceOver will sometimes not announce it as a list, which is…well…incredibly frustrating. We could select the <code>&lt;ol&gt;</code> directly, but this approach gives us future flexibility to use a <code>&lt;ul&gt;</code> too.</p>
<p>The actual <code>li</code> elements get padded up to create space and they increment the <code>steps</code> counter.</p>
<p>You might have noticed that we add padding to every <code>li</code> element, including the first one. The even more eagle-eyed amongst us might have also noticed that we set a negative top margin on the <code>[role="list"]</code>. This helps us with absolutely positioned elements because the edges of the relative parent are X pixels from the visual edge.</p>
<p>Did you know, you can make a CSS Custom Property negative by multiplying it by <code>-1</code>?</p>
<p>For example, if you set <code>--my-var</code> to <code>1</code>, and <code>--my-negative-var</code> to <code>calc(var(--my-var) * -1)</code>: <code>--my-negative-var</code> will be <code>-1</code>.</p>
<p>Let’s add some vertical lines to our stepper. Add the following to your CSS:</p>
<pre><code>.stepper li::before,
.stepper li::after {
  display: none;
  content: '';
  width: 2px;
  background: var(--color-primary);
  position: absolute;
  left: 7px;
}

/* Up line */
.stepper li::before {
  height: calc(var(--stepper-y-space) + var(--stepper-modifier));
  top: 0;
}

/* Down line */
.stepper li::after {
  height: 100%;
  top: calc(var(--stepper-y-space) + var(--stepper-modifier));
}
</code></pre>
<p>The first thing we do here is set each line to visually look the same: red and <code>2px</code> wide. Then, we hide them both by default because it is state that will determine their visibility.</p>
<p>The line is split in two parts; the <code>::before</code> pseudo-element is our up line and the <code>::after</code> pseudo-element is our down line. Because each <code>li</code> element has top space of <code>--stepper-y-space</code>, we make the up line have the same value in <code>height</code> and add that <code>--stepper-modifier</code>, which is <code>1.5ex</code> to account for the small amount of vertical space the font creates.</p>
<p>The down line uses similar logic but is 100% high. This makes it “leak” into the following <code>li</code> element. It’s all smoke and mirrors in here today, folks.</p>
<p>Now, we need to toggle their visibility. Add the following to your CSS:</p>
<pre><code>.stepper li[aria-current='step']::before {
  display: block;
}

.stepper li[data-status='complete']::after,
.stepper li[data-status='complete']::before {
  display: block;
}

/* Always hide the top up line and the bottom down line */
.stepper li:first-child::before,
.stepper li:last-child::after {
  display: none;
}
</code></pre>
<p>Firstly, the current item’s up line is shown. This is subsequently reset in the last block because if the first item is also the current item, there is not preceding step to link to, visually.</p>
<p>After that, we use an <a href="https://piccalil.li/cube-css/exception/">Exception</a> to link items together when they are complete. If an item has a <code>data-status='complete'</code> attribute, then both the up and down lines are shown.</p>
<p>Right, that’s the lines done, so let’s add our dots. Add the following to your CSS:</p>
<pre><code>.stepper strong {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--size-500);
  text-transform: none;
  position: relative;
}

/* Dot */
.stepper strong::after {
  content: '';
  display: block;
  width: 16px;
  height: 16px;
  border-radius: 16px;
  background-color: var(--color-secondary-glare);
  position: absolute;
  bottom: 100%;
  left: calc(var(--stepper-x-space) * -1);
  border: 1px solid var(--color-secondary);
  transform: translateY(50%);
  z-index: 1;
}
</code></pre>
<p>The first thing we are doing here is setting the <code>&lt;strong&gt;</code> in our stepper to be a block, which breaks it on to a new line. It’s also helpful to set it as a block because we make it a relative parent for both the decorative dot <em>and</em> the counter increment.</p>
<p>The dot is built up using another pseudo-element and for this, I am using pixels because I want exact sizing. We then push it out to the left using a negative version of the <code>--stepper-x-space</code> custom property. Lastly, using the magic of transforms, we can sit the dot in the center by first, setting <code>bottom: 100%</code> followed by <code>translateY(50%)</code> which pushes it back down, 50% of its height (<code>8px</code> in this instance).</p>
<p>Lastly, we want the dot to sit on top of the lines, so all we need to do for that is set <code>z-index</code> to <code>1</code>. This is because so far, nothing else in this <a href="https://philipwalton.com/articles/what-no-one-told-you-about-z-index/">stacking context</a> is set.</p>
<p>Let’s add our decorative counter next. This is the “faded” number that sits behind each step. Add the following to your CSS:</p>
<pre><code>.stepper strong::before {
  content: counter(steps, decimal-leading-zero);
  speak-as: numbers;
  font-family: var(--font-condensed);
  font-weight: 900;
  color: var(--color-secondary-glare);
  position: absolute;
  bottom: 2ex;
  left: -1ch;
  z-index: -1;
  line-height: 1;
}
</code></pre>
<p>Some cool stuff happening here, so let’s break it down:</p>
<ol>
<li>First up: we finally use that counter by setting the pseudo-element’s <code>content</code> as <code>counter(steps, decimal-leading-zero)</code>. That second property does what it says on the tin: adds a leading zero</li>
<li>Because we add a leading zero, I want to be double sure that this is announced as numbers, if it is announced by a screen reader. In Firefox, a <code>speak-as</code> property is supported, so we add <code>numbers</code> as the value</li>
<li>After the usual type settings, we have a magic trick. Because the number has a negative indent, we can use a really handy property to set that. By setting <code>left</code> as <code>-1ch</code>, we are shifting it left by negative the width of the <code>0</code> character, which is especially handy in this context</li>
</ol>
<p>We’re getting close to the end now. Let’s work with some finite state. Add the following to your CSS:</p>
<pre><code>.stepper [aria-current='step'] strong::after {
  background-color: var(--color-primary-glare);
  border-color: var(--color-primary);
}
</code></pre>
<p>Our active state gives us a useful, finite hook to work with. Ideally, whatever is powering this stepper will only set <code>aria-current="step"</code> on the current item. With this in mind, we can reasonably comfortably set some different colours to highlight it.</p>
<p>Next, add the following to your CSS:</p>
<pre><code>.stepper [data-status='complete'] strong::after {
  background-color: var(--color-primary);
  background-image: url('data:image/svg+xml;utf8,&lt;svg fill="white" width="9" height="7" xmlns="http://www.w3.org/2000/svg"&gt;&lt;path d="M7.868.141a.474.474 0 01.03.652L3.244 6.087a.433.433 0 01-.627.028L.292 3.911a.474.474 0 01-.03-.652.433.433 0 01.628-.028l1.996 1.892L7.24.17A.433.433 0 017.868.14z"/&gt;&lt;/svg&gt;');
  background-size: 9px 7px;
  background-position: 3px center;
  background-repeat: no-repeat;
  border-color: var(--color-primary);
}
</code></pre>
<p>Ok, this is fun. This is the “ticked” (or “checked”) state. We can use a smart CSS trick for this. You can use inline SVG <em>in</em> your CSS by setting a background image and prefixing the SVG code with <code>data:image/svg+xml;utf8,</code>, which essentially lets CSS understand that is SVG that should be treated as an image.</p>
<p>Because the dot is sized with pixels, we can use pixels to position the tick for us. This is mainly because a tick will never look central, even if it is central. We fix this using an <a href="https://marvelapp.com/blog/optical-adjustment-logic-vs-designers/">optical adjustment</a>, which is a nice term for “designer’s curse”, which I prefer to call it.</p>
<p>With all of that now set, <em>we are done</em>! Go ahead and load it up in your browser to enjoy your hard work.</p>
<h2>Wrapping up</h2>
<p>I really hope you’ve enjoyed this tutorial. <a href="https://github.com/piccalil-li/fecc-solution-008-progress-stepper/archive/main.zip">You can grab a zip of the final code that I wrote</a> and also <a href="https://fecc-challenge-008.netlify.app">see a live version</a> too. There’s even a <a href="https://github.com/piccalil-li/fecc-solution-008-progress-stepper">git repository</a>.</p>
<p>My <a href="https://geoffrich.net/demos/front-end-cc-008/">favourite attempt for this challenge</a> was by <a href="https://twitter.com/JustOneGeoff/">Geoff</a>, who also did a <a href="https://geoffrich.net/posts/progress-stepper/">fantastic, detailed write-up</a>. Nice work, Geoff!</p>
<p>It’s great to be back with these challenges. Stay tuned for more soon!</p>
<p>Until the next time, take it easy 👋</p>
        
        ]]></description>
        
      </item>
    
    </channel>
  </rss>
