Skip to content

feat: RAC NavigationTree - #10404

Open
snowystinger wants to merge 8 commits into
mainfrom
rac-sidenav
Open

feat: RAC NavigationTree#10404
snowystinger wants to merge 8 commits into
mainfrom
rac-sidenav

Conversation

@snowystinger

@snowystinger snowystinger commented Jul 31, 2026

Copy link
Copy Markdown
Member

Closes

Adds the RAC implementation of SideNav. Rewrites S2 to use that. Adds starter examples and docs page for new RAC.

Switches Docs SideNav over to our S2 SideNav.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@rspbot

rspbot commented Jul 31, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 31, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 31, 2026

Copy link
Copy Markdown

Comment thread packages/react-aria-components/src/NavigationTree.tsx

let defaultRef = useRef<HTMLDivElement>(null);
let mergedRef = mergeRefs(ref, defaultRef);
useEffect(() => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we always try to scroll the current item into view whenever it changes? Should we have a prop to disable this behaviour entirely? should there be an option to only do it on mount and no other time?

@nwidynski nwidynski Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fyi, I believe scroll-on-mount should be handled more holistically with an integration of the upcoming scroll-initial-target CSS attribute. Ideally that would be a mechanic for all collections, and also integrate with virtualizer to push the item into persisted keys.

Related to this, Im also not quite sure whether the current key here is being persisted? How does Tree learn of the Items quasi selection state? Is it just through “current” always being kept in-sync with the focusedKey?

@rspbot

rspbot commented Aug 3, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 3, 2026

Copy link
Copy Markdown

Comment thread packages/dev/s2-docs/pages/react-aria/SideNav.mdx Outdated

@yihuiliao yihuiliao Aug 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since i have scrollbar's on, when you expand a section, the scrollbar appears shifting the chevron to the left. we have the same issue on our current docs but it feels less noticeable bc the chevron is always stable (since it's positioned on the left). should we consider adding a scrollbar-gutter to sidenav?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I'm going to see if design has an opinion. Tree sidesteps this issue because the expand button is on the left instead of where the scroll bar would appear

@rspbot

rspbot commented Aug 11, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 11, 2026

Copy link
Copy Markdown

@snowystinger snowystinger changed the title feat: RAC SideNav feat: RAC NavigationTree Aug 13, 2026
@rspbot

rspbot commented Aug 13, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 13, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 13, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 13, 2026

Copy link
Copy Markdown
## API Changes

react-aria-components

/react-aria-components:NavigationTree

+NavigationTree <T> {
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean | FocusStrategy
+  children?: ReactNode | (T) => ReactNode
+  className?: ClassNameOrFunction<NavigationTreeRenderProps> = 'react-aria-NavigationTree'
+  defaultExpandedKeys?: Iterable<Key>
+  dependencies?: ReadonlyArray<any>
+  disabledKeys?: Iterable<Key>
+  expandedKeys?: Iterable<Key>
+  id?: string
+  items?: Iterable<T>
+  onExpandedChange?: (Set<Key>) => any
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, NavigationTreeRenderProps>
+  selectedRoute?: string | null
+  slot?: string | null
+  style?: StyleOrFunction<NavigationTreeRenderProps>
+}

/react-aria-components:NavigationTreeItem

+NavigationTreeItem <T extends {}> {
+  aria-label?: string
+  children: ReactNode
+  className?: ClassNameOrFunction<NavigationTreeItemRenderProps> = 'react-aria-NavigationTreeItem'
+  download?: boolean | string
+  hasChildItems?: boolean
+  href?: Href
+  hrefLang?: string
+  id?: Key
+  isDisabled?: boolean
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  ping?: string
+  referrerPolicy?: HTMLAttributeReferrerPolicy
+  rel?: string
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, NavigationTreeItemRenderProps>
+  routerOptions?: RouterOptions
+  style?: StyleOrFunction<NavigationTreeItemRenderProps>
+  target?: HTMLAttributeAnchorTarget
+  textValue: string
+}

/react-aria-components:NavigationTreeItemContent

+NavigationTreeItemContent {
+  children: ChildrenOrFunction<NavigationTreeItemContentRenderProps>
+}

/react-aria-components:NavigationTreeSection

+NavigationTreeSection <T extends {}> {
+  aria-label?: string
+  children?: ReactNode | (T) => ReactElement
+  className?: string
+  dependencies?: ReadonlyArray<any>
+  id?: Key
+  items?: Iterable<T>
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  style?: CSSProperties
+}

/react-aria-components:NavigationTreeHeader

+NavigationTreeHeader {
+  children?: ReactNode
+  className?: string
+  id?: string
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  style?: CSSProperties
+}

/react-aria-components:NavigationTreeContext

+NavigationTreeContext {
+  UNTYPED
+}

/react-aria-components:NavigationTreeProps

+NavigationTreeProps <T> {
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean | FocusStrategy
+  children?: ReactNode | (T) => ReactNode
+  className?: ClassNameOrFunction<NavigationTreeRenderProps> = 'react-aria-NavigationTree'
+  defaultExpandedKeys?: Iterable<Key>
+  dependencies?: ReadonlyArray<any>
+  disabledKeys?: Iterable<Key>
+  expandedKeys?: Iterable<Key>
+  id?: string
+  items?: Iterable<T>
+  onExpandedChange?: (Set<Key>) => any
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, NavigationTreeRenderProps>
+  selectedRoute?: string | null
+  slot?: string | null
+  style?: StyleOrFunction<NavigationTreeRenderProps>
+}

/react-aria-components:NavigationTreeRenderProps

+NavigationTreeRenderProps {
+  isEmpty: boolean
+  isFocusVisible: boolean
+  isFocused: boolean
+  state: TreeState<unknown>
+}

/react-aria-components:NavigationTreeItemProps

+NavigationTreeItemProps <T = {}> {
+  aria-label?: string
+  children: ReactNode
+  className?: ClassNameOrFunction<NavigationTreeItemRenderProps> = 'react-aria-NavigationTreeItem'
+  download?: boolean | string
+  hasChildItems?: boolean
+  href?: Href
+  hrefLang?: string
+  id?: Key
+  isDisabled?: boolean
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  ping?: string
+  referrerPolicy?: HTMLAttributeReferrerPolicy
+  rel?: string
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, NavigationTreeItemRenderProps>
+  routerOptions?: RouterOptions
+  style?: StyleOrFunction<NavigationTreeItemRenderProps>
+  target?: HTMLAttributeAnchorTarget
+  textValue: string
+}

/react-aria-components:NavigationTreeItemRenderProps

+NavigationTreeItemRenderProps {
+  allowsDragging?: boolean
+  hasChildItems: boolean
+  id: Key
+  isCurrent: boolean
+  isCurrentAncestor: boolean
+  isDisabled: boolean
+  isDragging?: boolean
+  isDropTarget?: boolean
+  isExpanded: boolean
+  isFocusVisible: boolean
+  isFocusVisibleWithin: boolean
+  isFocused: boolean
+  isHovered: boolean
+  isPressed: boolean
+  isSelected: boolean
+  level: number
+  selectionBehavior: SelectionBehavior
+  selectionMode: SelectionMode
+  state: TreeState<unknown>
+}

/react-aria-components:NavigationTreeItemContentProps

+NavigationTreeItemContentProps {
+  children: ChildrenOrFunction<NavigationTreeItemContentRenderProps>
+}

/react-aria-components:NavigationTreeItemContentRenderProps

+NavigationTreeItemContentRenderProps {
+  allowsDragging?: boolean
+  hasChildItems: boolean
+  id: Key
+  isCurrent: boolean
+  isCurrentAncestor: boolean
+  isDisabled: boolean
+  isDragging?: boolean
+  isDropTarget?: boolean
+  isExpanded: boolean
+  isFocusVisible: boolean
+  isFocusVisibleWithin: boolean
+  isFocused: boolean
+  isHovered: boolean
+  isPressed: boolean
+  isSelected: boolean
+  level: number
+  selectionBehavior: SelectionBehavior
+  selectionMode: SelectionMode
+  state: TreeState<unknown>
+}

/react-aria-components:NavigationTreeSectionProps

+NavigationTreeSectionProps <T> {
+  aria-label?: string
+  children?: ReactNode | (T) => ReactElement
+  className?: string
+  dependencies?: ReadonlyArray<any>
+  id?: Key
+  items?: Iterable<T>
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  style?: CSSProperties
+}

/react-aria-components:NavigationTreeHeaderProps

+NavigationTreeHeaderProps {
+  children?: ReactNode
+  className?: string
+  id?: string
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  style?: CSSProperties
+}

@rspbot

rspbot commented Aug 13, 2026

Copy link
Copy Markdown

Agent Skills Changes

Added (2)
Modified (8)
Install

React Spectrum S2:

npx skills add https://d1pzu54gtk2aed.cloudfront.net/pr/39e6f9a07d0b1a061678642e4b850e1d6e9998bc/

React Aria:

npx skills add https://d5iwopk28bdhl.cloudfront.net/pr/39e6f9a07d0b1a061678642e4b850e1d6e9998bc/

}
```

## Sections

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nest the above sections under Content to match other pages

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the style is a bit heavy for a sidenav. Maybe we can make it a bit closer to what we have in S2 with the selection line


## API

```tsx links={{NavigationTree: '#NavigationTree', NavigationTreeItem: '#NavigationTreeitem', NavigationTreeItemContent: '#NavigationTreeitemcontent', NavigationTreeSection: '#NavigationTreesection', NavigationTreeHeader: '#NavigationTreeheader', Link: 'Link'}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have an anatomy diagram...

}
```

## Current route

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noticed that you can't see the focus ring on the selected item with the current styles

@devongovett devongovett left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking if we want to merge for testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants