feat(tabs): enhance tab styles and improve active code tab handling (#712)

* feat(tabs): enhance tab styles with sliding indicators and hover effects

* feat(tabs): improve active code tab handling and initialization logic
This commit is contained in:
Cell
2026-03-12 11:41:52 +08:00
committed by GitHub
parent f2c112e402
commit c8d028aa0e
4 changed files with 164 additions and 38 deletions
+13
View File
@@ -40,6 +40,19 @@ def sum(a, b):
print(sum(1, 2))
```
## Active Tab
```js {group=tab-active name="Inactive Tab"}
function greet(name) {
return `Hello, ${name}!`
}
```
```python {group=tab-active name="Active Tab" .active}
def greet(name):
return f"Hello, {name}!"
```
## Mode
```ts {group=tab-mode name="Classic"}
+26 -13
View File
@@ -590,27 +590,40 @@ pre {
.tabs-items {
display: flex;
overflow-x: auto;
position: relative;
@include border-radius(top);
.tab-item {
padding: 0.4rem 0.8rem;
cursor: pointer;
font-weight: bold;
color: fixit-var(secondary);
white-space: nowrap;
transition: all 0.2s;
user-select: none;
&:hover {
color: fixit-var(global-font-color);
background-color: rgba(128, 128, 128, 0.05);
}
&.active {
color: fixit-var(global-font-color);
box-shadow: inset 0 -2px 0 fixit-var(code-tab-active-border-color, fixit-var(primary));
background-color: fixit-var(code-block-background-color);
}
@include border-radius;
// mobile style
// padding-inline: 0.6rem;
}
&:not(:has(.tab-item:hover)) .tab-item.active,
.tab-item:hover {
anchor-name: #{$rootPrefix}tab-hover;
color: fixit-var(global-font-color);
}
&::before {
content: '';
position-anchor: #{$rootPrefix}tab-hover;
position: absolute;
inset: anchor(inside);
translate: 2px 2px;
width: calc(anchor-size(width) - 0.25rem);
height: calc(anchor-size(height) - 0.25rem);
box-shadow: inset 0 0 1rem 0.25rem color-mix(in srgb, fixit-var(secondary) 30%, transparent);
pointer-events: none;
transition: 0.3s ease-in-out;
@include border-radius;
}
}
+114 -20
View File
@@ -107,17 +107,15 @@ tab-container {
// ========================================
// Style 1: Underline Style (Default)
// Clean, minimal with bottom border
// Clean, minimal with sliding indicator
// ========================================
&[type='underline'] {
> .tab-button {
@include set-fixit-var(tab-button-border-color, transparent);
@include tab-button-size(0);
&[aria-selected='true'],
&[aria-selected='false']:hover {
color: fixit-var(primary);
@include set-fixit-var(tab-button-border-color, fixit-var(primary));
}
&[aria-selected='false']:hover {
@@ -125,6 +123,21 @@ tab-container {
}
}
// Anchor tracking: follows hover, rests on active tab
&:not(:has(> .tab-button:hover)) > .tab-button[aria-selected='true'],
> .tab-button:hover {
anchor-name: #{$rootPrefix}tab-underline-active;
}
// Sliding box-shadow indicator via anchor positioning
&::part(tablist-tab-wrapper)::before {
content: '';
position: absolute;
position-anchor: #{$rootPrefix}tab-underline-active;
inset: anchor(inside);
pointer-events: none;
}
&[placement='top'],
&[placement='bottom'] {
gap: 0.5rem;
@@ -137,6 +150,10 @@ tab-container {
overflow-x: scroll;
overflow-y: hidden;
}
&::part(tablist-tab-wrapper)::before {
transition: all 0.3s ease-in-out, top, bottom 0s;
}
}
&[placement='left'],
@@ -144,17 +161,21 @@ tab-container {
> .tab-button {
@include tab-button-size(1rem, 0);
}
&::part(tablist-tab-wrapper)::before {
transition: all 0.3s ease-in-out, left, right 0s;
}
}
// Top placement
&[placement='top'] {
&::part(tablist-tab-wrapper) {
border-bottom: 2px solid fixit-var(global-border-color);
}
> .tab-button {
translate: 0 2px;
border-bottom: 2px solid fixit-var(tab-button-border-color);
&::before {
box-shadow: inset 0 -2px 0 fixit-var(primary);
translate: 0 2px;
}
}
}
@@ -162,11 +183,11 @@ tab-container {
&[placement='bottom'] {
&::part(tablist-tab-wrapper) {
border-top: 2px solid fixit-var(global-border-color);
}
> .tab-button {
translate: 0 -2px;
border-top: 2px solid fixit-var(tab-button-border-color);
&::before {
box-shadow: inset 0 2px 0 fixit-var(primary);
translate: 0 -2px;
}
}
}
@@ -174,12 +195,15 @@ tab-container {
&[placement='left'] {
&::part(tablist-tab-wrapper) {
border-right: 2px solid fixit-var(global-border-color);
&::before {
box-shadow: inset -2px 0 0 fixit-var(primary);
translate: 2px 0;
}
}
> .tab-button {
text-align: right;
translate: 2px 0;
border-right: 2px solid fixit-var(tab-button-border-color);
}
}
@@ -187,12 +211,15 @@ tab-container {
&[placement='right'] {
&::part(tablist-tab-wrapper) {
border-left: 2px solid fixit-var(global-border-color);
&::before {
box-shadow: inset 2px 0 0 fixit-var(primary);
translate: -2px 0;
}
}
> .tab-button {
text-align: left;
translate: -2px 0;
border-left: 2px solid fixit-var(tab-button-border-color);
}
}
}
@@ -451,8 +478,10 @@ tab-container {
}
> .tab-button {
position: relative;
font-weight: 400;
flex-grow: 1;
@include z-index(base);
@include tab-button-size;
@include border-radius;
@@ -463,12 +492,26 @@ tab-container {
}
&[aria-selected='true'] {
background-color: fixit-var(global-background-color);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
anchor-name: #{$rootPrefix}tab-segment-active;
background-color: transparent;
box-shadow: none;
}
}
[data-theme=dark] & {
box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1);
}
// Sliding active indicator via anchor positioning
&::part(tablist-tab-wrapper)::before {
content: '';
position: absolute;
pointer-events: none;
position-anchor: #{$rootPrefix}tab-segment-active;
inset: anchor(inside);
background-color: fixit-var(global-background-color);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
transition: all 0.15s ease, top, bottom 0s;
@include border-radius;
[data-theme=dark] & {
box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1);
}
}
}
@@ -531,3 +574,54 @@ tab-container {
tab-container .tab-panel:not([hidden]) {
animation: tabSlideIn 0.3s ease-out;
}
// Firefox fallback: `::part(...)::before` is not supported.
// We use a CSS variable to control the border color of the active tab button,
// and apply the sliding effect directly on the button itself.
@supports (-moz-appearance: none) {
tab-container {
&[type='underline'] {
> .tab-button {
@include set-fixit-var(tab-button-border-color, transparent);
&[aria-selected='true'],
&[aria-selected='false']:hover {
@include set-fixit-var(tab-button-border-color, fixit-var(primary));
}
}
&[placement='top'] > .tab-button {
translate: 0 2px;
border-bottom: 2px solid fixit-var(tab-button-border-color);
}
&[placement='bottom'] > .tab-button {
translate: 0 -2px;
border-top: 2px solid fixit-var(tab-button-border-color);
}
&[placement='left'] > .tab-button {
translate: 2px 0;
border-right: 2px solid fixit-var(tab-button-border-color);
}
&[placement='right'] > .tab-button {
translate: -2px 0;
border-left: 2px solid fixit-var(tab-button-border-color);
}
}
&[type='segment'] {
> .tab-button {
&[aria-selected='true'] {
background-color: fixit-var(global-background-color);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
[data-theme=dark] & {
box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1);
}
}
}
}
}
}
+11 -5
View File
@@ -640,13 +640,15 @@ class FixIt {
const $firstBlock = $tabs[0];
$firstBlock.parentNode.insertBefore($container, $firstBlock);
const activeTabIndex = $tabs.findIndex(tab => tab.classList.contains('active'));
$tabs.forEach(($tab, index) => {
const title = $tab.dataset.tabTitle || 'Code';
const defaultActiveTab = activeTabIndex === -1 && index === 0;
// tab button
const $btn = document.createElement('span');
$btn.className = 'tab-item';
if (index === 0) $btn.classList.add('active');
if (defaultActiveTab) $btn.classList.add('active');
$btn.textContent = title;
$btn.dataset.index = index;
$btn.title = title;
@@ -678,8 +680,7 @@ class FixIt {
$items.appendChild($btn);
// move block to content
$tab.classList.toggle('active', index === 0);
// remove is-collapsed class
$tab.classList.toggle('active', activeTabIndex === index || defaultActiveTab);
$tab.classList.remove('is-collapsed');
$content.appendChild($tab);
});
@@ -687,8 +688,13 @@ class FixIt {
$container.appendChild($header);
$container.appendChild($content);
// initialize actions for first tab
$items.firstElementChild.click();
// initialize actions for the active tab
if (activeTabIndex !== -1) {
const $activeBtn = $items.querySelector(`.tab-item[data-index="${activeTabIndex}"]`);
if ($activeBtn) $activeBtn.click();
} else {
$items.firstElementChild.click();
}
});
}