Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3bae755
feat(Tabs): initial implementation
KamilEmeleev Jun 29, 2026
0ae44f0
fix(Tabs): add transparency mask under close button
KamilEmeleev Jun 30, 2026
61149d2
refactor(Tab): simplify component
KamilEmeleev Jun 30, 2026
399d0f6
feat(Tabs): add transparency mask under next and previous buttons
KamilEmeleev Jun 30, 2026
0133899
refactor(Tabs): colocate tab styles with subcomponents
KamilEmeleev Jun 30, 2026
814d10a
fix(Tab): fix css-layout for onlyIcon state
KamilEmeleev Jul 1, 2026
56fe2ff
fix(Tabs): sync overflow attributes with orientation
KamilEmeleev Jul 1, 2026
63b957c
fix(Tabs): improve CSS-layout for the add-button
KamilEmeleev Jul 1, 2026
c3e6d46
fix(Tab): fix css-layout for onlyIcon state (round 2)
KamilEmeleev Jul 1, 2026
c06f3c6
fix(TabScrollButton): improve css-layout
KamilEmeleev Jul 1, 2026
6e2e256
fix(TabAddButton): improve css-layout
KamilEmeleev Jul 1, 2026
7f38994
docs(Tabs): improve Editable story
KamilEmeleev Jul 1, 2026
03758ec
fix(Tabs): disable add button with tabs
KamilEmeleev Jul 1, 2026
a826890
refactor(Tab): simplify a close-button
KamilEmeleev Jul 1, 2026
235f55f
fix(Tab): change data-closable to data-allows-removing
KamilEmeleev Jul 1, 2026
f47feac
fix(Tab): simplify css-layout for underlined tabs
KamilEmeleev Jul 1, 2026
e3bc5eb
fix(Tabs): show scroll mask in vertical orientation while scrolling
KamilEmeleev Jul 1, 2026
015eee1
refactor(Tabs): derive scroll buttons from overflow state
KamilEmeleev Jul 1, 2026
fea73c0
fix(Tabs): skip scroll correction without overflow
KamilEmeleev Jul 1, 2026
f9c63b5
fix(Tabs): align add button with tab list in vertical orientation
KamilEmeleev Jul 1, 2026
800f5ba
fix(Tab): restore underlined tab styles
KamilEmeleev Jul 1, 2026
d2f3b39
fix(Tabs): prevent false scroll in underlined tabs
KamilEmeleev Jul 1, 2026
902240d
docs(Tabs): simplify editable tabs description
KamilEmeleev Jul 1, 2026
b0c7285
fix(Tabs): add padding for add button in vertical orientation
KamilEmeleev Jul 2, 2026
48e0140
refactor(Tabs): add `overflow: clip` for underlined tabs
KamilEmeleev Jul 2, 2026
865274b
fix(Tabs): restore disabled state for underlined tabs
KamilEmeleev Jul 2, 2026
bc8c997
docs(Tabs): add EditablePlayground story
KamilEmeleev Jul 2, 2026
6ab1229
docs(Tabs): improve Editable story
KamilEmeleev Jul 2, 2026
0c7492b
chore(Tabs): approve api
KamilEmeleev Jul 2, 2026
43076ca
fix(Tabs): fix css-layout for underlined tabs
KamilEmeleev Jul 2, 2026
43e7a21
fix(Tabs): code review
KamilEmeleev Jul 2, 2026
64bd06c
fix(TabAddButton): extend TabAddButtonProps type
KamilEmeleev Jul 2, 2026
60ffcb7
chore(Tabs): approve api
KamilEmeleev Jul 2, 2026
ab15ef5
docs(Tabs): add Keyboard Activation story
KamilEmeleev Jul 3, 2026
fd5a644
fix(Tabs): keep selected tab close button visible
KamilEmeleev Jul 3, 2026
7379f0c
feat(Tabs): improve vertical scroll styling
KamilEmeleev Jul 3, 2026
7a8d1b3
chore: remove .claude/launch.json
KamilEmeleev Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/components/src/components/Tabs/Tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ You can render tabs dynamically by using `items` prop.

<Story of={Stories.Dynamic} />

## Editable

Editable tabs are controlled from outside. Keep the tab list in your app state,
pass it to the `items` prop, and update it when `onAdd` or `onRemove` is called.

Tabs can be removed with the close button, <kbd>Delete</kbd> or <kbd>Backspace</kbd>.

To see editable tabs in different states, open the [Editable Playground](?path=/story/components-tabs--editable-playground) story.

<Story of={Stories.Editable} />

## Vertical

Set the tabs to vertical by passing `orientation="vertical"` to `Tabs`.
Expand Down Expand Up @@ -104,6 +115,13 @@ You can use the `onSelectionChange` and `selectedKey` props to control the selec

<Story of={Stories.Controlled} />

## Keyboard Activation

Set `keyboardActivation="manual"` to move focus with arrow keys without selecting
the focused tab until the user presses <kbd>Enter</kbd> or <kbd>Space</kbd>.

<Story of={Stories.KeyboardActivation} />

## Links

Tabs items can be rendered as links by passing the `href` prop to the `Tab` component.
Expand Down
252 changes: 93 additions & 159 deletions packages/components/src/components/Tabs/Tabs.module.css
Original file line number Diff line number Diff line change
@@ -1,30 +1,56 @@
@import url('../../styles/mixins.css');
.container {
display: flex;
gap: var(--kbq-size-m);
}

.base {
display: flex;
position: relative;
}

/* container */
.container {
.scrollArea {
display: flex;
gap: var(--kbq-size-m);
position: relative;
flex: 0 1 auto;
min-inline-size: 0;
}

.default {
.tab {
min-inline-size: 40px;
.scrollBox {
--mask-size: 32px;

&.selected .content {
background-color: var(--kbq-states-background-transparent-active);
}
&[data-overflow-inline-start] {
mask-image: linear-gradient(
to right,
transparent var(--mask-size),
#000 calc(var(--mask-size) * 1.5)
);
}

&.hovered .content {
background-color: var(--kbq-states-background-transparent-hover);
}
&[data-overflow-inline-end] {
mask-image: linear-gradient(
to right,
#000 calc(100% - var(--mask-size) * 1.5),
transparent calc(100% - var(--mask-size))
);
}

&[data-overflow-inline-start][data-overflow-inline-end] {
mask-image: linear-gradient(
to right,
transparent var(--mask-size),
#000 calc(var(--mask-size) * 1.5),
#000 calc(100% - var(--mask-size) * 1.5),
transparent calc(100% - var(--mask-size))
);
}
}

.tabList {
flex-grow: 1;
display: flex;
position: relative;
}

.underlined {
.base::after {
content: '';
Expand All @@ -33,61 +59,11 @@
position: absolute;
block-size: var(--kbq-size-border-width);
background: var(--kbq-line-contrast-less);
z-index: var(--kbq-layer-absolute);
}

.tab {
--tab-content-offset: var(--kbq-size-m);

min-inline-size: 40px;
box-sizing: content-box;
padding-block: var(--kbq-size-s);
border-inline: var(--tab-content-offset) solid transparent;

.content {
padding-inline: var(--tab-content-offset);
margin-inline: calc(-1 * var(--tab-content-offset));
inline-size: calc(100% + calc(2 * var(--tab-content-offset)));
}

&::after {
content: '';
position: absolute;
inline-size: 100%;
block-size: 3px;
inset-block-end: 0;
inset-block-start: unset;
border-radius: 2px 2px 0 0;
background-color: transparent;
z-index: calc(var(--kbq-layer-absolute) + 1);
}
}

.tab.onlyIcon {
--tab-content-offset: 0px;

min-inline-size: 32px;
border-inline: var(--kbq-size-xs) solid transparent;
}

.tab:first-child {
border-inline-start: 0;
}

.tab:last-child {
border-inline-end: 0;
}

.tab.selected {
&::after {
background-color: var(--kbq-line-contrast);
}
z-index: calc(var(--kbq-layer-absolute) + 3);
}

.tab.hovered {
&::after {
background-color: var(--kbq-line-contrast-fade);
}
.tabList {
overflow: clip;
}
}

Expand All @@ -104,121 +80,79 @@
.tabList {
flex-direction: row;
}

.tab .content {
justify-content: center;
}
}

.vertical {
.scrollBox {
display: inline-block;
inline-size: auto;
scrollbar-width: auto;
overflow: hidden auto;
.base {
flex-direction: column;
min-block-size: 0;
flex-shrink: 0;
}

.tabList {
flex-direction: column;
.scrollArea {
inline-size: 100%;
min-block-size: 0;
}

.tab {
.scrollBox {
--mask-size: var(--kbq-size-3xl);

inline-size: 100%;
padding-block: var(--kbq-size-xxs);
display: inline-block;
overflow: hidden auto;
scrollbar-width: auto;
scrollbar-gutter: auto;
scrollbar-color: var(--kbq-scrollbar-thumb-default-background)
var(--kbq-background-transparent);

&[data-overflow-block-start][data-overflow-block-end] {
mask-image: linear-gradient(
to bottom,
transparent 0,
#000 var(--mask-size),
#000 calc(100% - var(--mask-size)),
transparent 100%
);
}

.content {
justify-content: flex-start;
&[data-overflow-block-start]:not([data-overflow-block-end]) {
mask-image: linear-gradient(
to bottom,
transparent 0,
#000 var(--mask-size)
);
}
}

.tab:first-child {
padding-block-start: 0;
&:not([data-overflow-block-start])[data-overflow-block-end] {
mask-image: linear-gradient(
to bottom,
#000 calc(100% - var(--mask-size)),
transparent 100%
);
}
}

.tab:last-child {
padding-block-end: 0;
.tabList {
flex-direction: column;
}

.label {
min-inline-size: 0;
[data-slot='add-button'] {
padding-block-start: var(--kbq-size-s);
}

@mixin ellipsis;
&:has(.tabList:empty) {
[data-slot='add-button'] {
padding-block-start: 0;
}
}
}

.stretched {
.scrollBox {
overflow: hidden;
}

.tab {
.scrollArea {
inline-size: 100%;
}

.label {
min-inline-size: 0;

@mixin ellipsis;
}
}

/* tab-list */
.tabList {
flex-grow: 1;
display: flex;
position: relative;
}

/* tab */
.tab {
display: flex;
outline: none;
cursor: pointer;
position: relative;
align-items: stretch;
text-decoration: none;
box-sizing: border-box;
justify-content: center;
transition: background-color var(--kbq-transition-default);

&::after {
transition: background-color var(--kbq-transition-default);
.scrollBox {
overflow: hidden;
}
}

.focusVisible .content {
outline-color: var(--kbq-states-line-focus-theme);
}

.disabled .content {
color: var(--kbq-states-foreground-disabled);
cursor: default;
}

.content {
display: flex;
gap: var(--kbq-size-xxs);
align-items: center;
overflow: hidden;
inline-size: 100%;
white-space: nowrap;
box-sizing: border-box;
min-block-size: 32px;
padding-inline: var(--kbq-size-m);
padding-block: var(--kbq-size-xs);
color: var(--kbq-foreground-contrast);
border-radius: var(--kbq-size-border-radius);
outline-offset: calc(-1 * var(--kbq-size-3xs));
outline: var(--kbq-size-3xs) solid;
outline-color: transparent;
transition:
outline-color var(--kbq-transition-default),
background-color var(--kbq-transition-default),
color var(--kbq-transition-default);
}

.addon {
flex-shrink: 0;
display: flex;
align-items: center;
}
Loading
Loading