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 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. 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 With the defaults, this renders three 86px tabs in a light pill track. The
a light pill track, a near-black pill sliding from the first to the second. near-black pill and white active label move from the first tab to the second.
--> -->
<div <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" } { "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>
<style> <style>
.hf-transition-tabs-slide-indicator { .hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px; --hf-tab-x: 0px;
--hf-tab-offset: calc(var(--hf-tab-x) * var(--hf-tab-step, 1));
position: relative; position: relative;
display: inline-grid; display: inline-grid;
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px)); 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; height: 36px;
border-radius: var(--hf-tab-radius, 999px); border-radius: var(--hf-tab-radius, 999px);
background: var(--hf-tab-fill, #18181b); 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); box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
} }
.hf-transition-tabs-slide-indicator button { .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; position: relative;
z-index: 1; z-index: 1;
height: 36px; height: 36px;
border: 0; border: 0;
background: transparent; background-color: transparent;
color: #71717a; 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; font-weight: 850;
} }
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style> </style>
<script> <script>
@@ -150,6 +170,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
for (var k = 0; k < labels.length; k += 1) { for (var k = 0; k < labels.length; k += 1) {
var button = document.createElement("button"); var button = document.createElement("button");
button.textContent = labels[k]; button.textContent = labels[k];
button.style.setProperty("--hf-tab-index", String(k));
roots[i].appendChild(button); roots[i].appendChild(button);
} }
roots[i].style.setProperty("--hf-tab-count", String(labels.length)); roots[i].style.setProperty("--hf-tab-count", String(labels.length));
@@ -160,7 +181,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<!-- <!--
Timeline integration: 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 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. 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 With the defaults, this renders three 86px tabs in a light pill track. The
a light pill track, a near-black pill sliding from the first to the second. near-black pill and white active label move from the first tab to the second.
--> -->
<div <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" } { "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>
<style> <style>
.hf-transition-tabs-slide-indicator { .hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px; --hf-tab-x: 0px;
--hf-tab-offset: calc(var(--hf-tab-x) * var(--hf-tab-step, 1));
position: relative; position: relative;
display: inline-grid; display: inline-grid;
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px)); 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; height: 36px;
border-radius: var(--hf-tab-radius, 999px); border-radius: var(--hf-tab-radius, 999px);
background: var(--hf-tab-fill, #18181b); 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); box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
} }
.hf-transition-tabs-slide-indicator button { .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; position: relative;
z-index: 1; z-index: 1;
height: 36px; height: 36px;
border: 0; border: 0;
background: transparent; background-color: transparent;
color: #71717a; 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; font-weight: 850;
} }
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style> </style>
<script> <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) { for (var k = 0; k < labels.length; k += 1) {
var button = document.createElement("button"); var button = document.createElement("button");
button.textContent = labels[k]; button.textContent = labels[k];
button.style.setProperty("--hf-tab-index", String(k));
roots[i].appendChild(button); roots[i].appendChild(button);
} }
roots[i].style.setProperty("--hf-tab-count", String(labels.length)); 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: 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-transition-tabs-slide-indicator {
--hf-tab-x: 0px; --hf-tab-x: 0px;
--hf-tab-offset: var(--hf-tab-x);
position: relative; position: relative;
display: inline-grid; display: inline-grid;
grid-template-columns: repeat(3, 86px); grid-template-columns: repeat(3, 86px);
@@ -52,21 +53,36 @@
height: 36px; height: 36px;
border-radius: 999px; border-radius: 999px;
background: #18181b; 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); box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
} }
.hf-transition-tabs-slide-indicator button { .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; position: relative;
z-index: 1; z-index: 1;
height: 36px; height: 36px;
border: 0; border: 0;
background: transparent; background-color: transparent;
color: #71717a; 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; font-weight: 600;
} }
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style> </style>
</head> </head>
<body> <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" } { "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 <span class="indicator"></span><button style="--hf-tab-index: 0">Plan</button
><button>Ask</button> ><button style="--hf-tab-index: 1">Debug</button
><button style="--hf-tab-index: 2">Ask</button>
</div> </div>
</div> </div>
<script> <script>
@@ -147,6 +164,7 @@
for (var k = 0; k < labels.length; k += 1) { for (var k = 0; k < labels.length; k += 1) {
var button = document.createElement("button"); var button = document.createElement("button");
button.textContent = labels[k]; button.textContent = labels[k];
button.style.setProperty("--hf-tab-index", String(k));
roots[i].appendChild(button); roots[i].appendChild(button);
} }
roots[i].style.setProperty("--hf-tab-count", String(labels.length)); roots[i].style.setProperty("--hf-tab-count", String(labels.length));
@@ -158,12 +176,11 @@
const tl = gsap.timeline({ paused: true }); const tl = gsap.timeline({ paused: true });
const startTime = 0.5; const startTime = 0.5;
tl.fromTo( tl.fromTo(
".hf-transition-tabs-slide-indicator .indicator", ".hf-transition-tabs-slide-indicator",
{ "--hf-tab-x": "0px" }, { "--hf-tab-x": "0px" },
{ "--hf-tab-x": "86px", duration: 0.42, ease: "power3.inOut" }, { "--hf-tab-x": "86px", duration: 0.42, ease: "power3.inOut" },
startTime, startTime,
); );
tl.set({}, {}, 5);
window.__timelines = window.__timelines || {}; window.__timelines = window.__timelines || {};
window.__timelines["tabs-slide-indicator-demo"] = tl; window.__timelines["tabs-slide-indicator-demo"] = tl;
</script> </script>
@@ -171,6 +188,7 @@
<style> <style>
.hf-transition-tabs-slide-indicator { .hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px; --hf-tab-x: 0px;
--hf-tab-offset: calc(var(--hf-tab-x) * var(--hf-tab-step, 1));
position: relative; position: relative;
display: inline-grid; display: inline-grid;
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px)); grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px));
@@ -188,21 +206,39 @@
height: 36px; height: 36px;
border-radius: var(--hf-tab-radius, 999px); border-radius: var(--hf-tab-radius, 999px);
background: var(--hf-tab-fill, #18181b); 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); box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
} }
.hf-transition-tabs-slide-indicator button { .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; position: relative;
z-index: 1; z-index: 1;
height: 36px; height: 36px;
border: 0; border: 0;
background: transparent; background-color: transparent;
color: #71717a; 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; font-weight: 850;
} }
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style> </style>
</body> </body>
</html> </html>
@@ -26,8 +26,8 @@
commas. Two or more names rebuild the row, one name or none leaves the 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. 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 With the defaults, this renders three 86px tabs in a light pill track. The
a light pill track, a near-black pill sliding from the first to the second. near-black pill and white active label move from the first tab to the second.
--> -->
<div <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" } { "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>
<style> <style>
.hf-transition-tabs-slide-indicator { .hf-transition-tabs-slide-indicator {
--hf-tab-x: 0px; --hf-tab-x: 0px;
--hf-tab-offset: calc(var(--hf-tab-x) * var(--hf-tab-step, 1));
position: relative; position: relative;
display: inline-grid; display: inline-grid;
grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px)); grid-template-columns: repeat(var(--hf-tab-count, 3), var(--hf-tab-width, 86px));
@@ -62,21 +64,39 @@
height: 36px; height: 36px;
border-radius: var(--hf-tab-radius, 999px); border-radius: var(--hf-tab-radius, 999px);
background: var(--hf-tab-fill, #18181b); 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); box-shadow: 0 12px 26px rgba(0, 0, 0, 0.18);
} }
.hf-transition-tabs-slide-indicator button { .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; position: relative;
z-index: 1; z-index: 1;
height: 36px; height: 36px;
border: 0; border: 0;
background: transparent; background-color: transparent;
color: #71717a; 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; font-weight: 850;
} }
.hf-transition-tabs-slide-indicator button:nth-of-type(2) {
color: #fff;
}
</style> </style>
<script> <script>
@@ -134,6 +154,7 @@
for (var k = 0; k < labels.length; k += 1) { for (var k = 0; k < labels.length; k += 1) {
var button = document.createElement("button"); var button = document.createElement("button");
button.textContent = labels[k]; button.textContent = labels[k];
button.style.setProperty("--hf-tab-index", String(k));
roots[i].appendChild(button); roots[i].appendChild(button);
} }
roots[i].style.setProperty("--hf-tab-count", String(labels.length)); roots[i].style.setProperty("--hf-tab-count", String(labels.length));
@@ -144,5 +165,5 @@
<!-- <!--
Timeline integration: 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);
--> -->