Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 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
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';
75 changes: 69 additions & 6 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 } 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 @@ -36,6 +36,13 @@ export default class CDSTab extends 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 All @@ -53,20 +60,66 @@ export default class CDSTab extends CDSContentSwitcherItem {
*/
@property()
tabTitle;

/**
* **Experimental**: Display an empty dot badge on the Tab.
*/
@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;

/**
* 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;
}

connectedCallback() {
Expand All @@ -82,6 +135,8 @@ export default class CDSTab extends CDSContentSwitcherItem {
disabled,
selected,
tabTitle,
vertical,
truncated,
_handleSlotChange: handleSlotChange,
} = this;
const accessibleLabel = tabTitle || this.getAttribute('aria-label');
Expand All @@ -97,9 +152,17 @@ export default class CDSTab extends 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>
`}
${!disabled && badgeIndicator
? html`<cds-badge-indicator></cds-badge-indicator>`
: ''}
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 @@ -28,3 +28,8 @@
#{$prefix}-tabs[type='contained'] ~ .#{$prefix}--tab-content {
background: $layer;
}

.#{$prefix}-ce-demo-devenv--tab-story-button {
display: inline-block;
inline-size: auto;
}
138 changes: 138 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,138 @@
/**
* 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 { 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.
* Matches Carbon's md breakpoint (673px) - below this, tabs display horizontally.
*/
const VERTICAL_TABS_BREAKPOINT = '(min-width: 673px)';

/**
* 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 @@ -52,8 +52,16 @@ flexibility in what is in rendered inside of `cds-tab` and a tab's panel.

<Canvas of={TabsStories.skeleton} />

### Vertical

<Canvas of={TabsStories.Vertical} />

## Component API

## `cds-tabs-vertical`

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

## `cds-tabs`

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