Skip to content
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
83195de
feat(tabs): create vertical tabs story
sangeethababu9223 Feb 25, 2026
03a654a
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Feb 27, 2026
468ed11
feat(tabs): panel component
sangeethababu9223 Feb 27, 2026
27ff0b8
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 2, 2026
b4d0066
feat(tabs): vertical tab wrapper component
sangeethababu9223 Mar 3, 2026
45b6de1
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 3, 2026
679cd97
feat(tabs): update vertical tabs component
sangeethababu9223 Mar 3, 2026
91356ef
feat(tabs): update vertical attribute behaviour
sangeethababu9223 Mar 3, 2026
12f62f8
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 5, 2026
d96009d
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 6, 2026
70bf223
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 8, 2026
660c847
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 8, 2026
bbf2b60
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 9, 2026
5f79300
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 9, 2026
278a64e
Merge branch 'feat/tabs-vertical-tab' of https://github.com/sangeetha…
sangeethababu9223 Mar 9, 2026
b44be4a
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 9, 2026
b464220
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 10, 2026
95c5b71
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 13, 2026
d8c6ae3
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 13, 2026
3ea084c
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 18, 2026
843f0fe
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 18, 2026
1fca201
Merge branch 'feat/tabs-vertical-tab' of https://github.com/sangeetha…
sangeethababu9223 Mar 18, 2026
6bcc915
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 20, 2026
56968e5
fix(tab): vertical style update
sangeethababu9223 Mar 20, 2026
5ef1553
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 24, 2026
7e6d2bd
feat(tabs): vertical tab update
sangeethababu9223 Mar 24, 2026
446ec2d
feat(tab): update doc with vertical
sangeethababu9223 Mar 24, 2026
de1c9b0
feat(tab): update doc with vertical
sangeethababu9223 Mar 24, 2026
f20670f
fix(tab): responsive behaviour for vertical tab
sangeethababu9223 Mar 25, 2026
47ada3e
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 25, 2026
a3f7cff
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 25, 2026
b1f3d1d
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 25, 2026
90873e0
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Mar 31, 2026
5616c66
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Apr 2, 2026
9ffd266
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Apr 7, 2026
9a946a1
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Apr 7, 2026
879049c
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 Apr 7, 2026
65af6e2
fix(tab): vertical tabs control updates
sangeethababu9223 Apr 7, 2026
26a136f
Merge branch 'feat/tabs-vertical-tab' of https://github.com/sangeetha…
sangeethababu9223 Apr 7, 2026
276effd
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Apr 13, 2026
947bccb
fix(tabs): resolve conflict
sangeethababu9223 Apr 20, 2026
dc795fd
Merge branch 'feat/tabs-vertical-tab' of https://github.com/sangeetha…
sangeethababu9223 Apr 20, 2026
f7ef63a
fix(tabs): conflict resolve
sangeethababu9223 Apr 21, 2026
2e6fdf9
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 Apr 21, 2026
ad8adc9
fix(tabs): resolve conflict
sangeethababu9223 Apr 28, 2026
6ecb25f
Merge branch 'feat/tabs-vertical-tab' of https://github.com/sangeetha…
sangeethababu9223 Apr 28, 2026
64bd39b
fix(tabs): conflict resolve
sangeethababu9223 May 7, 2026
e3797cf
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 May 8, 2026
bf27d84
fix(tabs): resolve mdx conflict
sangeethababu9223 May 13, 2026
b2ba51f
feat(tabs): vertical tab break point update
sangeethababu9223 May 13, 2026
791ad8f
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 May 14, 2026
2282332
fix(tabs): conflict resolve
sangeethababu9223 May 15, 2026
1a07a6a
fix(tabs): hover style issue fix
sangeethababu9223 May 15, 2026
2e3a417
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 May 18, 2026
c313c99
Merge branch 'main' into feat/tabs-vertical-tab
sangeethababu9223 May 18, 2026
39ba909
Merge remote-tracking branch 'upstream/main' into feat/tabs-vertical-tab
sangeethababu9223 May 19, 2026
29025c6
fix(tabs): stop scroll during navigation
sangeethababu9223 May 19, 2026
89eb8d6
Merge branch 'feat/tabs-vertical-tab' of https://github.com/sangeetha…
sangeethababu9223 May 19, 2026
ba0d28b
feat(tabs): conflict resolve
sangeethababu9223 May 20, 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
10 changes: 10 additions & 0 deletions packages/web-components/src/components/tabs/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ export enum TABS_TYPE {
CONTAINED = 'contained',
}

/**
* Vertical navigation direction, associated with key symbols.
*/
export const VERTICAL_NAVIGATION_DIRECTION = {
Up: -1,
ArrowUp: -1,
Down: 1,
ArrowDown: 1,
};

/**
* Tabs icon sizes.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/web-components/src/components/tabs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ import './tabs';
import './tab';
import './tab-skeleton';
import './tabs-skeleton';
import './tabs-vertical';
import '../badge-indicator';
import '../tooltip';
74 changes: 69 additions & 5 deletions packages/web-components/src/components/tabs/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { property, state } from 'lit/decorators.js';
import { property, state, query } from 'lit/decorators.js';
import { prefix } from '../../globals/settings';
import CDSContentSwitcherItem from '../content-switcher/content-switcher-item';
import { TABS_ICON_SIZE, TABS_TYPE } from './defs';
Expand Down Expand Up @@ -46,6 +46,13 @@ export default class CDSTab extends HostListenerMixin(CDSContentSwitcherItem) {
@property({ reflect: true })
type = TABS_TYPE.REGULAR;

/**
* `true` if the tab is in vertical orientation.
* This is automatically set by the parent `<cds-tabs>` when it's in vertical mode.
*/
@property({ type: Boolean, reflect: true })
vertical = false;

/**
* `true` if this tab is icon-only.
*/
Expand Down Expand Up @@ -77,6 +84,13 @@ export default class CDSTab extends HostListenerMixin(CDSContentSwitcherItem) {
@property({ type: Boolean, reflect: true, attribute: 'badge-indicator' })
badgeIndicator = false;

/**
* `true` if the tab text is truncated with ellipsis.
* This state is automatically updated when the component renders in vertical mode.
*/
@state()
truncated = false;

/**
* Whether this tab should be dismissable.
*/
Expand All @@ -89,13 +103,53 @@ export default class CDSTab extends HostListenerMixin(CDSContentSwitcherItem) {
@state()
_index = -1;

/**
* Reference to the label span element (only present in vertical mode).
* @private
*/
@query(`.${prefix}--tabs__nav-item-label`)
private _labelElement?: HTMLElement;

/**
* Checks if the text overflow ellipsis is currently applied to the label.
* This is useful for determining if a tooltip should be shown.
*
* @returns `true` if text is truncated/clamped, `false` otherwise or if not in vertical mode
*/
isTextTruncated(): boolean {
if (!this.vertical || !this._labelElement) {
return false;
}

// Compare scrollHeight with clientHeight to detect if content is overflowing
// When line-clamp is active and text exceeds 2 lines, scrollHeight > clientHeight
return this._labelElement.scrollHeight > this._labelElement.clientHeight;
}

/**
* Updates the truncated state after the component has rendered.
*/
updated(changedProperties: Map<PropertyKey, unknown>) {
super.updated(changedProperties);

// Check if text is truncated and update state when in vertical mode
if (this.vertical && this._labelElement) {
const isTruncated = this.isTextTruncated();
if (this.truncated !== isTruncated) {
this.truncated = isTruncated;
}
}
}

/**
* Handles `slotchange` event.
*/
protected _handleSlotChange({ target }: Event) {
// Retrieve content of the slot to use for aria-label.
const content = (target as HTMLSlotElement).assignedNodes();
this.tabTitle = content[0]?.textContent?.trim() || undefined;
const textContent = content[0]?.textContent;
// Normalize whitespace: trim and replace multiple spaces with single space
this.tabTitle = textContent?.trim().replace(/\s+/g, ' ') || undefined;
}

/**
Expand Down Expand Up @@ -137,6 +191,8 @@ export default class CDSTab extends HostListenerMixin(CDSContentSwitcherItem) {
secondaryLabel,
selected,
tabTitle,
vertical,
truncated,
_handleSlotChange: handleSlotChange,
_handleClick: handleClick,
} = this;
Expand Down Expand Up @@ -170,9 +226,17 @@ export default class CDSTab extends HostListenerMixin(CDSContentSwitcherItem) {
tabindex="${selected ? 0 : -1}"
?disabled="${disabled}"
aria-selected="${selected}">
<span class="${prefix}--tabs__nav-item-label-wrapper">
<slot @slotchange="${handleSlotChange}"></slot>
</span>
${vertical
? html`<span
class="${prefix}--tabs__nav-item-label"
title="${truncated ? tabTitle.trim() : ''}">
<slot @slotchange="${handleSlotChange}"></slot>
</span>`
: html`
<span class="${prefix}--tabs__nav-item-label-wrapper">
<slot @slotchange="${handleSlotChange}"></slot>
</span>
`}
${secondaryLabel
? html`<span
class="${prefix}--tabs__nav-item-secondary-label"
Expand Down
5 changes: 5 additions & 0 deletions packages/web-components/src/components/tabs/tabs-story.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ body {
#{$prefix}-tabs[type='contained'] ~ .#{$prefix}--tab-content {
background: $layer;
}

.#{$prefix}-ce-demo-devenv--tab-story-button {
display: inline-block;
inline-size: auto;
}
141 changes: 141 additions & 0 deletions packages/web-components/src/components/tabs/tabs-vertical.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Copyright IBM Corp. 2019, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import { LitElement, html } from 'lit';
import { property } from 'lit/decorators.js';
import { breakpoints } from '@carbon/layout';
import { prefix } from '../../globals/settings';
import styles from './tabs.scss?lit';
import { carbonElement as customElement } from '../../globals/decorators/carbon-element';

/**
* Breakpoint for switching between horizontal and vertical tab layouts.
* Uses Carbon's md breakpoint (42rem/672px) + 0.01rem offset to avoid overlap
* with max-width queries. This mirrors the Sass breakpoint behavior.
* Below this breakpoint, tabs display horizontally.
*/
const VERTICAL_TABS_BREAKPOINT = `(min-width: calc(${breakpoints.md.width} + 0.01rem))`;

/**
* Vertical tabs container component.
*
* @element cds-tabs-vertical
* @slot tabs - The `<cds-tabs>` navigation element.
* @slot panel - One or more `<div role="tabpanel">` elements.
*/
@customElement(`${prefix}-tabs-vertical`)
export default class CDSTabsVertical extends LitElement {
/**
* Option to set a height style only if using vertical variation.
*/
@property({ attribute: 'custom-height' })
customHeight?: string;

private _mediaQueryList: MediaQueryList | null = null;

private _handleViewportChange = (e: MediaQueryListEvent | MediaQueryList) => {
const tabs = this.querySelector(`${prefix}-tabs`);
if (e.matches) {
this.classList.add(`${prefix}--css-grid`);
if (tabs) {
tabs.setAttribute('vertical', '');
tabs.removeAttribute('type');
}
} else {
this.classList.remove(`${prefix}--css-grid`);
if (tabs) {
tabs.removeAttribute('vertical');
tabs.setAttribute('type', 'contained');
}
}
};

firstUpdated() {
this._mediaQueryList = window.matchMedia(VERTICAL_TABS_BREAKPOINT);
this._handleViewportChange(this._mediaQueryList);
this._mediaQueryList.addEventListener('change', this._handleViewportChange);

requestAnimationFrame(() => {
this._applyHeight();
});

const panelSlot = this.shadowRoot?.querySelector('slot[name="panel"]');
panelSlot?.addEventListener('slotchange', () => {
this._applyHeight();
});
}

updated(changedProperties) {
super.updated?.(changedProperties);
if (changedProperties.has('customHeight')) {
this._applyHeight();
}
}

disconnectedCallback() {
super.disconnectedCallback();
this._mediaQueryList?.removeEventListener(
'change',
this._handleViewportChange
);
this._mediaQueryList = null;
}

private _applyHeight() {
const isVertical = this.classList.contains(`${prefix}--css-grid`);

if (this.customHeight) {
this.style.height = this.customHeight;
return;
}

if (!isVertical) {
this.style.removeProperty('height');
return;
}

const panels = Array.from(
this.querySelectorAll<HTMLElement>('[slot="panel"]')
);

if (panels.length === 0) {
return;
}

const hiddenStates = panels.map((panel) => panel.hidden);
panels.forEach((panel) => {
panel.hidden = false;
});

const tallestPanel = Math.max(...panels.map((panel) => panel.offsetHeight));

panels.forEach((panel, index) => {
panel.hidden = hiddenStates[index];
});

const tabsEl = this.querySelector<HTMLElement>(`${prefix}-tabs`);
const tabsHeight = tabsEl?.offsetHeight ?? 0;

const height = Math.max(tallestPanel, tabsHeight);
if (height > 0) {
this.style.height = `${height}px`;
} else {
this.style.removeProperty('height');
}
}

render() {
return html`
<slot name="tabs"></slot>
<div class="${prefix}-panel-container">
<slot name="panel"></slot>
</div>
`;
}

static styles = styles;
}
8 changes: 8 additions & 0 deletions packages/web-components/src/components/tabs/tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,20 @@ flexibility in what is in rendered inside of `cds-tab` and a tab's panel.

<Canvas of={TabsStories.skeleton} />

### Vertical

<Canvas of={TabsStories.Vertical} />

### With Icons

<Canvas of={TabsStories.WithIcons} />

## Component API

## `cds-tabs-vertical`

<ArgTypes of="cds-tabs-vertical" />

## `cds-tabs`

<ArgTypes of="cds-tabs" />
Expand Down
Loading
Loading