Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
350 changes: 58 additions & 292 deletions packages/@react-spectrum/s2/src/SideNav.tsx

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/RoutedSideNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';
import {RouterProvider} from 'react-aria-components';
import React, {ReactNode, useState} from 'react';

export function RoutedSideNav(props: {
children: ({selectedRoute}: {selectedRoute: string}) => ReactNode;
defaultSelectedRoute: string;
}) {
let {children} = props;
let [selectedRoute, setSelectedRoute] = useState<string>(props.defaultSelectedRoute);

let updateSelection = (href: string) => {
setSelectedRoute(href);
};

return <RouterProvider navigate={updateSelection}>{children({selectedRoute})}</RouterProvider>;
}
204 changes: 204 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/SideNav.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {Layout} from '../../src/Layout';
export default Layout;

import docs from 'docs:react-aria-components';
import '../../tailwind/tailwind.css';
import {InlineAlert, Heading, Content} from '@react-spectrum/s2';

export const tags = ['navigation', 'nav', 'sidebar'];
export const version = 'alpha';
export const description = 'A navigation component that displays a nested, hierarchical set of links, with support for keyboard navigation and a current route indicator.';

# SideNav

<PageDescription>{docs.exports.SideNav.description}</PageDescription>

<ExampleSwitcher>
```tsx render docs={docs.exports.SideNav} links={docs.links} props={[]} type="vanilla" files={["starters/docs/src/SideNav.tsx", "starters/docs/src/SideNav.css", "./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem} from 'vanilla-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

<RoutedSideNav defaultSelectedRoute="/photos">
{({selectedRoute}) => (
<SideNav aria-label="Files" selectedRoute={selectedRoute} defaultExpandedKeys={['files']}>
<SideNavItem id="home" href="/home" title="Home" />
<SideNavItem id="files" href="/files" title="Files">
<SideNavItem id="photos" href="/photos" title="Photos" />
<SideNavItem id="videos" href="/videos" title="Videos" />
</SideNavItem>
<SideNavItem id="shared" href="/shared" title="Shared" />
</SideNav>
)}
</RoutedSideNav>
```

```tsx render docs={docs.exports.SideNav} links={docs.links} props={[]} type="tailwind" files={["starters/tailwind/src/SideNav.tsx", "./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem} from 'tailwind-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

<RoutedSideNav defaultSelectedRoute="/photos">
{({selectedRoute}) => (
<SideNav aria-label="Files" selectedRoute={selectedRoute} defaultExpandedKeys={['files']}>
<SideNavItem id="home" href="/home" title="Home" />
<SideNavItem id="files" href="/files" title="Files">
<SideNavItem id="photos" href="/photos" title="Photos" />
<SideNavItem id="videos" href="/videos" title="Videos" />
</SideNavItem>
<SideNavItem id="shared" href="/shared" title="Shared" />
</SideNav>
)}
</RoutedSideNav>
```

</ExampleSwitcher>

<InlineAlert variant="notice">
<Heading>Accessibility</Heading>
<Content>`SideNav` renders as a tree so keyboard users can navigate and expand the hierarchy. When it acts as the main navigation for a page, place it inside a [navigation landmark](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/navigation.html): wrap the `SideNav` in a `<nav>` element with an `aria-label` so assistive technology users can quickly find it.</Content>
</InlineAlert>

## Content

`SideNav` follows the [Collection Components API](collections?component=SideNav), accepting both static and dynamic collections. The example above shows a static collection. This example shows a dynamic collection, passing a list of objects to the `items` prop and a function to render the children.

```tsx render files={["./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem} from 'vanilla-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

function Example() {
let items = [
{id: 'overview', url: '/overview', label: 'Overview'},
{id: 'reports', url: '/reports', label: 'Reports'},
{id: 'settings', url: '/settings', label: 'Settings'}
];

return (
<RoutedSideNav defaultSelectedRoute="/reports">
{({selectedRoute}) => (
/*- begin highlight -*/
<SideNav aria-label="Sections" items={items} selectedRoute={selectedRoute}>
{item => <SideNavItem href={item.url} title={item.label} />}
</SideNav>
/*- end highlight -*/
)}
</RoutedSideNav>
);
}
```

## Current route

Each `SideNavItem` accepts an `href`. Set the `selectedRoute` prop on the `SideNav` to the current page's path, and the item whose `href` matches is marked with `aria-current="page"` (and a `data-current` attribute for styling).

Combine `SideNav` with a client side router by wrapping your app in a [RouterProvider](routing.html) so that activating a link updates the route. Then set `selectedRoute`. In this example the router navigation is stored in local state to show the current route updating as you activate links.

```tsx render
"use client";
import {SideNav, SideNavItem} from 'vanilla-starter/SideNav';
import {RouterProvider} from 'react-aria-components';
import {useState} from 'react';

function Example() {
let [route, setRoute] = useState('/inbox');
return (
/*- begin highlight -*/
<RouterProvider navigate={setRoute}>
<SideNav aria-label="Mail" selectedRoute={route}>
<SideNavItem href="/inbox" title="Inbox" />
<SideNavItem href="/drafts" title="Drafts" />
<SideNavItem href="/sent" title="Sent" />
</SideNav>
</RouterProvider>
/*- end highlight -*/
);
}
```

## Sections

Use `SideNavSection` to group related items, with an optional `SideNavHeader` to label each group.

```tsx render files={["./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem, SideNavSection, SideNavHeader} from 'vanilla-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

<RoutedSideNav defaultSelectedRoute="/projects/apollo">
{({selectedRoute}) => (
<SideNav aria-label="Workspace" selectedRoute={selectedRoute}>
{/*- begin highlight -*/}
<SideNavSection>
<SideNavHeader>Personal</SideNavHeader>
<SideNavItem href="/home" title="Home" />
<SideNavItem href="/starred" title="Starred" />
</SideNavSection>
{/*- end highlight -*/}
<SideNavSection>
<SideNavHeader>Projects</SideNavHeader>
<SideNavItem href="/projects/apollo" title="Apollo" />
<SideNavItem href="/projects/gemini" title="Gemini" />
</SideNavSection>
</SideNav>
)}
</RoutedSideNav>
```

## Disabled

Set the `isDisabled` prop on a `SideNavItem` to disable it. Disabled items cannot be navigated to or focused.

```tsx render files={["./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem} from 'vanilla-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

<RoutedSideNav defaultSelectedRoute="/home">
{({selectedRoute}) => (
<SideNav aria-label="Files" selectedRoute={selectedRoute}>
<SideNavItem href="/home" title="Home" />
{/*- begin highlight -*/}
<SideNavItem href="/archive" title="Archive" isDisabled />
{/*- end highlight -*/}
<SideNavItem href="/trash" title="Trash" />
</SideNav>
)}
</RoutedSideNav>
```

## API

```tsx links={{SideNav: '#sidenav', SideNavItem: '#sidenavitem', SideNavItemContent: '#sidenavitemcontent', SideNavSection: '#sidenavsection', SideNavHeader: '#sidenavheader', Link: 'Link'}}
<SideNav>
<SideNavSection>
<SideNavHeader />
<SideNavItem>
<SideNavItemContent>
<Link />
</SideNavItemContent>
</SideNavItem>
</SideNavSection>
</SideNav>
```

### SideNav

<PropTable component={docs.exports.SideNav} links={docs.links} showDescription />

### SideNavItem

<PropTable component={docs.exports.SideNavItem} links={docs.links} showDescription />

### SideNavItemContent

<PropTable component={docs.exports.SideNavItemContent} links={docs.links} showDescription />

### SideNavSection

<PropTable component={docs.exports.SideNavSection} links={docs.links} showDescription />

### SideNavHeader

<PropTable component={docs.exports.SideNavHeader} links={docs.links} showDescription />
37 changes: 37 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';
import React, {createContext, ReactNode, useContext, useState} from 'react';

// A tiny in-memory router that mirrors the parts of the `react-router` API used
// to integrate with React Aria. In a real app, `MemoryRouter`, `useNavigate`, and
// `useLocation` would come from the `react-router` package — the wiring above is
// identical.

interface Location {
pathname: string;
}

interface RouterContextValue {
location: Location;
navigate: (pathname: string) => void;
}

const RouterContext = createContext<RouterContextValue>({
location: {pathname: '/'},
navigate: () => {}
});

export function MemoryRouter(props: {initialEntries?: string[]; children: ReactNode}) {
let {initialEntries = ['/'], children} = props;
let [location, setLocation] = useState<Location>({pathname: initialEntries[0]});
let navigate = (pathname: string) => setLocation({pathname});

return <RouterContext.Provider value={{location, navigate}}>{children}</RouterContext.Provider>;
}

export function useLocation(): Location {
return useContext(RouterContext).location;
}

export function useNavigate(): (pathname: string) => void {
return useContext(RouterContext).navigate;
}
41 changes: 41 additions & 0 deletions packages/react-aria-components/exports/SideNav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

// Mark as a client only package. This will cause a build time error if you try
// to import it from a React Server Component in a framework like Next.js.
import 'client-only';

export {
SideNav,
SideNavItem,
SideNavItemContent,
SideNavSection,
SideNavHeader,
SideNavContext
} from '../src/SideNav';
export type {
SideNavProps,
SideNavRenderProps,
SideNavItemProps,
SideNavItemRenderProps,
SideNavItemContentProps,
SideNavItemContentRenderProps,
SideNavSectionProps,
SideNavHeaderProps
} from '../src/SideNav';
export type {Key} from '@react-types/shared';

export {Button} from '../src/Button';
export type {ButtonProps, ButtonRenderProps} from '../src/Button';

export {Link} from '../src/Link';
export type {LinkProps, LinkRenderProps} from '../src/Link';
18 changes: 18 additions & 0 deletions packages/react-aria-components/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ export {
} from '../src/ToggleButtonGroup';
export {Toolbar, ToolbarContext} from '../src/Toolbar';
export {TooltipTrigger, Tooltip, TooltipTriggerStateContext, TooltipContext} from '../src/Tooltip';
export {
SideNav,
SideNavItem,
SideNavItemContent,
SideNavSection,
SideNavHeader,
SideNavContext
} from '../src/SideNav';
export {
TreeLoadMoreItem,
Tree,
Expand Down Expand Up @@ -517,6 +525,16 @@ export type {ToggleButtonProps, ToggleButtonRenderProps} from '../src/ToggleButt
export type {ToggleButtonGroupProps, ToggleButtonGroupRenderProps} from '../src/ToggleButtonGroup';
export type {ToolbarProps, ToolbarRenderProps} from '../src/Toolbar';
export type {TooltipProps, TooltipRenderProps, TooltipTriggerComponentProps} from '../src/Tooltip';
export type {
SideNavProps,
SideNavRenderProps,
SideNavItemProps,
SideNavItemRenderProps,
SideNavItemContentProps,
SideNavItemContentRenderProps,
SideNavSectionProps,
SideNavHeaderProps
} from '../src/SideNav';
export type {
TreeProps,
TreeRenderProps,
Expand Down
Loading