destroytoday.com

Custom focus rings using :focus-within

Now that I’m exploring a modern Cushion, I started looking into focus states. With the current Cushion, the younger, less considerate version of myself took the naive approach by hiding those “ugly” focus rings on all elements and calling it a day. Occasionally, I’d hear from folks that it’s not obvious which input field is focused. While I did agree, I never really did anything about it. I never took the time to actually tab through the app and see what happens. Once I did, I felt like if someone triggered a cursor: none on the page. Because I didn’t deal with this every day, it wasn’t high on my priority list. Luckily, I’ve built up more experience since then, and I now know better.

When starting something new, it’s much easier to establish a “guiding principle” from the beginning, then simply apply it to everything you build from that point on. Otherwise, you might find yourself trying to retrofit the principle into code that wasn’t written with that principle in mind. There might even be areas where the code doesn’t even allow the principle without serious restructuring. Then, once you finally finish refactoring one area, you realize you barely made a dent in applying the principle to the entirety of the app.

These situations tend to paralyze me because they send my mental simulation “process” into somewhat of an infinite loop. I think through all the work that would be involved to apply an obvious principle—clear focus states and support for keyboard navigation—to a codebase with six years of legacy code. This is yet another reason why I’m exploring a modern Cushion by starting with a blank page. I don’t want the solution to look and feel like a duct-taped bumper. I want to do it the right way from the start.

Okay—I just looked up at the title of this post and realized I’ve become the food blogger who needs to write their life story before getting to the recipe.

While looking into focus states, I came across the CSS pseudo class :focus-within, which has a very Zen-like name to it. This handy selector does exactly what it says by letting you style an element when the focus exists within the element. Back when I started Cushion in 2014, this pseudo class didn’t exist, so the only option was :focus, which made more-complex components trickier to style. I’m thrilled to now have :focus-within in my toolbox.

To provide an example of how I’m using :focus-within, I have a textfield component where both the label and input are contained within a box:

textfield

As an aside, this uses the handy trick of wrapping the <input> element in the <label>, so you can click anywhere within the box and it’ll set the focus on the input field:

<label>
  <span>Name</span>
  <input>
</label>

My design for the focus state puts a blue box right inside the grey box:

textfield-outer-focus-ring

If I were to simply style the input field with a border using :focus, I’d end up with the border only around the input field and not around the entire block. This feels claustrophobic and doesn’t translate well to other input types.

textfield-inner-focus-ring

Sure, I could resize the input field to the size of the box and add more padding, but again, this wouldn’t translate well to other input types where you can’t easily do that. Or, I could use a CSS trick, like input:focus + label::after to create the border using an ::after pseudo element. But, why rely on tricks now that we have a proper way to do this? Using label:focus-within, I can easily achieve this style:

label:focus-within {
  outline: 2px solid blue;
}

The above example is simplified semi-pseudo code, but for the actual design, the code is a bit more complex:

.TextInputField { /* <label> */
  position: relative;
}

.TextInputField:focus-within::after {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: solid 2px blue;
  content: "";
}

I do still rely on a pseudo element, but that’s only because I need both an outline and a border for the focus ring. And before any “well, actually” folks leap out of their seat to say that I can use both outline and border on the label element, there’s more context behind this implementation that leads me to do it this way.

After applying this style to a text input component, I do see a refactor opportunity to extract the label and focus state into their own <Field> component. Then, my text input component and any other form components could simply nest inside the field component.