(1)
+
+ const items: {navigation: string; counter?: number | string; href?: string}[] = [
+ {navigation: 'Code', href: '#code'},
+ {navigation: 'Issues', counter: '12K', href: '#issues'},
+ {navigation: 'Pull Requests', counter: 13, href: '#pull-requests'},
+ {navigation: 'Discussions', counter: 5, href: '#discussions'},
+ {navigation: 'Actions', counter: 4, href: '#actions'},
+ {navigation: 'Projects', counter: 9, href: '#projects'},
+ {navigation: 'Insights', counter: '0', href: '#insights'},
+ {navigation: 'Settings', counter: 10, href: '#settings'},
+ {navigation: 'Security', href: '#security'},
+ ]
+
+ return (
+
+
UnderlineNav - Overflow on Narrow Screen
+
+ {items.map((item, index) => (
+ {
+ event.preventDefault()
+ setSelectedIndex(index)
+ }}
+ counter={item.counter}
+ href={item.href}
+ >
+ {item.navigation}
+
+ ))}
+
+
+ )
+}
diff --git a/packages/react/package.json b/packages/react/package.json
index eb1d60b4ec1..7e02218e57b 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -37,6 +37,7 @@
],
"scripts": {
"build": "./script/build",
+ "build:components": "npx rollup -c",
"clean": "rimraf dist generated",
"start": "concurrently npm:start:*",
"start:storybook": "STORYBOOK=true storybook dev -p 6006",
diff --git a/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx b/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
index 6b35a2c739b..31dd299acc6 100644
--- a/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
+++ b/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
@@ -154,3 +154,126 @@ export const VariantFlush = () => {
)
}
+
+/**
+ * - At extra-narrow viewport (< 544px): Shows first 2 items inline; rest in menu
+ * - At narrow viewport (544px - 768px): Shows first 3 items inline; rest in menu
+ * - At regular viewport (768px - 1024px): Shows first 5 items inline; rest in menu
+ * - At medium viewport (1024px - 1280px): Shows first 6 items inline; rest in menu
+ * - At large viewport (1280px - 1400px): Shows first 7 items inline; rest in menu
+ * - At wide viewport (> 1400px): Shows all items inline; menu is hidden
+ */
+export const ResponsiveOverflow = () => {
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+
+ return (
+ 1400px, hide menu)
+ }}
+ >
+ {items.map((item, index) => (
+ {
+ event.preventDefault()
+ setSelectedIndex(index)
+ }}
+ counter={item.counter}
+ href={item.href}
+ >
+ {item.navigation}
+
+ ))}
+
+ )
+}
+
+export const ResponsiveOverflowXNarrow = () => {
+ return
+}
+
+ResponsiveOverflowXNarrow.parameters = {
+ viewport: {
+ viewports: {
+ ...INITIAL_VIEWPORTS,
+ xnarrowScreen: {
+ name: 'Extra Narrow Screen',
+ styles: {
+ width: '400px',
+ height: '100%',
+ },
+ },
+ },
+ defaultViewport: 'xnarrowScreen',
+ },
+}
+
+export const ResponsiveOverflowNarrow = () => {
+ return
+}
+
+ResponsiveOverflowNarrow.parameters = {
+ viewport: {
+ viewports: {
+ ...INITIAL_VIEWPORTS,
+ narrowScreen: {
+ name: 'Narrow Screen',
+ styles: {
+ width: '600px',
+ height: '100%',
+ },
+ },
+ },
+ defaultViewport: 'narrowScreen',
+ },
+}
+
+export const ResponsiveOverflowRegular = () => {
+ return
+}
+
+ResponsiveOverflowRegular.parameters = {
+ viewport: {
+ viewports: {
+ ...INITIAL_VIEWPORTS,
+ regularScreen: {
+ name: 'Regular Screen',
+ styles: {
+ width: '900px',
+ height: '100%',
+ },
+ },
+ },
+ defaultViewport: 'regularScreen',
+ },
+}
+
+export const ResponsiveOverflowWide = () => {
+ return
+}
+
+ResponsiveOverflowWide.parameters = {
+ viewport: {
+ viewports: {
+ ...INITIAL_VIEWPORTS,
+ wideScreen: {
+ name: 'Wide Screen',
+ styles: {
+ width: '1500px',
+ height: '100%',
+ },
+ },
+ },
+ defaultViewport: 'wideScreen',
+ },
+}
diff --git a/packages/react/src/UnderlineNav/UnderlineNav.module.css b/packages/react/src/UnderlineNav/UnderlineNav.module.css
index d905aff3fcf..36905b5927f 100644
--- a/packages/react/src/UnderlineNav/UnderlineNav.module.css
+++ b/packages/react/src/UnderlineNav/UnderlineNav.module.css
@@ -20,3 +20,88 @@
margin-left: 0;
}
}
+
+/* Container for overflow menu when using responsive mode */
+.ResponsiveOverflowContainer {
+ display: flex;
+ align-items: center;
+}
+
+/* Responsive menu visibility - extra-narrow viewport (max-width: 544px) */
+@media (max-width: 544px) {
+ .ResponsiveOverflowContainer[data-hide-menu-xnarrow='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+
+ /* Use :has() to target menu item li elements, excluding the container */
+ li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-xnarrow='true']) {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive menu visibility - narrow viewport (544px - 768px) */
+@media (min-width: 544px) and (max-width: 768px) {
+ .ResponsiveOverflowContainer[data-hide-menu-narrow='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+
+ li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-narrow='true']) {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive menu visibility - regular viewport (768px - 1024px) */
+@media (min-width: 768px) and (max-width: 1024px) {
+ .ResponsiveOverflowContainer[data-hide-menu-regular='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+
+ li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-regular='true']) {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive menu visibility - medium viewport (1024px - 1280px) */
+@media (min-width: 1024px) and (max-width: 1280px) {
+ .ResponsiveOverflowContainer[data-hide-menu-medium='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+
+ li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-medium='true']) {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive menu visibility - large viewport (1280px - 1400px) */
+@media (min-width: 1280px) and (max-width: 1400px) {
+ .ResponsiveOverflowContainer[data-hide-menu-large='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+
+ li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-large='true']) {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive menu visibility - wide viewport (min-width: 1400px) */
+@media (min-width: 1400px) {
+ .ResponsiveOverflowContainer[data-hide-menu-wide='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+
+ li:not(.ResponsiveOverflowContainer):has([data-hide-in-menu-wide='true']) {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
diff --git a/packages/react/src/UnderlineNav/UnderlineNav.tsx b/packages/react/src/UnderlineNav/UnderlineNav.tsx
index c0a252d3ffe..750ae119b7d 100644
--- a/packages/react/src/UnderlineNav/UnderlineNav.tsx
+++ b/packages/react/src/UnderlineNav/UnderlineNav.tsx
@@ -3,7 +3,7 @@ import React, {useRef, forwardRef, useCallback, useState, useEffect} from 'react
import {UnderlineNavContext} from './UnderlineNavContext'
import type {ResizeObserverEntry} from '../hooks/useResizeObserver'
import {useResizeObserver} from '../hooks/useResizeObserver'
-import type {ChildWidthArray, ResponsiveProps, ChildSize} from './types'
+import type {ChildWidthArray, ResponsiveProps, ChildSize, ResponsiveOverflowConfig} from './types'
import VisuallyHidden from '../_VisuallyHidden'
import {dividerStyles, menuItemStyles, baseMenuMinWidth} from './styles'
import {UnderlineItemList, UnderlineWrapper, LoadingCounter, GAP} from '../internal/components/UnderlineTabbedInterface'
@@ -33,6 +33,21 @@ export type UnderlineNavProps = {
* Setting this to `flush` will remove the horizontal padding on the items.
*/
variant?: 'inset' | 'flush'
+ /**
+ * When provided, items are rendered with data attributes that CSS media queries use
+ * to control visibility. The overflow menu is always rendered but uses CSS to hide
+ * items that are visible inline at the current viewport.
+ *
+ * Example:
+ * ```
+ * {
+ * narrow: [0, 1], // Show first 2 items inline at narrow viewport
+ * regular: [0, 1, 2, 3], // Show first 4 items inline at regular viewport
+ * wide: 'all' // Show all items inline at wide viewport
+ * }
+ * ```
+ */
+ responsiveOverflow?: ResponsiveOverflowConfig
}
// When page is loaded, we don't have ref for the more button as it is not on the DOM yet.
// However, we need to calculate number of possible items when the more button present as well. So using the width of the more button as a constant.
@@ -49,7 +64,7 @@ const overflowEffect = (
noIconChildWidthArray: ChildWidthArray,
updateListAndMenu: (props: ResponsiveProps, iconsVisible: boolean) => void,
) => {
- let iconsVisible = true
+ let iconsVisible = false
if (childWidthArray.length === 0) {
updateListAndMenu({items: childArray, menuItems: []}, iconsVisible)
}
@@ -69,6 +84,7 @@ const overflowEffect = (
// First, we check if we can fit all the items with their icons
if (childArray.length <= numberOfItemsPossible) {
items.push(...childArray)
+ iconsVisible = true
} else if (childArray.length <= numberOfItemsWithoutIconPossible) {
// if we can't fit all the items with their icons, we check if we can fit all the items without their icons
iconsVisible = false
@@ -142,6 +158,114 @@ const baseMenuInlineStyles: React.CSSProperties = {
right: 0,
}
+/**
+ * Check if an item index should be hidden at a given breakpoint
+ */
+function isItemHiddenAtBreakpoint(
+ config: ResponsiveOverflowConfig,
+ breakpoint: 'xnarrow' | 'narrow' | 'regular' | 'medium' | 'large' | 'wide',
+ index: number,
+): boolean {
+ const visibleItems = config[breakpoint]
+ if (visibleItems === undefined || visibleItems === 'all') {
+ return false
+ }
+ return !visibleItems.includes(index)
+}
+
+/**
+ * Get data attributes for an inline item based on responsive config
+ */
+function getResponsiveItemAttributes(
+ config: ResponsiveOverflowConfig,
+ index: number,
+): {
+ 'data-hide-xnarrow'?: boolean
+ 'data-hide-narrow'?: boolean
+ 'data-hide-regular'?: boolean
+ 'data-hide-medium'?: boolean
+ 'data-hide-large'?: boolean
+ 'data-hide-wide'?: boolean
+} {
+ return {
+ 'data-hide-xnarrow': isItemHiddenAtBreakpoint(config, 'xnarrow', index) || undefined,
+ 'data-hide-narrow': isItemHiddenAtBreakpoint(config, 'narrow', index) || undefined,
+ 'data-hide-regular': isItemHiddenAtBreakpoint(config, 'regular', index) || undefined,
+ 'data-hide-medium': isItemHiddenAtBreakpoint(config, 'medium', index) || undefined,
+ 'data-hide-large': isItemHiddenAtBreakpoint(config, 'large', index) || undefined,
+ 'data-hide-wide': isItemHiddenAtBreakpoint(config, 'wide', index) || undefined,
+ }
+}
+
+/**
+ * Get data attributes for a menu item based on responsive config.
+ * Menu items should be hidden when they are visible inline.
+ */
+function getResponsiveMenuItemAttributes(
+ config: ResponsiveOverflowConfig,
+ index: number,
+): {
+ 'data-hide-in-menu-xnarrow'?: boolean
+ 'data-hide-in-menu-narrow'?: boolean
+ 'data-hide-in-menu-regular'?: boolean
+ 'data-hide-in-menu-medium'?: boolean
+ 'data-hide-in-menu-large'?: boolean
+ 'data-hide-in-menu-wide'?: boolean
+} {
+ // Hide in menu when NOT hidden inline (i.e., when visible inline)
+ return {
+ 'data-hide-in-menu-xnarrow': !isItemHiddenAtBreakpoint(config, 'xnarrow', index) || undefined,
+ 'data-hide-in-menu-narrow': !isItemHiddenAtBreakpoint(config, 'narrow', index) || undefined,
+ 'data-hide-in-menu-regular': !isItemHiddenAtBreakpoint(config, 'regular', index) || undefined,
+ 'data-hide-in-menu-medium': !isItemHiddenAtBreakpoint(config, 'medium', index) || undefined,
+ 'data-hide-in-menu-large': !isItemHiddenAtBreakpoint(config, 'large', index) || undefined,
+ 'data-hide-in-menu-wide': !isItemHiddenAtBreakpoint(config, 'wide', index) || undefined,
+ }
+}
+
+/**
+ * Get data attributes for the menu container based on responsive config.
+ * Menu should be hidden when all items are visible inline.
+ */
+function getResponsiveMenuContainerAttributes(config: ResponsiveOverflowConfig): {
+ 'data-hide-menu-xnarrow'?: boolean
+ 'data-hide-menu-narrow'?: boolean
+ 'data-hide-menu-regular'?: boolean
+ 'data-hide-menu-medium'?: boolean
+ 'data-hide-menu-large'?: boolean
+ 'data-hide-menu-wide'?: boolean
+} {
+ return {
+ 'data-hide-menu-xnarrow': config.xnarrow === 'all' || undefined,
+ 'data-hide-menu-narrow': config.narrow === 'all' || undefined,
+ 'data-hide-menu-regular': config.regular === 'all' || undefined,
+ 'data-hide-menu-medium': config.medium === 'all' || undefined,
+ 'data-hide-menu-large': config.large === 'all' || undefined,
+ 'data-hide-menu-wide': config.wide === 'all' || undefined,
+ }
+}
+
+/**
+ * Get all item indices that could be hidden at any breakpoint.
+ * These items need to be included in the overflow menu.
+ */
+function getResponsiveMenuItemIndices(config: ResponsiveOverflowConfig, totalItems: number): number[] {
+ const hiddenIndices = new Set()
+
+ for (const breakpoint of ['xnarrow', 'narrow', 'regular', 'medium', 'large', 'wide'] as const) {
+ const visibleItems = config[breakpoint]
+ if (visibleItems !== undefined && visibleItems !== 'all') {
+ for (let i = 0; i < totalItems; i++) {
+ if (!visibleItems.includes(i)) {
+ hiddenIndices.add(i)
+ }
+ }
+ }
+ }
+
+ return Array.from(hiddenIndices).sort((a, b) => a - b)
+}
+
export const UnderlineNav = forwardRef(
(
{
@@ -151,6 +275,7 @@ export const UnderlineNav = forwardRef(
variant = 'inset',
className,
children,
+ responsiveOverflow,
}: UnderlineNavProps,
forwardedRef,
) => {
@@ -162,10 +287,16 @@ export const UnderlineNav = forwardRef(
const containerRef = React.useRef(null)
const disclosureWidgetId = useId()
+ // Determine if we're using CSS-based responsive mode
+ const useResponsiveMode = Boolean(responsiveOverflow)
+
const [isWidgetOpen, setIsWidgetOpen] = useState(false)
- const [iconsVisible, setIconsVisible] = useState(true)
+ const [iconsVisible, setIconsVisible] = useState(false)
const [childWidthArray, setChildWidthArray] = useState([])
const [noIconChildWidthArray, setNoIconChildWidthArray] = useState([])
+ // Track whether the initial overflow calculation is complete to prevent CLS
+ // In responsive mode, we're ready immediately since CSS handles visibility
+ const [isReady, setIsReady] = useState(useResponsiveMode)
const validChildren = getValidChildren(children)
@@ -248,6 +379,8 @@ export const UnderlineNav = forwardRef(
const updateListAndMenu = useCallback((props: ResponsiveProps, displayIcons: boolean) => {
setResponsiveProps(props)
setIconsVisible(displayIcons)
+ // Mark as ready after the first overflow calculation completes
+ setIsReady(true)
}, [])
const setChildrenWidth = useCallback((size: ChildSize) => {
setChildWidthArray(arr => {
@@ -320,6 +453,119 @@ export const UnderlineNav = forwardRef(
}
}
+ if (useResponsiveMode && responsiveOverflow) {
+ // Get indices of items that need to be in the menu
+ const responsiveMenuItemIndices = getResponsiveMenuItemIndices(responsiveOverflow, validChildren.length)
+ const responsiveMenuItems = responsiveMenuItemIndices.map(index => ({
+ index,
+ child: validChildren[index],
+ }))
+
+ // Clone children with responsive data attributes
+ const responsiveListItems = validChildren.map((child, index) => {
+ const attrs = getResponsiveItemAttributes(responsiveOverflow, index)
+ return React.cloneElement(child, {...attrs, key: child.key ?? index})
+ })
+
+ const menuContainerAttrs = getResponsiveMenuContainerAttributes(responsiveOverflow)
+ const hasMenuItems = responsiveMenuItems.length > 0
+
+ return (
+
+ {ariaLabel && {`${ariaLabel} navigation`}}
+
+
+ {responsiveListItems}
+ {hasMenuItems && (
+
+
+
+ = baseMenuMinWidth
+ ? baseMenuInlineStyles
+ : menuInlineStyles),
+ display: isWidgetOpen ? 'block' : 'none',
+ }}
+ >
+ {responsiveMenuItems.map(({index, child}) => {
+ const {children: menuItemChildren, counter, onSelect, ...menuItemProps} = child.props
+ const menuItemAttrs = getResponsiveMenuItemAttributes(responsiveOverflow, index)
+
+ return (
+ | React.KeyboardEvent,
+ ) => {
+ closeOverlay()
+ focusOnMoreMenuBtn()
+ typeof onSelect === 'function' && onSelect(event)
+ }}
+ {...menuItemProps}
+ {...menuItemAttrs}
+ >
+
+ {menuItemChildren}
+ {loadingCounters ? (
+
+ ) : (
+ counter !== undefined && (
+
+ {counter}
+
+ )
+ )}
+
+
+ )
+ })}
+
+
+ )}
+
+
+
+ )
+ }
+
+ // ============================================
+ // Default JS-based overflow rendering
+ // ============================================
return (
{ariaLabel && {`${ariaLabel} navigation`}}
-
+
{listItems}
{menuItems.length > 0 && (
diff --git a/packages/react/src/UnderlineNav/UnderlineNavItem.module.css b/packages/react/src/UnderlineNav/UnderlineNavItem.module.css
index c1e75b06204..320317c4e76 100644
--- a/packages/react/src/UnderlineNav/UnderlineNavItem.module.css
+++ b/packages/react/src/UnderlineNav/UnderlineNavItem.module.css
@@ -3,3 +3,51 @@
flex-direction: column;
align-items: center;
}
+
+/* Responsive item visibility - extra small viewport (max-width: 544px) */
+@media (max-width: 544px) {
+ .UnderlineNavItem[data-hide-xnarrow='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive item visibility - narrow viewport (544px - 768px) */
+@media (min-width: 544px) and (max-width: 768px) {
+ .UnderlineNavItem[data-hide-narrow='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive item visibility - regular viewport (768px - 1024px) */
+@media (min-width: 768px) and (max-width: 1024px) {
+ .UnderlineNavItem[data-hide-regular='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive item visibility - medium viewport (1024px - 1280px) */
+@media (min-width: 1024px) and (max-width: 1280px) {
+ .UnderlineNavItem[data-hide-medium='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive item visibility - large viewport (1280px - 1400px) */
+@media (min-width: 1280px) and (max-width: 1400px) {
+ .UnderlineNavItem[data-hide-large='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
+
+/* Responsive item visibility - wide viewport (min-width: 1400px) */
+@media (min-width: 1400px) {
+ .UnderlineNavItem[data-hide-wide='true'] {
+ /* stylelint-disable-next-line declaration-no-important */
+ display: none !important;
+ }
+}
diff --git a/packages/react/src/UnderlineNav/UnderlineNavItem.tsx b/packages/react/src/UnderlineNav/UnderlineNavItem.tsx
index 3341f677e11..39275455090 100644
--- a/packages/react/src/UnderlineNav/UnderlineNavItem.tsx
+++ b/packages/react/src/UnderlineNav/UnderlineNavItem.tsx
@@ -57,6 +57,31 @@ export type UnderlineNavItemProps = {
* Counter
*/
counter?: number | string
+
+ /**
+ * @internal Used by UnderlineNav for CSS based responsive overflow
+ */
+ 'data-hide-xnarrow'?: string
+ /**
+ * @internal Used by UnderlineNav for CSS based responsive overflow
+ */
+ 'data-hide-narrow'?: string
+ /**
+ * @internal Used by UnderlineNav for CSS based responsive overflow
+ */
+ 'data-hide-regular'?: string
+ /**
+ * @internal Used by UnderlineNav for CSS based responsive overflow
+ */
+ 'data-hide-medium'?: string
+ /**
+ * @internal Used by UnderlineNav for CSS based responsive overflow
+ */
+ 'data-hide-large'?: string
+ /**
+ * @internal Used by UnderlineNav for CSS based responsive overflow
+ */
+ 'data-hide-wide'?: string
} & LinkProps
export const UnderlineNavItem = forwardRef(
@@ -70,6 +95,12 @@ export const UnderlineNavItem = forwardRef(
'aria-current': ariaCurrent,
icon: Icon,
leadingVisual,
+ 'data-hide-xnarrow': dataHideXnarrow,
+ 'data-hide-narrow': dataHideNarrow,
+ 'data-hide-regular': dataHideRegular,
+ 'data-hide-medium': dataHideMedium,
+ 'data-hide-large': dataHideLarge,
+ 'data-hide-wide': dataHideWide,
...props
},
forwardedRef,
@@ -120,7 +151,15 @@ export const UnderlineNavItem = forwardRef(
)
return (
-
+
>
}
+
+/**
+ *
+ * Each breakpoint key defines which item indices should be visible at that viewport size.
+ * Items not in the visible array will be hidden in the inline list but shown in the overflow menu.
+ *
+ * Example:
+ * ```
+ * {
+ * narrow: [0, 1], // Show first 2 items inline at narrow viewport
+ * regular: [0, 1, 2, 3], // Show first 4 items inline at regular viewport
+ * wide: 'all' // Show all items inline at wide viewport (hide menu)
+ * }
+ * ```
+ */
+export type ResponsiveOverflowConfig = {
+ /** Items visible at extra-narrow viewport (max-width: 544px) */
+ xnarrow?: number[] | 'all'
+ /** Items visible at narrow viewport (544px - 768px) */
+ narrow?: number[] | 'all'
+ /** Items visible at regular viewport (768px - 1024px) */
+ regular?: number[] | 'all'
+ /** Items visible at medium viewport (1024px - 1280px) */
+ medium?: number[] | 'all'
+ /** Items visible at large viewport (1280px - 1400px) */
+ large?: number[] | 'all'
+ /** Items visible at wide viewport (min-width: 1400px) */
+ wide?: number[] | 'all'
+}
diff --git a/packages/react/src/internal/components/UnderlineTabbedInterface.module.css b/packages/react/src/internal/components/UnderlineTabbedInterface.module.css
index 85214c5c317..ebe310aee9e 100644
--- a/packages/react/src/internal/components/UnderlineTabbedInterface.module.css
+++ b/packages/react/src/internal/components/UnderlineTabbedInterface.module.css
@@ -12,6 +12,13 @@
/* stylelint-disable-next-line primer/box-shadow */
box-shadow: inset 0 -1px var(--borderColor-muted);
+ /* Hide overflow until calculation is complete to prevent CLS */
+ overflow: hidden;
+
+ &[data-ready='true'] {
+ overflow: visible;
+ }
+
&[data-variant='flush'] {
/* stylelint-disable-next-line primer/spacing */
padding-inline: unset;
diff --git a/packages/react/src/internal/components/UnderlineTabbedInterface.tsx b/packages/react/src/internal/components/UnderlineTabbedInterface.tsx
index 509459ea47f..ffadb25adc2 100644
--- a/packages/react/src/internal/components/UnderlineTabbedInterface.tsx
+++ b/packages/react/src/internal/components/UnderlineTabbedInterface.tsx
@@ -32,14 +32,18 @@ type UnderlineWrapperProps = {
as?: As
className?: string
ref?: React.Ref
+ /** Indicates whether the overflow calculation is complete. When false, overflow is hidden to prevent CLS. */
+ ready?: boolean
}
export const UnderlineWrapper = forwardRef((props, ref) => {
- const {children, className, as: Component = 'div', ...rest} = props
+ const {children, className, as: Component = 'div', ready, ...rest} = props
+
return (
}
+ data-ready={ready ? 'true' : undefined}
{...rest}
>
{children}
@@ -74,6 +78,7 @@ export type UnderlineItemProps = {
export const UnderlineItem = React.forwardRef((props, ref) => {
const {as: Component = 'a', children, counter, icon: Icon, iconsVisible, loadingCounters, className, ...rest} = props
const textContent = getTextContent(children)
+
return (
{iconsVisible && Icon && {isElement(Icon) ? Icon : }}