diff --git a/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx b/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
index 6b35a2c739b..c5b74955d1d 100644
--- a/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
+++ b/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
@@ -14,6 +14,7 @@ import {
import type {Meta} from '@storybook/react-vite'
import {UnderlineNav} from './index'
import {INITIAL_VIEWPORTS} from 'storybook/viewport'
+import Popover from '../Popover'
const meta = {
title: 'Components/UnderlineNav/Features',
@@ -154,3 +155,35 @@ export const VariantFlush = () => {
)
}
+
+export const WithPopover = () => {
+ return (
+
+ }>
+ Code
+
+ }>
+ Issues
+
+ } counter={12}>
+ Security
+
+ Popover content
+
+
+ }>
+ Insights
+
+ }>
+ Settings
+
+
+ )
+}
diff --git a/packages/react/src/UnderlineNav/UnderlineNav.test.tsx b/packages/react/src/UnderlineNav/UnderlineNav.test.tsx
index 7b4c2dd8497..6039ec2ad8a 100644
--- a/packages/react/src/UnderlineNav/UnderlineNav.test.tsx
+++ b/packages/react/src/UnderlineNav/UnderlineNav.test.tsx
@@ -210,6 +210,34 @@ describe('UnderlineNav', () => {
expect(screen.getByTestId('jsx-element')).toBeInTheDocument()
expect(screen.getByTestId('functional-component')).toBeInTheDocument()
})
+
+ it('extracts only direct text content for data-content attribute, ignoring nested elements', () => {
+ render(
+
+
+ Tab Label
+ Hidden element
+
+ ,
+ )
+
+ const item = screen.getByRole('link', {name: /Tab Label/})
+ const textSpan = item.querySelector('[data-component="text"]')
+ // data-content should only have the content of the Text and not the nested span
+ expect(textSpan).toHaveAttribute('data-content', 'Tab Label')
+ })
+
+ it('handles string children correctly for data-content attribute', () => {
+ render(
+
+ Simple Text
+ ,
+ )
+
+ const item = screen.getByRole('link', {name: 'Simple Text'})
+ const textSpan = item.querySelector('[data-component="text"]')
+ expect(textSpan).toHaveAttribute('data-content', 'Simple Text')
+ })
})
describe('Keyboard Navigation', () => {
diff --git a/packages/react/src/internal/components/UnderlineTabbedInterface.tsx b/packages/react/src/internal/components/UnderlineTabbedInterface.tsx
index a6fa4bee785..509459ea47f 100644
--- a/packages/react/src/internal/components/UnderlineTabbedInterface.tsx
+++ b/packages/react/src/internal/components/UnderlineTabbedInterface.tsx
@@ -13,6 +13,20 @@ import {clsx} from 'clsx'
// The gap between the list items. It is a constant because the gap is used to calculate the possible number of items that can fit in the container.
export const GAP = 8
+// Helper to extract direct text content from children for the data-content attribute.
+// This is used by CSS to reserve space for bold text (preventing layout shift).
+// Only extracts strings/numbers, not text from nested React elements (e.g., Popovers).
+function getTextContent(children: React.ReactNode): string {
+ if (typeof children === 'string' || typeof children === 'number') {
+ return String(children)
+ }
+ if (Array.isArray(children)) {
+ return children.map(getTextContent).join('')
+ }
+ // Skip React elements - we only want direct text content, not text from nested components
+ return ''
+}
+
type UnderlineWrapperProps = {
slot?: string
as?: As
@@ -59,11 +73,12 @@ 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 : }}
{children && (
-
+
{children}
)}