Skip to content

Commit db38a44

Browse files
committed
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
1 parent dd0626a commit db38a44

2 files changed

Lines changed: 91 additions & 14 deletions

File tree

registry/components/tabs-slide-indicator/demo.html

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,28 @@
5454
background: #18181b;
5555
transform: translateX(var(--hf-tab-x));
5656
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
57+
overflow: hidden;
58+
z-index: 2;
5759
}
5860
.hf-transition-tabs-slide-indicator button {
5961
position: relative;
6062
z-index: 1;
6163
height: 36px;
6264
border: 0;
6365
background: transparent;
64-
color: #71717a;
66+
color: #6b6b73;
6567
font-weight: 600;
6668
}
67-
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
69+
.hf-transition-tabs-slide-indicator .indicator-labels {
70+
position: absolute;
71+
top: 0;
72+
left: 0;
73+
display: grid;
74+
grid-template-columns: repeat(3, 86px);
75+
transform: translateX(calc(var(--hf-tab-x) * -1));
76+
pointer-events: none;
77+
}
78+
.hf-transition-tabs-slide-indicator .indicator-labels button {
6879
color: #fff;
6980
}
7081
</style>
@@ -88,7 +99,12 @@
8899
{ "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" }
89100
]'
90101
>
91-
<span class="indicator"></span><button>Plan</button><button>Debug</button
102+
<span class="indicator"
103+
><span class="indicator-labels" aria-hidden="true"
104+
><button tabindex="-1">Plan</button><button tabindex="-1">Debug</button
105+
><button tabindex="-1">Ask</button></span
106+
></span
107+
><button>Plan</button><button>Debug</button
92108
><button>Ask</button>
93109
</div>
94110
</div>
@@ -117,6 +133,17 @@
117133
return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;
118134
}
119135

136+
// The mirrored copy is decorative: it repeats a label the row
137+
// already carries, so it stays out of the tab order.
138+
function makeTab(text, mirrored) {
139+
var button = document.createElement("button");
140+
button.textContent = text;
141+
if (mirrored) {
142+
button.tabIndex = -1;
143+
}
144+
return button;
145+
}
146+
120147
var accent = accents[pick(accents, vars.accent, "ink")];
121148
var width = widths[pick(widths, vars.width, "standard")];
122149
var radius = shapes[pick(shapes, vars.shape, "pill")];
@@ -140,14 +167,18 @@
140167
roots[i].style.setProperty("--hf-tab-radius", radius);
141168
// A single tab has nowhere to slide to, so it is left as authored.
142169
if (labels.length > 1) {
170+
// Every label exists twice: once in the row and once in the
171+
// clipped copy the pill carries, so both are rebuilt together.
172+
var mirror = roots[i].querySelector(".indicator-labels");
143173
var old = roots[i].querySelectorAll("button");
144174
for (var j = 0; j < old.length; j += 1) {
145175
old[j].parentNode.removeChild(old[j]);
146176
}
147177
for (var k = 0; k < labels.length; k += 1) {
148-
var button = document.createElement("button");
149-
button.textContent = labels[k];
150-
roots[i].appendChild(button);
178+
roots[i].appendChild(makeTab(labels[k], false));
179+
if (mirror) {
180+
mirror.appendChild(makeTab(labels[k], true));
181+
}
151182
}
152183
roots[i].style.setProperty("--hf-tab-count", String(labels.length));
153184
}
@@ -190,17 +221,28 @@
190221
background: var(--hf-tab-fill, #18181b);
191222
transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1)));
192223
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
224+
overflow: hidden;
225+
z-index: 2;
193226
}
194227
.hf-transition-tabs-slide-indicator button {
195228
position: relative;
196229
z-index: 1;
197230
height: 36px;
198231
border: 0;
199232
background: transparent;
200-
color: #71717a;
233+
color: #6b6b73;
201234
font-weight: 850;
202235
}
203-
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
236+
.hf-transition-tabs-slide-indicator .indicator-labels {
237+
position: absolute;
238+
top: 0;
239+
left: 0;
240+
display: grid;
241+
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px));
242+
transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1) * -1));
243+
pointer-events: none;
244+
}
245+
.hf-transition-tabs-slide-indicator .indicator-labels button {
204246
color: #fff;
205247
}
206248
</style>

registry/components/tabs-slide-indicator/tabs-slide-indicator.html

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@
3939
{ "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" }
4040
]'
4141
>
42-
<span class="indicator"></span><button>Plan</button><button>Debug</button><button>Ask</button>
42+
<span class="indicator"
43+
><span class="indicator-labels" aria-hidden="true"
44+
><button tabindex="-1">Plan</button><button tabindex="-1">Debug</button
45+
><button tabindex="-1">Ask</button></span
46+
></span
47+
><button>Plan</button><button>Debug</button><button>Ask</button>
4348
</div>
4449

4550
<style>
@@ -64,17 +69,32 @@
6469
background: var(--hf-tab-fill, #18181b);
6570
transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1)));
6671
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
72+
overflow: hidden;
73+
z-index: 2;
6774
}
6875
.hf-transition-tabs-slide-indicator button {
6976
position: relative;
7077
z-index: 1;
7178
height: 36px;
7279
border: 0;
7380
background: transparent;
74-
color: #71717a;
81+
color: #6b6b73;
7582
font-weight: 850;
7683
}
77-
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
84+
/* The pill carries a clipped copy of the label row. Countering the pill's
85+
own translate keeps that copy aligned with the buttons underneath at
86+
every frame, so the white label is exactly the part of the row the pill
87+
covers - it follows the indicator instead of being pinned to one tab. */
88+
.hf-transition-tabs-slide-indicator .indicator-labels {
89+
position: absolute;
90+
top: 0;
91+
left: 0;
92+
display: grid;
93+
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px));
94+
transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1) * -1));
95+
pointer-events: none;
96+
}
97+
.hf-transition-tabs-slide-indicator .indicator-labels button {
7898
color: #fff;
7999
}
80100
</style>
@@ -104,6 +124,17 @@
104124
return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;
105125
}
106126

127+
// The mirrored copy is decorative: it repeats a label the row already
128+
// carries, so it stays out of the tab order.
129+
function makeTab(text, mirrored) {
130+
var button = document.createElement("button");
131+
button.textContent = text;
132+
if (mirrored) {
133+
button.tabIndex = -1;
134+
}
135+
return button;
136+
}
137+
107138
var accent = accents[pick(accents, vars.accent, "ink")];
108139
var width = widths[pick(widths, vars.width, "standard")];
109140
var radius = shapes[pick(shapes, vars.shape, "pill")];
@@ -127,14 +158,18 @@
127158
roots[i].style.setProperty("--hf-tab-radius", radius);
128159
// A single tab has nowhere to slide to, so it is left as authored.
129160
if (labels.length > 1) {
161+
// Every label exists twice: once in the row and once in the clipped
162+
// copy the pill carries, so both have to be rebuilt together.
163+
var mirror = roots[i].querySelector(".indicator-labels");
130164
var old = roots[i].querySelectorAll("button");
131165
for (var j = 0; j < old.length; j += 1) {
132166
old[j].parentNode.removeChild(old[j]);
133167
}
134168
for (var k = 0; k < labels.length; k += 1) {
135-
var button = document.createElement("button");
136-
button.textContent = labels[k];
137-
roots[i].appendChild(button);
169+
roots[i].appendChild(makeTab(labels[k], false));
170+
if (mirror) {
171+
mirror.appendChild(makeTab(labels[k], true));
172+
}
138173
}
139174
roots[i].style.setProperty("--hf-tab-count", String(labels.length));
140175
}

0 commit comments

Comments
 (0)