Repository: primer/brand
Pull request #1001: Add subheading to SubNav component
Author: login: rezrah
Created: 2025-05-08T11:28:41Z | Merged: 2025-05-09T10:15:08Z

## Summary

Closes https://github.com/github/primer/issues/5147

Adds a new `SubNav.SubHeading` child to the `SubNav` component for deeper IA support.

🔗 [Storybook preview link](https://primer-cce331d353-26139705.drafts.github.io/brand/storybook/?path=/story/components-subnav-features--sub-heading&globals=colorMode:dark)
🔗 [Docs preview](https://primer-cce331d353-26139705.drafts.github.io/brand/components/SubNav#subheading)

Before: Parent => Child, Child, Child
Now: Parent => Child => Child, Child, Child

No breaking changes to API, existing functionality or visual presentation. 

## List of notable changes:

<!--
E.g.

- **added** # for # component because #
- **removed** props for # component because #
- **updated** documentation for # component because #
-->

- Adds support for  `SubNav.SubHeading` 
- Adds Storybook examples
- Adds visual test coverage

## What should reviewers focus on?

<!--
Help reviewers check the changes applied.

E.g. For site designers

Verify that the implementation is aligned with the design proposal:
- Ensure the layout and the spacing are correct in different viewport sizes (desktop, tablet and mobile).
- Check dark and light color modes.
- Check the interactive states, such as hover, focus or active ones.
-->

- Please verify that the new feature appear as intended in BOTH light and dark color modes
- There is no design available for when SubNav.SubHeading is active on mobile, so I've retained the button appearance as before. [Example in Storybook covering this path.
](https://primer-cce331d353-26139705.drafts.github.io/brand/storybook/?path=/story/components-subnav-features--active-sub-heading-narrow&globals=colorMode:dark;viewport:iphonex)

## Steps to test:

<!--
Help reviewers test the feature by providing steps to reproduce the behavior.

E.g.

1. Open the # component in CI-deployed preview environment
2. Go to # story in Storybook
3. Verify that # behaves as described in the following issue.
-->

1. [Storybook examples](https://primer-cce331d353-26139705.drafts.github.io/brand/storybook/?path=/story/components-subnav-features--sub-heading&globals=colorMode:dark)


## Contributor checklist:

- [ ] All new and existing CI checks pass
- [ ] Tests prove that the feature works and covers both happy and unhappy paths
- [ ] Any drop in coverage, breaking changes or regressions have been documented above
- [ ] UI Changes contain new visual snapshots (generated by adding `update snapshots` label to the PR)
- [ ] All developer debugging and non-functional logging has been removed
- [ ] Related issues have been referenced in the PR description

## Reviewer checklist:

- [ ] Check that pull request and proposed changes adhere to our [contribution guidelines](../../CONTRIBUTING.md) and [code of conduct](../../CODE_OF_CONDUCT.md)
- [ ] Check that tests prove the feature works and covers both happy and unhappy paths
- [ ] Check that there aren't other open Pull Requests for the same update/change

## Screenshots:

![Screenshot 2025-05-08 at 12 40 44](https://github.com/user-attachments/assets/22d30c99-d6e0-4dff-bcb9-91129add0cc7)




Diff:
diff --git a/.changeset/fresh-ads-begin.md b/.changeset/fresh-ads-begin.md
new file mode 100644
index 0000000000..484478d6c0
--- /dev/null
+++ b/.changeset/fresh-ads-begin.md
@@ -0,0 +1,20 @@
+---
+'@primer/react-brand': patch
+---
+
+Add support for deeper link nesting in `SubNav`
+
+Use `SubNav.SubHeading` to present another tier of content between the `SubNav.Heading` and `SubNav.Link` sub-components.
+
+Usage example:
+
+```jsx
+<SubNav>
+  <SubNav.Heading href="#">Top-level page</SubNav.Heading>
+  <SubNav.SubHeading href="#">Sub-level page</SubNav.SubHeading>
+  <SubNav.Link href="#">Child page</SubNav.Link>
+  <SubNav.Link href="#">Child page</SubNav.Link>
+  <SubNav.Link href="#">Child page</SubNav.Link>
+  <SubNav.Link href="#">Child page</SubNav.Link>
+</SubNav>
+```
diff --git a/apps/docs/content/components/SubNav.mdx b/apps/docs/content/components/SubNav.mdx
index 5488b5b65d..65287f8994 100644
--- a/apps/docs/content/components/SubNav.mdx
+++ b/apps/docs/content/components/SubNav.mdx
@@ -31,6 +31,22 @@ import {SubNav} from '@primer/react-brand'
 </Container>
 ```
 
+### Subheading
+
+```jsx live
+<Container sx={{position: 'relative', height: 100}}>
+  <SubNav>
+    <SubNav.Heading href="#">AI</SubNav.Heading>
+    <SubNav.SubHeading href="#" aria-current="page">
+      Copilot
+    </SubNav.SubHeading>
+    <SubNav.Link href="#">What&apos;s new</SubNav.Link>
+    <SubNav.Link href="#">Plans &amp; pricing</SubNav.Link>
+    <SubNav.Link href="#">For business</SubNav.Link>
+  </SubNav>
+</Container>
+```
+
 ### Shadow
 
 ```jsx live
@@ -131,6 +147,12 @@ Ensure that the text of each `SubNav.Link` within the anchor-based submenu fits
 | ------ | -------- | ------- | -------- | -------------------------------- |
 | `href` | `string` |         |          | URL of the page the link goes to |
 
+### SubNav.SubHeading
+
+| name   | type     | default | required | description                      |
+| ------ | -------- | ------- | -------- | -------------------------------- |
+| `href` | `string` |         |          | URL of the page the link goes to |
+
 ### SubNav.Link <Label>Required</Label>
 
 | name           | type                                                            | default | required | description                                     |
diff --git a/apps/next-docs/content/components/SubNav/index.mdx b/apps/next-docs/content/components/SubNav/index.mdx
index e253a06267..0b5b372673 100644
--- a/apps/next-docs/content/components/SubNav/index.mdx
+++ b/apps/next-docs/content/components/SubNav/index.mdx
@@ -33,6 +33,22 @@ import {SubNav} from '@primer/react-brand'
 </div>
 ```
 
+### Subheading
+
+```jsx live
+<div style={{position: 'relative', height: 100, width: '100%'}}>
+  <SubNav>
+    <SubNav.Heading href="#">AI</SubNav.Heading>
+    <SubNav.SubHeading href="#" aria-current="page">
+      Copilot
+    </SubNav.SubHeading>
+    <SubNav.Link href="#">What&apos;s new</SubNav.Link>
+    <SubNav.Link href="#">Plans &amp; pricing</SubNav.Link>
+    <SubNav.Link href="#">For business</SubNav.Link>
+  </SubNav>
+</div>
+```
+
 ### Shadow
 
 ```jsx live
@@ -133,6 +149,12 @@ Ensure that the text of each `SubNav.Link` within the anchor-based submenu fits
 | ------ | -------- | ------- | -------- | -------------------------------- |
 | `href` | `string` |         |          | URL of the page the link goes to |
 
+### SubNav.SubHeading
+
+| name   | type     | default | required | description                      |
+| ------ | -------- | ------- | -------- | -------------------------------- |
+| `href` | `string` |         |          | URL of the page the link goes to |
+
 ### SubNav.Link <Label>Required</Label>
 
 | name           | type                      | default | required | description                                     |
diff --git a/packages/react/src/SubNav/SubNav.features.stories.tsx b/packages/react/src/SubNav/SubNav.features.stories.tsx
index 5aa1b2ad53..057450eafd 100644
--- a/packages/react/src/SubNav/SubNav.features.stories.tsx
+++ b/packages/react/src/SubNav/SubNav.features.stories.tsx
@@ -92,6 +92,98 @@ WithShadow.parameters = {
   layout: 'fullscreen',
 }
 
+export const SubHeading = args => (
+  <SubNav {...args}>
+    <SubNav.Heading href="#">AI</SubNav.Heading>
+    <SubNav.SubHeading href="#">Copilot</SubNav.SubHeading>
+    <SubNav.Link href="#">For business</SubNav.Link>
+    <SubNav.Link href="#" aria-current="page">
+      Extensions
+    </SubNav.Link>
+    <SubNav.Link href="#">Tutorials</SubNav.Link>
+    <SubNav.Link href="#">
+      What&apos;s new
+      <SubNav.SubMenu>
+        <SubNav.Link href="#">Sub-feature page one</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page two</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page three</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page four</SubNav.Link>
+      </SubNav.SubMenu>
+    </SubNav.Link>
+    <SubNav.Link href="#">Plans & pricing</SubNav.Link>
+    <SubNav.Action href="#">Call to action</SubNav.Action>
+  </SubNav>
+)
+SubHeading.parameters = {
+  layout: 'fullscreen',
+}
+
+export const SubHeadingNarrow = args => <SubHeading {...args} />
+SubHeadingNarrow.parameters = {
+  layout: 'fullscreen',
+  viewport: {
+    defaultViewport: 'iphonex',
+  },
+}
+
+export const SubHeadingNarrowOpen = args => <SubHeading {...args} />
+SubHeadingNarrowOpen.parameters = {
+  layout: 'fullscreen',
+  viewport: {
+    defaultViewport: 'iphonex',
+  },
+}
+SubHeadingNarrowOpen.play = async ({canvasElement}) => {
+  const canvas = within(canvasElement)
+  await userEvent.click(canvas.getByTestId('SubNav-root-button'))
+}
+
+export const ActiveSubHeading = args => (
+  <SubNav {...args}>
+    <SubNav.Heading href="#">AI</SubNav.Heading>
+    <SubNav.SubHeading href="#" aria-current="page">
+      Copilot
+    </SubNav.SubHeading>
+    <SubNav.Link href="#">For business</SubNav.Link>
+    <SubNav.Link href="#">Extensions</SubNav.Link>
+    <SubNav.Link href="#">Tutorials</SubNav.Link>
+    <SubNav.Link href="#">
+      What&apos;s new
+      <SubNav.SubMenu>
+        <SubNav.Link href="#">Sub-feature page one</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page two</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page three</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page four</SubNav.Link>
+      </SubNav.SubMenu>
+    </SubNav.Link>
+    <SubNav.Link href="#">Plans & pricing</SubNav.Link>
+    <SubNav.Action href="#">Call to action</SubNav.Action>
+  </SubNav>
+)
+ActiveSubHeading.parameters = {
+  layout: 'fullscreen',
+}
+
+export const ActiveSubHeadingNarrow = args => <ActiveSubHeading {...args} />
+ActiveSubHeadingNarrow.parameters = {
+  layout: 'fullscreen',
+  viewport: {
+    defaultViewport: 'iphonex',
+  },
+}
+
+export const ActiveSubHeadingNarrowOpen = args => <ActiveSubHeading {...args} />
+ActiveSubHeadingNarrowOpen.parameters = {
+  layout: 'fullscreen',
+  viewport: {
+    defaultViewport: 'iphonex',
+  },
+}
+ActiveSubHeadingNarrowOpen.play = async ({canvasElement}) => {
+  const canvas = within(canvasElement)
+  await userEvent.click(canvas.getByTestId('SubNav-root-button'))
+}
+
 export const FullWidth = args => (
   <SubNav {...args} fullWidth>
     <SubNav.Heading href="#">Features</SubNav.Heading>
diff --git a/packages/react/src/SubNav/SubNav.module.css b/packages/react/src/SubNav/SubNav.module.css
index 19b4872357..9eed1b9823 100644
--- a/packages/react/src/SubNav/SubNav.module.css
+++ b/packages/react/src/SubNav/SubNav.module.css
@@ -183,7 +183,8 @@
   text-decoration: none !important; /* dotcom override */
 }
 
-.SubNav__link-label::after {
+.SubNav__link-label::after,
+.SubNav__subHeading::after {
   content: '';
   position: absolute;
   bottom: calc(var(--base-size-2) * -1);
@@ -305,6 +306,11 @@
     padding-block-end: var(--base-size-16);
   }
 
+  .SubNav--has-sub-heading .SubNav__links-overlay--open {
+    border-top: var(--brand-borderWidth-thin) solid var(--brand-color-border-default);
+    margin-block-start: var(--base-size-16);
+  }
+
   .SubNav__links-overlay--open .SubNav__link:hover .SubNav__link-label {
     color: var(--brand-color-text-default);
   }
@@ -326,6 +332,10 @@
     justify-content: end;
   }
 
+  .SubNav--has-sub-heading:not(.SubNav--subHeadingActive) .SubNav__overlay-toggle {
+    margin-block-start: var(--base-size-12);
+  }
+
   .SubNav__overlay-toggle-content {
     justify-content: space-between;
     width: 100%;
@@ -461,12 +471,29 @@
 
 .SubNav__link:focus-visible,
 .SubNav__heading:focus-visible,
+.SubNav__subHeading:focus-visible,
 .SubNav__sub-menu-toggle:focus-visible,
 .SubNav__overlay-toggle:focus-visible {
   outline: var(--brand-borderWidth-thick) solid var(--brand-color-focus);
   outline-offset: var(--base-size-2);
 }
 
+.SubNav__subHeading {
+  display: block;
+  position: relative;
+}
+
+.SubNav__subHeading[aria-current]:not([aria-current='false'])::after,
+.SubNav__subHeading:hover::after {
+  border-color: var(--brand-color-text-default);
+  opacity: 1;
+  transform: scale(0.8, 1);
+}
+
+.SubNav__subHeading:focus-visible::after {
+  opacity: 0;
+}
+
 @media screen and (min-width: 48rem) {
   .SubNav {
     padding: var(--base-size-16) var(--base-size-24);
@@ -527,6 +554,11 @@
     margin-inline-end: var(--base-size-20);
   }
 
+  .SubNav__subHeading {
+    font-size: var(--brand-text-size-100);
+    line-height: var(--brand-text-lineHeight-100);
+  }
+
   .SubNav__links-overlay {
     align-items: center;
     display: flex;
diff --git a/packages/react/src/SubNav/SubNav.module.css.d.ts b/packages/react/src/SubNav/SubNav.module.css.d.ts
index eaa3543c1d..6e25e2716e 100644
--- a/packages/react/src/SubNav/SubNav.module.css.d.ts
+++ b/packages/react/src/SubNav/SubNav.module.css.d.ts
@@ -2,8 +2,10 @@ declare const styles: {
   readonly "SubNav": string;
   readonly "SubNav--full-width": string;
   readonly "SubNav--has-shadow": string;
+  readonly "SubNav--has-sub-heading": string;
   readonly "SubNav--header-container-outer": string;
   readonly "SubNav--open": string;
+  readonly "SubNav--subHeadingActive": string;
   readonly "SubNav__action": string;
   readonly "SubNav__action-container": string;
   readonly "SubNav__anchor-menu-container": string;
@@ -35,6 +37,7 @@ declare const styles: {
   readonly "SubNav__sub-menu-icon": string;
   readonly "SubNav__sub-menu-list": string;
   readonly "SubNav__sub-menu-toggle": string;
+  readonly "SubNav__subHeading": string;
   readonly "fade-in": string;
   readonly "fade-in-down": string;
 };
diff --git a/packages/react/src/SubNav/SubNav.test.tsx b/packages/react/src/SubNav/SubNav.test.tsx
index 56b1c1fdff..8541fc3d0f 100644
--- a/packages/react/src/SubNav/SubNav.test.tsx
+++ b/packages/react/src/SubNav/SubNav.test.tsx
@@ -79,28 +79,56 @@ describe('SubNav', () => {
   it('has a button that opens the menu when clicked', async () => {
     const {getByRole} = render(<MockSubNavFixture />)
 
-    const buttonEl = getByRole('button', {name: 'Navigation menu. Current page: page three'})
+    let buttonEl = getByRole('button', {name: 'Navigation menu. Current page: page three'})
     const overlayEl = getByRole('list')
-
     expect(overlayEl).not.toHaveClass('SubNav__links-overlay--open')
     expect(buttonEl).toHaveAttribute('aria-expanded', 'false')
 
     await userEvent.click(buttonEl)
 
+    buttonEl = getByRole('button', {name: 'Navigation menu. Current page: page three'})
     expect(overlayEl).toHaveClass('SubNav__links-overlay--open')
     expect(buttonEl).toHaveAttribute('aria-expanded', 'true')
   })
 
-  it('closes the overlay when button is pressed again', async () => {
+  it('retains focus on the button after opening the menu', async () => {
     const {getByRole} = render(<MockSubNavFixture />)
 
     const buttonEl = getByRole('button', {name: 'Navigation menu. Current page: page three'})
+
+    await userEvent.click(buttonEl)
+
+    expect(buttonEl).toHaveFocus()
+    expect(buttonEl).toHaveAttribute('aria-expanded', 'true')
+  })
+
+  it('retains focus on the button after closing the menu', async () => {
+    const {getByRole} = render(<MockSubNavFixture />)
+
+    const buttonEl = getByRole('button', {name: 'Navigation menu. Current page: page three'})
+
+    await userEvent.click(buttonEl)
+    expect(buttonEl).toHaveAttribute('aria-expanded', 'true')
+
+    await userEvent.click(buttonEl)
+
+    expect(buttonEl).toHaveFocus()
+    expect(buttonEl).toHaveAttribute('aria-expanded', 'false')
+  })
+
+  it('closes the overlay when button is pressed again', async () => {
+    const {getByRole} = render(<MockSubNavFixture />)
+
+    let buttonEl = getByRole('button', {name: 'Navigation menu. Current page: page three'})
     const overlayEl = getByRole('list')
 
     await userEvent.click(buttonEl)
     expect(overlayEl).toHaveClass('SubNav__links-overlay--open')
 
+    buttonEl = getByRole('button', {name: 'Navigation menu. Current page: page three'})
+
     await userEvent.click(buttonEl)
+
     expect(overlayEl).not.toHaveClass('SubNav__links-overlay--open')
   })
 
@@ -200,4 +228,21 @@ describe('SubNav', () => {
 
     expect(queryByRole('link', {name: 'Copilot feature page one'})).not.toBeInTheDocument()
   })
+
+  it('renders an optional subheading into the document', () => {
+    const expectedText = 'Subheading'
+    const expectedLink = '#subheading'
+    const {getByRole} = render(
+      <SubNav>
+        <SubNav.Heading href={headingLink}>{heading}</SubNav.Heading>
+        <SubNav.SubHeading href={expectedLink}>{expectedText}</SubNav.SubHeading>
+        <SubNav.Link href="#">Link</SubNav.Link>
+      </SubNav>,
+    )
+
+    const el = getByRole('link', {name: expectedText})
+
+    expect(el).toBeInTheDocument()
+    expect(el).toHaveAttribute('href', expectedLink)
+  })
 })
diff --git a/packages/react/src/SubNav/SubNav.tsx b/packages/react/src/SubNav/SubNav.tsx
index eefa57c340..9b288502c1 100644
--- a/packages/react/src/SubNav/SubNav.tsx
+++ b/packages/react/src/SubNav/SubNav.tsx
@@ -119,6 +119,34 @@ function SubNavProvider({children}: {children: React.ReactNode}) {
   )
 }
 
+type SeparatorProps = {
+  activeLinklabel?: string
+} & BaseProps<HTMLSpanElement>
+
+function Separator({activeLinklabel}: SeparatorProps) {
+  return (
+    <span
+      role="separator"
+      className={clsx(
+        styles['SubNav__heading-separator'],
+        activeLinklabel && styles['SubNav__heading-separator--has-adjacent-label'],
+      )}
+      aria-hidden
+    >
+      <svg xmlns="http://www.w3.org/2000/svg" width="8" height="16" viewBox="0 0 8 16" fill="none" aria-hidden>
+        <g clipPath="url(#clip0_50_1307)">
+          <path d="M0 15.2992L5.472 0.701172H7.632L2.16 15.2992H0Z" fill="currentColor" />
+        </g>
+        <defs>
+          <clipPath id="clip0_50_1307">
+            <rect width="7.632" height="14.598" transform="translate(0 0.701172)" />
+          </clipPath>
+        </defs>
+      </svg>
+    </span>
+  )
+}
+
 export type SubNavProps = {
   hasShadow?: boolean
   /**
@@ -133,6 +161,7 @@ const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullW
   const rootRef = React.useRef<HTMLDivElement>(null)
   const navRef = React.useRef<HTMLElement>(null)
   const overlayRef = React.useRef<HTMLUListElement>(null)
+  const narrowButtonRef = useRef<HTMLButtonElement>(null)
   const [isOpenAtNarrow, setIsOpenAtNarrow] = useState(false)
   const idForLinkContainer = useId()
   const [hasAnchoredNav, setHasAnchoredNav] = useState(false)
@@ -183,13 +212,16 @@ const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullW
 
   const {
     heading: HeadingChild,
+    subheading: SubHeadingChild,
     links: LinkChildren,
     action: ActionChild,
   } = childrenArr.reduce(
-    (acc: {heading?: ReactNode; links: ReactElement[]; action?: ReactNode}, child) => {
+    (acc: {heading?: ReactNode; subheading?: ReactNode; links: ReactElement[]; action?: ReactNode}, child) => {
       if (isValidElement(child)) {
         if (child.type === SubNavHeading) {
           acc.heading = child
+        } else if (child.type === SubNavSubHeading) {
+          acc.subheading = child
         } else if (child.type === SubNavLink) {
           const [link, subMenu] = child.props.children
 
@@ -213,7 +245,7 @@ const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullW
       }
       return acc
     },
-    {heading: undefined, links: [], action: undefined},
+    {heading: undefined, subheading: undefined, links: [], action: undefined},
   )
 
   const activeLinklabel =
@@ -221,8 +253,51 @@ const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullW
 
   // needed to prevent rendering of anchor subnav inside the narrow <button> element
   const MaybeSubNav = activeLink?.props.children?.[1]?.props?.variant === 'anchor' && activeLink.props.children?.[1]
+
+  const subHeadingIsActive = isValidElement(SubHeadingChild) && SubHeadingChild.props['aria-current']
+
+  const NarrowButton = useMemo(
+    () => (
+      <button
+        ref={narrowButtonRef}
+        className={clsx(styles['SubNav__overlay-toggle'], isOpenAtNarrow && styles['SubNav__overlay-toggle--open'])}
+        data-testid={testIds.button}
+        onClick={isOpenAtNarrow ? closeMenuCallback : handleMenuToggle}
+        aria-expanded={isOpenAtNarrow ? 'true' : 'false'}
+        aria-controls={idForLinkContainer}
+      >
+        {activeLinklabel && <span className="visually-hidden">Navigation menu. Current page: </span>}
+        <span
+          className={clsx(
+            styles['SubNav__overlay-toggle-content'],
+            !activeLinklabel && styles['SubNav__overlay-toggle-content--end'],
+          )}
+        >
+          {activeLinklabel && (
+            <Text as="span" size="200">
+              {activeLinklabel}
+            </Text>
+          )}
+          {isOpenAtNarrow ? (
+            <ChevronUpIcon className={styles['SubNav__overlay-toggle-icon']} size={24} />
+          ) : (
+            <ChevronDownIcon className={styles['SubNav__overlay-toggle-icon']} size={24} />
+          )}
+        </span>
+      </button>
+    ),
+    [activeLinklabel, closeMenuCallback, handleMenuToggle, idForLinkContainer, isOpenAtNarrow],
+  )
+
   return (
-    <div className={clsx(styles['SubNav__container'], hasAnchoredNav && styles['SubNav__container--with-anchor-nav'])}>
+    <div
+      className={clsx(
+        styles['SubNav__container'],
+        SubHeadingChild && styles['SubNav--has-sub-heading'],
+        SubHeadingChild && subHeadingIsActive && styles['SubNav--subHeadingActive'],
+        hasAnchoredNav && styles['SubNav__container--with-anchor-nav'],
+      )}
+    >
       <SubNavProvider>
         <nav
           ref={navRef}
@@ -239,65 +314,19 @@ const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullW
           <div ref={rootRef} className={styles['SubNav--header-container-outer']}>
             <div className={styles['SubNav__header-container']}>
               {HeadingChild && <div className={styles['SubNav__heading-container']}>{HeadingChild}</div>}
-              <span
-                role="separator"
-                className={clsx(
-                  styles['SubNav__heading-separator'],
-                  activeLinklabel && styles['SubNav__heading-separator--has-adjacent-label'],
-                )}
-                aria-hidden
-              >
-                <svg
-                  xmlns="http://www.w3.org/2000/svg"
-                  width="8"
-                  height="16"
-                  viewBox="0 0 8 16"
-                  fill="none"
-                  aria-hidden
-                >
-                  <g clipPath="url(#clip0_50_1307)">
-                    <path d="M0 15.2992L5.472 0.701172H7.632L2.16 15.2992H0Z" fill="currentColor" />
-                  </g>
-                  <defs>
-                    <clipPath id="clip0_50_1307">
-                      <rect width="7.632" height="14.598" transform="translate(0 0.701172)" />
-                    </clipPath>
-                  </defs>
-                </svg>
-              </span>
-              {!isLarge && (
-                <button
-                  className={clsx(
-                    styles['SubNav__overlay-toggle'],
-                    isOpenAtNarrow && styles['SubNav__overlay-toggle--open'],
-                  )}
-                  data-testid={testIds.button}
-                  onClick={isOpenAtNarrow ? closeMenuCallback : handleMenuToggle}
-                  aria-expanded={isOpenAtNarrow ? 'true' : 'false'}
-                  aria-controls={idForLinkContainer}
-                >
-                  {activeLinklabel && <span className="visually-hidden">Navigation menu. Current page: </span>}
-                  <span
-                    className={clsx(
-                      styles['SubNav__overlay-toggle-content'],
-                      !activeLinklabel && styles['SubNav__overlay-toggle-content--end'],
-                    )}
-                  >
-                    {activeLinklabel && (
-                      <Text as="span" size="200">
-                        {activeLinklabel}
-                      </Text>
-                    )}
-                    {isOpenAtNarrow ? (
-                      <ChevronUpIcon className={styles['SubNav__overlay-toggle-icon']} size={24} />
-                    ) : (
-                      <ChevronDownIcon className={styles['SubNav__overlay-toggle-icon']} size={24} />
-                    )}
-                  </span>
-                </button>
+              <Separator activeLinklabel={activeLinklabel} />
+              {SubHeadingChild && (isLarge || !subHeadingIsActive) && (
+                <>
+                  <div className={styles['SubNav__heading-container']}>{SubHeadingChild}</div>
+                  <Separator activeLinklabel={activeLinklabel} />
+                </>
               )}
+
+              {!isLarge && (!SubHeadingChild || subHeadingIsActive) && NarrowButton}
+
               {MaybeSubNav && MaybeSubNav}
             </div>
+            {!isLarge && SubHeadingChild && !subHeadingIsActive && NarrowButton}
             {LinkChildren.length && (
               <ul
                 ref={overlayRef}
@@ -338,6 +367,25 @@ const SubNavHeading = ({href, children, className, 'data-testid': testID, ...pro
   )
 }
 
+type SubNavSubHeadingProps = {
+  href: string
+  'data-testid'?: string
+} & PropsWithChildren<React.HTMLProps<HTMLAnchorElement>> &
+  BaseProps<HTMLAnchorElement>
+
+const SubNavSubHeading = ({href, children, className, 'data-testid': testID, ...props}: SubNavSubHeadingProps) => {
+  return (
+    <a
+      href={href}
+      className={clsx(styles['SubNav__heading'], styles['SubNav__link'], styles['SubNav__subHeading'], className)}
+      data-testid={testIds.heading || testID}
+      {...props}
+    >
+      {children}
+    </a>
+  )
+}
+
 type SubNavLinkProps = {
   href: string
   'data-testid'?: string
@@ -608,6 +656,7 @@ function _SubNavAction({children, href, variant = 'primary', size = 'small', ...
  */
 export const SubNav = Object.assign(_SubNavRoot, {
   Heading: SubNavHeading,
+  SubHeading: SubNavSubHeading,
   Link: SubNavLink,
   Action: _SubNavAction,
   SubMenu: _SubMenu,
diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts b/packages/react/src/SubNav/SubNav.visual.spec.ts
index a1b3e99b79..570d5c8687 100644
--- a/packages/react/src/SubNav/SubNav.visual.spec.ts
+++ b/packages/react/src/SubNav/SubNav.visual.spec.ts
@@ -55,6 +55,72 @@ test.describe('Visual Comparison: SubNav', () => {
     expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
   })
 
+  test('SubNav / Sub Heading', async ({page}) => {
+    await page.goto('http://localhost:6006/iframe.html?args=&id=components-subnav-features--sub-heading&viewMode=story')
+
+    await page.waitForTimeout(500)
+    expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
+  })
+
+  // eslint-disable-next-line i18n-text/no-en
+  test.describe('Mobile viewport test for Sub Heading Narrow', () => {
+    test.use({viewport: {width: 360, height: 800}})
+    test('SubNav / Sub Heading Narrow', async ({page}) => {
+      await page.goto(
+        'http://localhost:6006/iframe.html?args=&id=components-subnav-features--sub-heading-narrow&viewMode=story',
+      )
+
+      await page.waitForTimeout(500)
+      expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
+    })
+  })
+
+  // eslint-disable-next-line i18n-text/no-en
+  test.describe('Mobile viewport test for Sub Heading Narrow Open', () => {
+    test.use({viewport: {width: 360, height: 800}})
+    test('SubNav / Sub Heading Narrow Open', async ({page}) => {
+      await page.goto(
+        'http://localhost:6006/iframe.html?args=&id=components-subnav-features--sub-heading-narrow-open&viewMode=story',
+      )
+
+      await page.waitForTimeout(500)
+      expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
+    })
+  })
+  test('SubNav / Active Sub Heading', async ({page}) => {
+    await page.goto(
+      'http://localhost:6006/iframe.html?args=&id=components-subnav-features--active-sub-heading&viewMode=story',
+    )
+
+    await page.waitForTimeout(500)
+    expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
+  })
+
+  // eslint-disable-next-line i18n-text/no-en
+  test.describe('Mobile viewport test for Active Sub Heading Narrow', () => {
+    test.use({viewport: {width: 360, height: 800}})
+    test('SubNav / Active Sub Heading Narrow', async ({page}) => {
+      await page.goto(
+        'http://localhost:6006/iframe.html?args=&id=components-subnav-features--active-sub-heading-narrow&viewMode=story',
+      )
+
+      await page.waitForTimeout(500)
+      expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
+    })
+  })
+
+  // eslint-disable-next-line i18n-text/no-en
+  test.describe('Mobile viewport test for Active Sub Heading Narrow Open', () => {
+    test.use({viewport: {width: 360, height: 800}})
+    test('SubNav / Active Sub Heading Narrow Open', async ({page}) => {
+      await page.goto(
+        'http://localhost:6006/iframe.html?args=&id=components-subnav-features--active-sub-heading-narrow-open&viewMode=story',
+      )
+
+      await page.waitForTimeout(500)
+      expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
+    })
+  })
   test('SubNav / Full Width', async ({page}) => {
     await page.goto('http://localhost:6006/iframe.html?args=&id=components-subnav-features--full-width&viewMode=story')
 
diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-0855d-ing-Narrow-SubNav-Active-Sub-Heading-Narrow-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-0855d-ing-Narrow-SubNav-Active-Sub-Heading-Narrow-1-linux.png
new file mode 100644
index 0000000000..2fd539390c
Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-0855d-ing-Narrow-SubNav-Active-Sub-Heading-Narrow-1-linux.png differ
diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-0f54f--Narrow-Open-SubNav-Sub-Heading-Narrow-Open-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-0f54f--Narrow-Open-SubNav-Sub-Heading-Narrow-Open-1-linux.png
new file mode 100644
index 0000000000..c8a9ff19cd
Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-0f54f--Narrow-Open-SubNav-Sub-Heading-Narrow-Open-1-linux.png differ
diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-7134f--Open-SubNav-Active-Sub-Heading-Narrow-Open-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-7134f--Open-SubNav-Active-Sub-Heading-Narrow-Open-1-linux.png
new file mode 100644
index 0000000000..c91ec5ec09
Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-7134f--Open-SubNav-Active-Sub-Heading-Narrow-Open-1-linux.png differ
diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-for-Sub-Heading-Narrow-SubNav-Sub-Heading-Narrow-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-for-Sub-Heading-Narrow-SubNav-Sub-Heading-Narrow-1-linux.png
new file mode 100644
index 0000000000..39a4e66a92
Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-Mobile-viewport-test-for-Sub-Heading-Narrow-SubNav-Sub-Heading-Narrow-1-linux.png differ
diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Active-Sub-Heading-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Active-Sub-Heading-1-linux.png
new file mode 100644
index 0000000000..a6642b9784
Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Active-Sub-Heading-1-linux.png differ
diff --git a/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Sub-Heading-1-linux.png b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Sub-Heading-1-linux.png
new file mode 100644
index 0000000000..8702f47c23
Binary files /dev/null and b/packages/react/src/SubNav/SubNav.visual.spec.ts-snapshots/Visual-Comparison-SubNav-SubNav-Sub-Heading-1-linux.png differ


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new `SubNav.SubHeading` tier for deeper navigation in the `SubNav` component, along with supporting tests, stories, and documentation.

- Adds a `SubNav.SubHeading` component and integrates it into `SubNavRoot`
- Extends visual tests to cover multiple subheading states and viewports
- Updates Storybook stories and documentation to demonstrate the new subheading feature

### Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                             | Description                                      |
| ------------------------------------------------ | ------------------------------------------------ |
| packages/react/src/SubNav/SubNav.tsx             | Added `Separator` helper, `SubNavSubHeading`, integration and focus logic for narrow viewports |
| packages/react/src/SubNav/SubNav.visual.spec.ts  | Added visual snapshot tests for subheadings and mobile variants |
| packages/react/src/SubNav/SubNav.test.tsx        | Added unit test to verify rendering of `SubNav.SubHeading` |
| packages/react/src/SubNav/SubNav.module.css      | Added styling rules for `.SubNav__subHeading` and related states |
| packages/react/src/SubNav/SubNav.features.stories.tsx | Added Storybook stories for subheading features |
| apps/next-docs/content/components/SubNav/index.mdx | Documented `SubNav.SubHeading` usage in docs |
| apps/docs/content/components/SubNav.mdx          | Documented `SubNav.SubHeading` usage in docs |
| .changeset/fresh-ads-begin.md                    | Changelog entry for introducing `SubNav.SubHeading` |
</details>



<details>
<summary>Comments suppressed due to low confidence (4)</summary>

**packages/react/src/SubNav/SubNav.tsx:123**
* [nitpick] The property name `activeLinklabel` should use consistent camelCase as `activeLinkLabel` to match convention and improve readability.
```
activeLinklabel?: string
```
**packages/react/src/SubNav/SubNav.visual.spec.ts:58**
* Visual tests currently capture the light mode view. Add a dark mode snapshot test (e.g. append `&globals=colorMode:dark` to the URL) to ensure the subheading renders correctly in both themes.
```
test('SubNav / Sub Heading', async ({page}) => {
```
**packages/react/src/SubNav/SubNav.tsx:164**
* The code uses `useRef` without importing it. Either import `useRef` from React or use `React.useRef` for consistency with other ref hooks.
```
const narrowButtonRef = useRef<HTMLButtonElement>(null)
```
**packages/react/src/SubNav/SubNav.tsx:388**
* `SubNavSubHeading` reuses `testIds.heading` for its test ID. Introduce a dedicated `testIds.subHeading` to avoid collisions with the main heading.
```
data-testid={testIds.heading || testID}
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-08T12:57:14Z
user: login: simmonsjenna
body: @rezrah This looks good to me in both dark and light mode and across device sizes. The only question I had is: does the lack of side padding in the Storybook examples affect this, or is that provided by its container? I noticed that the examples before and after the new Subheading examples have visible side padding so I was curious if that was a bug or not.
state: APPROVED
author_association: NONE
submitted_at: 2025-05-08T14:48:36Z
user: login: joshfarrant
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T08:22:06Z
user: login: rezrah
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T09:25:10Z
user: login: rezrah
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T09:25:25Z
user: login: rezrah
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T09:26:29Z
user: login: danielguillan
body: Looks good!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-09T09:55:26Z

Review comments:
diff_hunk: @@ -162,6 +191,13 @@ const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullW
     }
   }, [isOpenAtNarrow, isLarge])
 
+  // On narrow viewports the focus should be retained on the button when menu is open
+  useEffect(() => {
+    if (!isLarge && narrowButtonRef.current && isOpenAtNarrow) {
+      narrowButtonRef.current.focus()
+    }
+  }, [isOpenAtNarrow, isLarge])
path: packages/react/src/SubNav/SubNav.tsx
user: login: joshfarrant
body: I think this may not be working as expected as, when the dropdown is collapsed, focus is lost and so returns to the start of the document.

To reproduce, open the narrow-viewport menu using the keyboard, hit <kbd>Escape</kbd> to close it, press tab, then observe that focus now returns to the start of the document.

One way to resolve this is as follows.

```suggestion
  useEffect(() => {
    if (!isLarge && narrowButtonRef.current) {
      narrowButtonRef.current.focus()
    }
  }, [isOpenAtNarrow, isLarge])
```

However I think the fact that we need this code block in the first place is actually indicative of another issue, because removing it entirely causes the button to lose focus every time the menu expands or collapses, which shouldn't naturally happen.

I believe the issue is being caused by the `useCallback` on L264. The callback is recomputed every render (probably due to `activeLinkLabel` changing) and so the button is being recreated each render, meaning that when the `_SubNavRoot` component re-renders when the dropdown expands/collapses, the button is destroyed and recreated, causing it to lose focus.

The easiest way to fix this is just to not `useCallback` the button and instead just store the JSX in a variable, eg:

```tsx
const narrowButton = (
  <button
    ref={narrowButtonRef}
    className={clsx(styles['SubNav__overlay-toggle'], isOpenAtNarrow && styles['SubNav__overlay-toggle--open'])}
    {/* etc */}
  >
    {/* ...*/}
  </button>
)
```

Which you can then use below as

```tsx
{!isLarge && (!SubHeadingChild || subHeadingIsActive) && narrowButton}

// and

{!isLarge && SubHeadingChild && !subHeadingIsActive && narrowButton}
```

This will stop the button being torn down and recreated every render, and allow you to remove the `useEffect` in favour of adding `narrowButtonRef.current?.focus()` on L176
created_at: 2025-05-09T08:07:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,4 +203,21 @@ describe('SubNav', () => {
 
     expect(queryByRole('link', {name: 'Copilot feature page one'})).not.toBeInTheDocument()
   })
+
+  it('renders an optional subheading into the document', () => {
+    const expectedText = 'Subheading'
+    const expectedLink = '#subheading'
+    const {getByText} = render(
+      <SubNav>
+        <SubNav.Heading href={headingLink}>{heading}</SubNav.Heading>
+        <SubNav.SubHeading href={expectedLink}>{expectedText}</SubNav.SubHeading>
+        <SubNav.Link href="#">Link</SubNav.Link>
+      </SubNav>,
+    )
+
+    const el = getByText(expectedText)
path: packages/react/src/SubNav/SubNav.test.tsx
user: login: joshfarrant
body: Very minor, but the following will also check that the element is a link


```suggestion
    const {getByRole} = render(
      <SubNav>
        <SubNav.Heading href={headingLink}>{heading}</SubNav.Heading>
        <SubNav.SubHeading href={expectedLink}>{expectedText}</SubNav.SubHeading>
        <SubNav.Link href="#">Link</SubNav.Link>
      </SubNav>,
    )

    const el = getByRole('link', {name: expectedText})
```
created_at: 2025-05-09T08:15:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,4 +203,21 @@ describe('SubNav', () => {
 
     expect(queryByRole('link', {name: 'Copilot feature page one'})).not.toBeInTheDocument()
   })
+
+  it('renders an optional subheading into the document', () => {
+    const expectedText = 'Subheading'
+    const expectedLink = '#subheading'
+    const {getByText} = render(
+      <SubNav>
+        <SubNav.Heading href={headingLink}>{heading}</SubNav.Heading>
+        <SubNav.SubHeading href={expectedLink}>{expectedText}</SubNav.SubHeading>
+        <SubNav.Link href="#">Link</SubNav.Link>
+      </SubNav>,
+    )
+
+    const el = getByText(expectedText)
+
+    expect(el).toBeInTheDocument()
+    expect(el).toHaveAttribute('href', expectedLink)
+  })
 })
path: packages/react/src/SubNav/SubNav.test.tsx
user: login: joshfarrant
body: If you get chance, it would be great to have a test which checks that focus is returned to the button on narrow submenu close too
created_at: 2025-05-09T08:17:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,6 +92,98 @@ WithShadow.parameters = {
   layout: 'fullscreen',
 }
 
+export const SubHeading = args => (
+  <SubNav {...args}>
+    <SubNav.Heading href="#">AI</SubNav.Heading>
+    <SubNav.SubHeading href="#">Copilot</SubNav.SubHeading>
+    <SubNav.Link href="#">For business</SubNav.Link>
+    <SubNav.Link href="#" aria-current="page">
+      Extensions
+    </SubNav.Link>
+    <SubNav.Link href="#">Tutorials</SubNav.Link>
+    <SubNav.Link href="#">
+      What&apos;s new
+      <SubNav.SubMenu>
+        <SubNav.Link href="#">Sub-feature page one</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page two</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page three</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page four</SubNav.Link>
+      </SubNav.SubMenu>
+    </SubNav.Link>
+    <SubNav.Link href="#">Plans & pricing</SubNav.Link>
+    <SubNav.Action href="#">Call to action</SubNav.Action>
+  </SubNav>
+)
+SubHeading.parameters = {
+  layout: 'fullscreen',
+}
+
+export const SubHeadingNarrow = args => <SubHeading {...args} />
+SubHeadingNarrow.parameters = {
+  layout: 'fullscreen',
+  viewport: {
+    defaultViewport: 'iphonex',
+  },
+}
+
+export const SubHeadingNarrowOpen = args => <SubHeading {...args} />
+SubHeadingNarrowOpen.parameters = {
+  layout: 'fullscreen',
+  viewport: {
+    defaultViewport: 'iphonex',
+  },
+}
+SubHeadingNarrowOpen.play = async ({canvasElement}) => {
+  const canvas = within(canvasElement)
+  await userEvent.click(canvas.getByTestId('SubNav-root-button'))
path: packages/react/src/SubNav/SubNav.features.stories.tsx
user: login: joshfarrant
body: Really minor and appreciate this is more of a demo than a test, but it would be nice to use a `getByRole` in these cases, rather than adding more `getByTestId` uses to the codebase.
created_at: 2025-05-09T08:20:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,6 +92,98 @@ WithShadow.parameters = {
   layout: 'fullscreen',
 }
 
+export const SubHeading = args => (
+  <SubNav {...args}>
+    <SubNav.Heading href="#">AI</SubNav.Heading>
+    <SubNav.SubHeading href="#">Copilot</SubNav.SubHeading>
+    <SubNav.Link href="#">For business</SubNav.Link>
+    <SubNav.Link href="#" aria-current="page">
+      Extensions
+    </SubNav.Link>
+    <SubNav.Link href="#">Tutorials</SubNav.Link>
+    <SubNav.Link href="#">
+      What&apos;s new
+      <SubNav.SubMenu>
+        <SubNav.Link href="#">Sub-feature page one</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page two</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page three</SubNav.Link>
+        <SubNav.Link href="#">Sub-feature page four</SubNav.Link>
+      </SubNav.SubMenu>
+    </SubNav.Link>
+    <SubNav.Link href="#">Plans & pricing</SubNav.Link>
+    <SubNav.Action href="#">Call to action</SubNav.Action>
+  </SubNav>
+)
+SubHeading.parameters = {
+  layout: 'fullscreen',
+}
+
+export const SubHeadingNarrow = args => <SubHeading {...args} />
+SubHeadingNarrow.parameters = {
+  layout: 'fullscreen',
+  viewport: {
+    defaultViewport: 'iphonex',
+  },
+}
+
+export const SubHeadingNarrowOpen = args => <SubHeading {...args} />
+SubHeadingNarrowOpen.parameters = {
+  layout: 'fullscreen',
+  viewport: {
+    defaultViewport: 'iphonex',
+  },
+}
+SubHeadingNarrowOpen.play = async ({canvasElement}) => {
+  const canvas = within(canvasElement)
+  await userEvent.click(canvas.getByTestId('SubNav-root-button'))
path: packages/react/src/SubNav/SubNav.features.stories.tsx
user: login: rezrah
body: Good suggestion. Will defer that for now given the urgency for this to ship.
created_at: 2025-05-09T09:25:10Z
author_association: COLLABORATOR
diff_hunk: @@ -200,4 +203,21 @@ describe('SubNav', () => {
 
     expect(queryByRole('link', {name: 'Copilot feature page one'})).not.toBeInTheDocument()
   })
+
+  it('renders an optional subheading into the document', () => {
+    const expectedText = 'Subheading'
+    const expectedLink = '#subheading'
+    const {getByText} = render(
+      <SubNav>
+        <SubNav.Heading href={headingLink}>{heading}</SubNav.Heading>
+        <SubNav.SubHeading href={expectedLink}>{expectedText}</SubNav.SubHeading>
+        <SubNav.Link href="#">Link</SubNav.Link>
+      </SubNav>,
+    )
+
+    const el = getByText(expectedText)
+
+    expect(el).toBeInTheDocument()
+    expect(el).toHaveAttribute('href', expectedLink)
+  })
 })
path: packages/react/src/SubNav/SubNav.test.tsx
user: login: rezrah
body: 👍 Added
created_at: 2025-05-09T09:25:25Z
author_association: COLLABORATOR
diff_hunk: @@ -162,6 +191,13 @@ const _SubNavRoot = memo(({id, children, className, 'data-testid': testId, fullW
     }
   }, [isOpenAtNarrow, isLarge])
 
+  // On narrow viewports the focus should be retained on the button when menu is open
+  useEffect(() => {
+    if (!isLarge && narrowButtonRef.current && isOpenAtNarrow) {
+      narrowButtonRef.current.focus()
+    }
+  }, [isOpenAtNarrow, isLarge])
path: packages/react/src/SubNav/SubNav.tsx
user: login: rezrah
body: Good suggestion. I was scratching my head about this one, and went with the latter option with added memoization. Works as before now.
created_at: 2025-05-09T09:26:29Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-08T11:28:45Z
body: ###  🦋  Changeset detected

Latest commit: be1e117ae05682f5c16aa0b76ff4447cf8fe31ed

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 7 packages</summary>

  | Name                     | Type  |
| ------------------------ | ----- |
| @primer/react-brand      | Patch |
| @primer/brand-docs       | Patch |
| @primer/brand-primitives | Patch |
| @primer/brand-e2e        | Patch |
| @primer/brand-fonts      | Patch |
| @primer/brand-config     | Patch |
| @primer/brand-storybook  | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/primer/brand/new/rezrah/subnav-subheading-update?filename=.changeset/empty-ways-retire.md&value=---%0A%22%40primer%2Freact-brand%22%3A%20patch%0A%22%40primer%2Fbrand-docs%22%3A%20patch%0A---%0A%0AAdd%20subheading%20to%20SubNav%20component%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-08T11:30:24Z
body: ### 🟢 No design token changes found


<hidden purpose="for-rewritable-pr-comment-action-use" value="primitives-diff"></hidden>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T11:34:12Z
body: ### 🟢 No visual differences found

Our visual comparison tests did not find any differences in the UI.


<hidden purpose="for-rewritable-pr-comment-action-use" value="visual-comparison-diff"></hidden>
author_association: CONTRIBUTOR
user: login: rezrah
created_at: 2025-05-08T15:24:30Z
body: > @rezrah This looks good to me in both dark and light mode and across device sizes. The only question I had is: does the lack of side padding in the Storybook examples affect this, or is that provided by its container? I noticed that the examples before and after the new Subheading examples have visible side padding so I was curious if that was a bug or not.

Thanks for spotting that @simmonsjenna. I mistakenly left the `fullWidth` option enabled. After removing it, the default padding has been restored. Quick before/afters:

![Screenshot 2025-05-08 at 16 17 21](https://github.com/user-attachments/assets/d66daf40-e2bf-4739-b75e-8dcdf69eec80)


author_association: COLLABORATOR
user: login: joshfarrant
created_at: 2025-05-09T10:16:12Z
body: Thanks for addressing the comments @rezrah 🙇

Just a heads up that the memoization added around the `<button>` ([mentioned here](https://github.com/primer/brand/pull/1001#discussion_r2081296740)) isn't actually providing any benefit. If you add a `console.count('rendering button')` into the body of the function inside `useMemo` then open/close the overlay then you can see that it actually gets re-rendered every time the `_SubNavRoot` renders, so all `useMemo` is doing is adding memory overhead 

https://github.com/user-attachments/assets/9b1e0783-dfce-4182-80b2-19a908b2e5cd

Not a big deal either way, but as it provides no benefit and (slightly) hurts performance I'd lean towards getting rid of it
author_association: CONTRIBUTOR