DEV NOTES
Pure CSS Tabs with :target and :has()
There’s a category of UI component that everyone assumes needs JavaScript, and tabs sit near the top of the list. This post is about deleting that assumption.
The old trick: :target
CSS has always had a state mechanism hiding in plain sight: the URL fragment. Link to #panel-2, and #panel-2:target matches. That gives you tabs in three lines:
.panel { display: none; }
.panel:target { display: block; }
.panel:first-child { display: block; } /* default tab */
The problem is that last line — it’s a lie. Once any panel is targeted, the first one should hide, and :first-child doesn’t know about it.
The new trick: :has()
This is where :has() earns its “parent selector” reputation:
/* Hide the default panel only when some other panel is targeted */
.tabs:has(:target) .panel:first-child:not(:target) {
display: none;
}
One selector, and the default-tab problem is gone. The container itself knows whether any of its children is targeted, and adjusts.
Why bother?
Partly because it’s a good puzzle — and I love experimenting with CSS the way some people do crosswords. But mostly because every one of these experiments recalibrates my sense of where JavaScript is actually required. The answer keeps shrinking.
The full experiment is on my CodePen, along with the zero-JS modal that uses the same philosophy. Steal freely.