fix(catalog): sync tab labels with the sliding indicator (#3267)

* fix(catalog): sync tab labels with the sliding indicator

* fix(catalog): preserve tab contrast during indicator motion
This commit is contained in:
Akshar Patel
2026-09-01 20:40:11 -04:00
committed by GitHub
parent db92f8ab59
commit 305de15432
4 changed files with 146 additions and 47 deletions
@@ -42,8 +42,8 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
commas. Two or more names rebuild the row, one name or none leaves the
authored markup alone, since a single tab has nowhere to slide to.
On every default the result is identical to the original: three 86px tabs in
a light pill track, a near-black pill sliding from the first to the second.
With the defaults, this renders three 86px tabs in a light pill track. The
near-black pill and white active label move from the first tab to the second.
-->
<div
@@ -55,12 +55,14 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
{ "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" }
]'
>
<span class="indicator"></span><button>Plan</button><button>Debug</button><button>Ask</button>
<span class="indicator"></span><button style="--hf-tab-index: 0">Plan</button
><button style="--hf-tab-index: 1">Debug</button><button style="--hf-tab-index: 2">Ask</button>
</div>
<style>
.hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px;
--hf-tab-offset: calc(var(--hf-tab-x) * var(--hf-tab-step, 1));
position: relative;
display: inline-grid;
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px));
@@ -78,21 +80,39 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
height: 36px;
border-radius: var(--hf-tab-radius, 999px);
background: var(--hf-tab-fill, #18181b);
transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1)));
transform: translateX(var(--hf-tab-offset));
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
}
.hf-transition-tabs-slide-indicator button {
--hf-tab-highlight-start: clamp(
0px,
calc(var(--hf-tab-offset) - var(--hf-tab-index) * var(--hf-tab-width, 86px)),
var(--hf-tab-width, 86px)
);
--hf-tab-highlight-end: clamp(
0px,
calc(
var(--hf-tab-offset) + var(--hf-tab-width, 86px) - var(--hf-tab-index) *
var(--hf-tab-width, 86px)
),
var(--hf-tab-width, 86px)
);
position: relative;
z-index: 1;
height: 36px;
border: 0;
background: transparent;
color: #71717a;
background-color: transparent;
background-image: linear-gradient(
to right,
#6f6f78 0 var(--hf-tab-highlight-start),
#fff var(--hf-tab-highlight-start) var(--hf-tab-highlight-end),
#6f6f78 var(--hf-tab-highlight-end) 100%
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-weight: 850;
}
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style>
<script>
@@ -150,6 +170,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
for (var k = 0; k < labels.length; k += 1) {
var button = document.createElement("button");
button.textContent = labels[k];
button.style.setProperty("--hf-tab-index", String(k));
roots[i].appendChild(button);
}
roots[i].style.setProperty("--hf-tab-count", String(labels.length));
@@ -160,7 +181,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<!--
Timeline integration:
tl.fromTo('.hf-transition-tabs-slide-indicator .indicator', { '--hf-tab-x': '0px' }, { '--hf-tab-x': '86px', duration: 0.42, ease: 'power3.inOut' }, startTime);
tl.fromTo('.hf-transition-tabs-slide-indicator', { '--hf-tab-x': '0px' }, { '--hf-tab-x': '86px', duration: 0.42, ease: 'power3.inOut' }, startTime);
-->
```
@@ -228,8 +249,8 @@ defaults, so this behaves exactly like the preview above until you change one:
commas. Two or more names rebuild the row, one name or none leaves the
authored markup alone, since a single tab has nowhere to slide to.
On every default the result is identical to the original: three 86px tabs in
a light pill track, a near-black pill sliding from the first to the second.
With the defaults, this renders three 86px tabs in a light pill track. The
near-black pill and white active label move from the first tab to the second.
-->
<div
@@ -241,12 +262,14 @@ defaults, so this behaves exactly like the preview above until you change one:
{ "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" }
]'
>
<span class="indicator"></span><button>Plan</button><button>Debug</button><button>Ask</button>
<span class="indicator"></span><button style="--hf-tab-index: 0">Plan</button
><button style="--hf-tab-index: 1">Debug</button><button style="--hf-tab-index: 2">Ask</button>
</div>
<style>
.hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px;
--hf-tab-offset: calc(var(--hf-tab-x) * var(--hf-tab-step, 1));
position: relative;
display: inline-grid;
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px));
@@ -264,21 +287,39 @@ defaults, so this behaves exactly like the preview above until you change one:
height: 36px;
border-radius: var(--hf-tab-radius, 999px);
background: var(--hf-tab-fill, #18181b);
transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1)));
transform: translateX(var(--hf-tab-offset));
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
}
.hf-transition-tabs-slide-indicator button {
--hf-tab-highlight-start: clamp(
0px,
calc(var(--hf-tab-offset) - var(--hf-tab-index) * var(--hf-tab-width, 86px)),
var(--hf-tab-width, 86px)
);
--hf-tab-highlight-end: clamp(
0px,
calc(
var(--hf-tab-offset) + var(--hf-tab-width, 86px) - var(--hf-tab-index) *
var(--hf-tab-width, 86px)
),
var(--hf-tab-width, 86px)
);
position: relative;
z-index: 1;
height: 36px;
border: 0;
background: transparent;
color: #71717a;
background-color: transparent;
background-image: linear-gradient(
to right,
#6f6f78 0 var(--hf-tab-highlight-start),
#fff var(--hf-tab-highlight-start) var(--hf-tab-highlight-end),
#6f6f78 var(--hf-tab-highlight-end) 100%
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-weight: 850;
}
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style>
<script>
@@ -336,6 +377,7 @@ defaults, so this behaves exactly like the preview above until you change one:
for (var k = 0; k < labels.length; k += 1) {
var button = document.createElement("button");
button.textContent = labels[k];
button.style.setProperty("--hf-tab-index", String(k));
roots[i].appendChild(button);
}
roots[i].style.setProperty("--hf-tab-count", String(labels.length));
@@ -346,7 +388,7 @@ defaults, so this behaves exactly like the preview above until you change one:
<!--
Timeline integration:
tl.fromTo('.hf-transition-tabs-slide-indicator .indicator', { '--hf-tab-x': '0px' }, { '--hf-tab-x': '86px', duration: 0.42, ease: 'power3.inOut' }, startTime);
tl.fromTo('.hf-transition-tabs-slide-indicator', { '--hf-tab-x': '0px' }, { '--hf-tab-x': '86px', duration: 0.42, ease: 'power3.inOut' }, startTime);
-->
```
File diff suppressed because one or more lines are too long
+52 -16
View File
@@ -35,6 +35,7 @@
}
.hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px;
--hf-tab-offset: var(--hf-tab-x);
position: relative;
display: inline-grid;
grid-template-columns: repeat(3, 86px);
@@ -52,21 +53,36 @@
height: 36px;
border-radius: 999px;
background: #18181b;
transform: translateX(var(--hf-tab-x));
transform: translateX(var(--hf-tab-offset));
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
}
.hf-transition-tabs-slide-indicator button {
--hf-tab-highlight-start: clamp(
0px,
calc(var(--hf-tab-offset) - var(--hf-tab-index) * 86px),
86px
);
--hf-tab-highlight-end: clamp(
0px,
calc(var(--hf-tab-offset) + 86px - var(--hf-tab-index) * 86px),
86px
);
position: relative;
z-index: 1;
height: 36px;
border: 0;
background: transparent;
color: #71717a;
background-color: transparent;
background-image: linear-gradient(
to right,
#6f6f78 0 var(--hf-tab-highlight-start),
#fff var(--hf-tab-highlight-start) var(--hf-tab-highlight-end),
#6f6f78 var(--hf-tab-highlight-end) 100%
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-weight: 600;
}
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style>
</head>
<body>
@@ -88,8 +104,9 @@
{ "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" }
]'
>
<span class="indicator"></span><button>Plan</button><button>Debug</button
><button>Ask</button>
<span class="indicator"></span><button style="--hf-tab-index: 0">Plan</button
><button style="--hf-tab-index: 1">Debug</button
><button style="--hf-tab-index: 2">Ask</button>
</div>
</div>
<script>
@@ -147,6 +164,7 @@
for (var k = 0; k < labels.length; k += 1) {
var button = document.createElement("button");
button.textContent = labels[k];
button.style.setProperty("--hf-tab-index", String(k));
roots[i].appendChild(button);
}
roots[i].style.setProperty("--hf-tab-count", String(labels.length));
@@ -158,12 +176,11 @@
const tl = gsap.timeline({ paused: true });
const startTime = 0.5;
tl.fromTo(
".hf-transition-tabs-slide-indicator .indicator",
".hf-transition-tabs-slide-indicator",
{ "--hf-tab-x": "0px" },
{ "--hf-tab-x": "86px", duration: 0.42, ease: "power3.inOut" },
startTime,
);
tl.set({}, {}, 5);
window.__timelines = window.__timelines || {};
window.__timelines["tabs-slide-indicator-demo"] = tl;
</script>
@@ -171,6 +188,7 @@
<style>
.hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px;
--hf-tab-offset: calc(var(--hf-tab-x) * var(--hf-tab-step, 1));
position: relative;
display: inline-grid;
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px));
@@ -188,21 +206,39 @@
height: 36px;
border-radius: var(--hf-tab-radius, 999px);
background: var(--hf-tab-fill, #18181b);
transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1)));
transform: translateX(var(--hf-tab-offset));
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
}
.hf-transition-tabs-slide-indicator button {
--hf-tab-highlight-start: clamp(
0px,
calc(var(--hf-tab-offset) - var(--hf-tab-index) * var(--hf-tab-width, 86px)),
var(--hf-tab-width, 86px)
);
--hf-tab-highlight-end: clamp(
0px,
calc(
var(--hf-tab-offset) + var(--hf-tab-width, 86px) - var(--hf-tab-index) *
var(--hf-tab-width, 86px)
),
var(--hf-tab-width, 86px)
);
position: relative;
z-index: 1;
height: 36px;
border: 0;
background: transparent;
color: #71717a;
background-color: transparent;
background-image: linear-gradient(
to right,
#6f6f78 0 var(--hf-tab-highlight-start),
#fff var(--hf-tab-highlight-start) var(--hf-tab-highlight-end),
#6f6f78 var(--hf-tab-highlight-end) 100%
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-weight: 850;
}
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style>
</body>
</html>
@@ -26,8 +26,8 @@
commas. Two or more names rebuild the row, one name or none leaves the
authored markup alone, since a single tab has nowhere to slide to.
On every default the result is identical to the original: three 86px tabs in
a light pill track, a near-black pill sliding from the first to the second.
With the defaults, this renders three 86px tabs in a light pill track. The
near-black pill and white active label move from the first tab to the second.
-->
<div
@@ -39,12 +39,14 @@
{ "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" }
]'
>
<span class="indicator"></span><button>Plan</button><button>Debug</button><button>Ask</button>
<span class="indicator"></span><button style="--hf-tab-index: 0">Plan</button
><button style="--hf-tab-index: 1">Debug</button><button style="--hf-tab-index: 2">Ask</button>
</div>
<style>
.hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px;
--hf-tab-offset: calc(var(--hf-tab-x) * var(--hf-tab-step, 1));
position: relative;
display: inline-grid;
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px));
@@ -62,21 +64,39 @@
height: 36px;
border-radius: var(--hf-tab-radius, 999px);
background: var(--hf-tab-fill, #18181b);
transform: translateX(calc(var(--hf-tab-x) * var(--hf-tab-step, 1)));
transform: translateX(var(--hf-tab-offset));
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
}
.hf-transition-tabs-slide-indicator button {
--hf-tab-highlight-start: clamp(
0px,
calc(var(--hf-tab-offset) - var(--hf-tab-index) * var(--hf-tab-width, 86px)),
var(--hf-tab-width, 86px)
);
--hf-tab-highlight-end: clamp(
0px,
calc(
var(--hf-tab-offset) + var(--hf-tab-width, 86px) - var(--hf-tab-index) *
var(--hf-tab-width, 86px)
),
var(--hf-tab-width, 86px)
);
position: relative;
z-index: 1;
height: 36px;
border: 0;
background: transparent;
color: #71717a;
background-color: transparent;
background-image: linear-gradient(
to right,
#6f6f78 0 var(--hf-tab-highlight-start),
#fff var(--hf-tab-highlight-start) var(--hf-tab-highlight-end),
#6f6f78 var(--hf-tab-highlight-end) 100%
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-weight: 850;
}
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style>
<script>
@@ -134,6 +154,7 @@
for (var k = 0; k < labels.length; k += 1) {
var button = document.createElement("button");
button.textContent = labels[k];
button.style.setProperty("--hf-tab-index", String(k));
roots[i].appendChild(button);
}
roots[i].style.setProperty("--hf-tab-count", String(labels.length));
@@ -144,5 +165,5 @@
<!--
Timeline integration:
tl.fromTo('.hf-transition-tabs-slide-indicator .indicator', { '--hf-tab-x': '0px' }, { '--hf-tab-x': '86px', duration: 0.42, ease: 'power3.inOut' }, startTime);
tl.fromTo('.hf-transition-tabs-slide-indicator', { '--hf-tab-x': '0px' }, { '--hf-tab-x': '86px', duration: 0.42, ease: 'power3.inOut' }, startTime);
-->