HTML Button Group for the OCD

The CSS :has pseudo-class enables squeaky clean markup for group components, like a button group.

Mdash recently shipped its button group using :has and the OCD within me ranks the markup 5 out of 5:

<div role="group">
  <button ord="secondary" aria-pressed="true">One</button>
  <button ord="secondary">Two</button>
  <button ord="secondary">Three</button>
</div>

With Firefox recently adding :has support, this now works across all browsers.

Previously, you would need to define a custom tag (or class if you're not following TAC CSS) for the grouping element. That's no longer necessary because :has effectively selects a parent based on its children, like button children in this case. A div with the group role wasn't enough of a selector in the past to define a button group, but now we can look into its content, which is what :has makes possible, and so with that the cleanest CSS rule for a button group is now:

[role="group"]:has(> button) {
  display: inline-flex;

  & button {
    /* Button default styles */

    &[aria-pressed="true"] {
      /* Pressed button styles */
    }

    &:first-of-type {
      /* First button styles */
    }

    &:last-of-type {
      /* Last button styles */
    }
  }
}

That's native CSS nesting by the way, not SCSS.

I have learned that it is best to let the consuming app manage its own clicks for simple components like this, so this button group is CSS-only. An app can easily toggle its buttons as desired using event delegation and the ariaPressed property:

<div onclick="toggle(event)" role="group">
  <button ord="secondary" value="ready">Ready</button>
  <button ord="secondary" value="complete">Complete</button>
  <button ord="secondary" value="overdue">Overdue</button>
</div>
<script>
  // Allows only one button pressed at a time
  function toggle(e) {
    e.currentTarget.querySelectorAll('button').forEach(btn => {
      btn.ariaPressed = btn === e.target ? 'true' : 'false';
      console.log(`Button: ${btn.value}, Pressed: ${btn.ariaPressed}`);
    })
  }
</script>

Using whatever techniques the app has for handling events, it can implement radio-style logic or checkbox-style logic or maybe a button group with link buttons. This approach gets a nice button group style and while letting the app leverage native API for all sorts of interactive use cases.

It's gonna be fun looking at components from the perspective of :has. I can imagine several more instances where this will mean cleaner, more standards-based markup.