← Back to gallery
CSS

Active Tab Underline with :has()

A tab bar that slides an underline across active tabs by switching container-level custom properties via :has(), with a class-toggle fallback for browsers without :has() support.

Duration
1s
Resolution
1440×900
Format
CSS
tab-barunderline-slide:hasaria-selectedcustom-properties

Layout transition / :has() / aria-selected

Active tab underline with :has()

Reimplements a sliding tab indicator driven by a :has(...) rule on the container, so container custom properties flip whenever the selected descendant changes — no JS class toggling needed.

dashboard barOverview

Equal-weight tabs

Dashboard Bar

Four dashboard tabs share equal width, letting the underline slide across uniform increments via translateX driven by a parent :has(...) rule.

  • dashboard
  • equal width
  • translateX

Content-width tabs

Editor Tab Strip

An editor-style tab strip where tabs grow to fit their labels; the underline morphs in both x and width between active tabs.

  • editor
  • content width
  • data-attrs

Pill indicator

Browser Tabs

A browser-style tab bar where a soft pill — not a thin underline — rides behind the active label, driven by the same :has() pattern.

  • pill
  • browser
  • weighty

Tab underline inspector

Dashboard Bar

Click any tab to watch the indicator slide.

Indicator
2px underline
Slide
0.22s

The equal-width layout keeps translation math simple: each tab is 1 / N of the container width, so the indicator jumps in clean fractions.

css
.tab-bar {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  --active-index: 0;
}

.tab-bar button {
  padding: 10px 16px;
  background: transparent;
  border: 0;
  color: rgba(148, 163, 184, 0.8);
  cursor: pointer;
  transition: color 0.22s ease;
}

.tab-bar [aria-selected="true"] { color: #7dd3fc; }

/* :has() switches container-level custom property based on which tab is selected */
.tab-bar:has(button:nth-of-type(2)[aria-selected="true"]) { --active-index: 1; }
.tab-bar:has(button:nth-of-type(3)[aria-selected="true"]) { --active-index: 2; }
.tab-bar:has(button:nth-of-type(4)[aria-selected="true"]) { --active-index: 3; }

.tab-bar::after {
  content: '';
  position: absolute;
  bottom: 0;
  height: 2px;
  width: calc(100% / 4);
  background: #7dd3fc;
  transform: translateX(calc(100% * var(--active-index)));
  transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* Fallback without :has() */
@supports not selector(:has(*)) {
  .tab-bar::after { display: none; }
  .tab-bar [aria-selected="true"]::after {
    content: '';
    position: absolute;
    inset: auto 0 0 0;
    height: 2px;
    background: #7dd3fc;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tab-bar::after { transition: none; }
}