From db38a44ab3287550d071f12c8eb63f6724909895 Mon Sep 17 00:00:00 2001 From: rajanpanth Date: Sun, 23 Aug 2026 21:33:45 +0545 Subject: [PATCH] fix(registry): make the tabs active label follow the sliding indicator The component pinned its active colour to the second button with button:nth-of-type(2), while the pill starts under the first. Before the slide, Plan sat gray on the dark pill and Debug sat white on the light track, and the two only agreed once the pill reached Debug. The pill now carries a clipped copy of the label row. Countering the pill's own translate keeps that copy aligned with the buttons underneath at every frame, so the white text is exactly the part of the row the pill covers and it crosses over as the pill moves, driven by the same --hf-tab-x the timeline already animates. The inactive gray also failed WCAG AA at 4.40:1 against the track. #6b6b73 measures 4.80:1. Fixes #3266 --- .../components/tabs-slide-indicator/demo.html | 58 ++++++++++++++++--- .../tabs-slide-indicator.html | 47 +++++++++++++-- 2 files changed, 91 insertions(+), 14 deletions(-) diff --git a/registry/components/tabs-slide-indicator/demo.html b/registry/components/tabs-slide-indicator/demo.html index 99c36446a..d4afca005 100644 --- a/registry/components/tabs-slide-indicator/demo.html +++ b/registry/components/tabs-slide-indicator/demo.html @@ -54,6 +54,8 @@ background: #18181b; transform: translateX(var(--hf-tab-x)); box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18); + overflow: hidden; + z-index: 2; } .hf-transition-tabs-slide-indicator button { position: relative; @@ -61,10 +63,19 @@ height: 36px; border: 0; background: transparent; - color: #71717a; + color: #6b6b73; font-weight: 600; } - .hf-transition-tabs-slide-indicator button:nth-of-type(2) { + .hf-transition-tabs-slide-indicator .indicator-labels { + position: absolute; + top: 0; + left: 0; + display: grid; + grid-template-columns: repeat(3, 86px); + transform: translateX(calc(var(--hf-tab-x) * -1)); + pointer-events: none; + } + .hf-transition-tabs-slide-indicator .indicator-labels button { color: #fff; } @@ -88,7 +99,12 @@ { "id": "labels", "type": "string", "role": "content", "label": "Labels", "description": "The tab names, separated by commas. Two or more names rebuild the row.", "default": "Plan,Debug,Ask" } ]' > - @@ -117,6 +133,17 @@ return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback; } + // The mirrored copy is decorative: it repeats a label the row + // already carries, so it stays out of the tab order. + function makeTab(text, mirrored) { + var button = document.createElement("button"); + button.textContent = text; + if (mirrored) { + button.tabIndex = -1; + } + return button; + } + var accent = accents[pick(accents, vars.accent, "ink")]; var width = widths[pick(widths, vars.width, "standard")]; var radius = shapes[pick(shapes, vars.shape, "pill")]; @@ -140,14 +167,18 @@ roots[i].style.setProperty("--hf-tab-radius", radius); // A single tab has nowhere to slide to, so it is left as authored. if (labels.length > 1) { + // Every label exists twice: once in the row and once in the + // clipped copy the pill carries, so both are rebuilt together. + var mirror = roots[i].querySelector(".indicator-labels"); var old = roots[i].querySelectorAll("button"); for (var j = 0; j < old.length; j += 1) { old[j].parentNode.removeChild(old[j]); } for (var k = 0; k < labels.length; k += 1) { - var button = document.createElement("button"); - button.textContent = labels[k]; - roots[i].appendChild(button); + roots[i].appendChild(makeTab(labels[k], false)); + if (mirror) { + mirror.appendChild(makeTab(labels[k], true)); + } } roots[i].style.setProperty("--hf-tab-count", String(labels.length)); } @@ -190,6 +221,8 @@ background: var(--hf-tab-fill, #18181b); transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1))); box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18); + overflow: hidden; + z-index: 2; } .hf-transition-tabs-slide-indicator button { position: relative; @@ -197,10 +230,19 @@ height: 36px; border: 0; background: transparent; - color: #71717a; + color: #6b6b73; font-weight: 850; } - .hf-transition-tabs-slide-indicator button:nth-of-type(2) { + .hf-transition-tabs-slide-indicator .indicator-labels { + position: absolute; + top: 0; + left: 0; + display: grid; + grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px)); + transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1) * -1)); + pointer-events: none; + } + .hf-transition-tabs-slide-indicator .indicator-labels button { color: #fff; } diff --git a/registry/components/tabs-slide-indicator/tabs-slide-indicator.html b/registry/components/tabs-slide-indicator/tabs-slide-indicator.html index d472843d4..3512e5eee 100644 --- a/registry/components/tabs-slide-indicator/tabs-slide-indicator.html +++ b/registry/components/tabs-slide-indicator/tabs-slide-indicator.html @@ -39,7 +39,12 @@ { "id": "labels", "type": "string", "role": "content", "label": "Labels", "description": "The tab names, separated by commas. Two or more names rebuild the row.", "default": "Plan,Debug,Ask" } ]' > - + @@ -104,6 +124,17 @@ return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback; } + // The mirrored copy is decorative: it repeats a label the row already + // carries, so it stays out of the tab order. + function makeTab(text, mirrored) { + var button = document.createElement("button"); + button.textContent = text; + if (mirrored) { + button.tabIndex = -1; + } + return button; + } + var accent = accents[pick(accents, vars.accent, "ink")]; var width = widths[pick(widths, vars.width, "standard")]; var radius = shapes[pick(shapes, vars.shape, "pill")]; @@ -127,14 +158,18 @@ roots[i].style.setProperty("--hf-tab-radius", radius); // A single tab has nowhere to slide to, so it is left as authored. if (labels.length > 1) { + // Every label exists twice: once in the row and once in the clipped + // copy the pill carries, so both have to be rebuilt together. + var mirror = roots[i].querySelector(".indicator-labels"); var old = roots[i].querySelectorAll("button"); for (var j = 0; j < old.length; j += 1) { old[j].parentNode.removeChild(old[j]); } for (var k = 0; k < labels.length; k += 1) { - var button = document.createElement("button"); - button.textContent = labels[k]; - roots[i].appendChild(button); + roots[i].appendChild(makeTab(labels[k], false)); + if (mirror) { + mirror.appendChild(makeTab(labels[k], true)); + } } roots[i].style.setProperty("--hf-tab-count", String(labels.length)); }