diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx
index b0ae08add..06e6ccb78 100644
--- a/apps/www/src/app/examples/page.tsx
+++ b/apps/www/src/app/examples/page.tsx
@@ -111,7 +111,28 @@ const Page = () => {
Analytics
-
+ alert('Resources trailing icon clicked')}
+ aria-label='Resources group actions'
+ style={{
+ border: 0,
+ background: 'transparent',
+ color: 'inherit',
+ padding: 0,
+ display: 'inline-flex',
+ alignItems: 'center',
+ cursor: 'pointer'
+ }}
+ >
+
+
+ }
+ >
}>
Reports
@@ -132,7 +153,27 @@ const Page = () => {
-
+ alert('Account trailing icon clicked')}
+ aria-label='Account group actions'
+ style={{
+ border: 0,
+ background: 'transparent',
+ color: 'inherit',
+ padding: 0,
+ display: 'inline-flex',
+ alignItems: 'center',
+ cursor: 'pointer'
+ }}
+ >
+
+
+ }
+ >
}>
Settings
diff --git a/apps/www/src/content/docs/components/sidebar/demo.ts b/apps/www/src/content/docs/components/sidebar/demo.ts
index 0ad382909..035856134 100644
--- a/apps/www/src/content/docs/components/sidebar/demo.ts
+++ b/apps/www/src/content/docs/components/sidebar/demo.ts
@@ -358,6 +358,38 @@ export const hideTooltipDemo = {
`)
};
+export const accordionGroupDemo = {
+ type: 'code',
+ code: sidebarLayout(`
+
+
+
+
+
+ Apsara
+
+
+
+ }>
+ Overview
+
+
+ }>
+ Reports
+
+ }>
+ Activities
+
+
+ }>
+ }>
+ Settings
+
+
+
+ `)
+};
+
export const moreDemo = {
type: 'code',
code: sidebarLayout(`
diff --git a/apps/www/src/content/docs/components/sidebar/index.mdx b/apps/www/src/content/docs/components/sidebar/index.mdx
index 3bc09f882..80b405ddb 100644
--- a/apps/www/src/content/docs/components/sidebar/index.mdx
+++ b/apps/www/src/content/docs/components/sidebar/index.mdx
@@ -8,6 +8,7 @@ import {
preview,
positionDemo,
variantDemo,
+ accordionGroupDemo,
stateDemo,
tooltipDemo,
collapsibleDemo,
@@ -122,6 +123,12 @@ Set `hideCollapsedItemTooltip` to disable tooltips on navigation items when the
+### Accordion Group
+
+Enable `accordion` on `Sidebar.Group` to make section items collapsible. You can also pass `trailingIcon` for section-level actions.
+
+
+
### More
Use `Sidebar.More` when you want to keep a section compact and move secondary items into a menu.
diff --git a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx
index 88bacde54..b3e581706 100644
--- a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx
+++ b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx
@@ -285,6 +285,154 @@ describe('Sidebar', () => {
const group = screen.getByLabelText(MAIN_GROUP_LABEL);
expect(group).toBeInTheDocument();
});
+
+ it('renders accordion trigger when accordion is enabled', () => {
+ render(
+
+
+ }
+ >
+ }>
+ {DASHBOARD_ITEM_TEXT}
+
+
+
+
+ );
+
+ const trigger = screen.getByRole('button', { name: /Main/ });
+ expect(trigger).toBeInTheDocument();
+ expect(trigger).toHaveAttribute('data-panel-open');
+ });
+
+ it('toggles group items when accordion is enabled', () => {
+ render(
+
+
+ }
+ >
+ }>
+ {DASHBOARD_ITEM_TEXT}
+
+
+
+
+ );
+
+ const trigger = screen.getByRole('button', { name: /Main/ });
+ expect(screen.getByText(DASHBOARD_ITEM_TEXT)).toBeInTheDocument();
+
+ fireEvent.click(trigger);
+ expect(screen.queryByText(DASHBOARD_ITEM_TEXT)).not.toBeInTheDocument();
+
+ fireEvent.click(trigger);
+ expect(screen.getByText(DASHBOARD_ITEM_TEXT)).toBeInTheDocument();
+ });
+
+ it('forces accordion panel open when sidebar is collapsed', () => {
+ const { rerender } = render(
+
+
+ }
+ >
+ }>
+ {DASHBOARD_ITEM_TEXT}
+
+
+
+
+ );
+
+ const trigger = screen.getByRole('button', { name: /Main/ });
+ fireEvent.click(trigger);
+ expect(screen.queryByText(DASHBOARD_ITEM_TEXT)).not.toBeInTheDocument();
+
+ rerender(
+
+
+ }
+ >
+ }>
+ {DASHBOARD_ITEM_TEXT}
+
+
+
+
+ );
+
+ expect(
+ screen.getByRole('listitem', { name: DASHBOARD_ITEM_TEXT })
+ ).toBeInTheDocument();
+ });
+
+ it('renders right icon when provided in accordion header', () => {
+ render(
+
+
+ +}
+ >
+ }>
+ {DASHBOARD_ITEM_TEXT}
+
+
+
+
+ );
+
+ expect(screen.getByTestId('group-trailing-icon')).toBeInTheDocument();
+ });
+
+ it('does not toggle accordion when trailing icon is clicked', () => {
+ const onTrailingIconClick = vi.fn();
+
+ render(
+
+
+
+ +
+
+ }
+ >
+ }>
+ {DASHBOARD_ITEM_TEXT}
+
+
+
+
+ );
+
+ const trigger = screen.getByRole('button', { name: /Main/ });
+ expect(trigger).toHaveAttribute('data-panel-open');
+
+ fireEvent.click(screen.getByTestId('group-trailing-action'));
+
+ expect(onTrailingIconClick).toHaveBeenCalledTimes(1);
+ expect(trigger).toHaveAttribute('data-panel-open');
+ expect(screen.getByText(DASHBOARD_ITEM_TEXT)).toBeInTheDocument();
+ });
});
describe('Sidebar More', () => {
diff --git a/packages/raystack/components/sidebar/sidebar-misc.tsx b/packages/raystack/components/sidebar/sidebar-misc.tsx
index 9c4b182e5..f782f67d1 100644
--- a/packages/raystack/components/sidebar/sidebar-misc.tsx
+++ b/packages/raystack/components/sidebar/sidebar-misc.tsx
@@ -1,9 +1,12 @@
'use client';
+import { Accordion as AccordionPrimitive } from '@base-ui/react';
+import { TriangleDownIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
-import { ComponentProps, ReactNode } from 'react';
+import { ComponentProps, ReactNode, useContext } from 'react';
import { Flex } from '../flex';
import styles from './sidebar.module.css';
+import { SidebarContext } from './sidebar-root';
export function SidebarHeader({
className,
@@ -38,50 +41,142 @@ SidebarFooter.displayName = 'Sidebar.Footer';
export interface SidebarNavigationGroupProps extends ComponentProps<'section'> {
label: string;
+ value?: string;
+ accordion?: boolean;
leadingIcon?: ReactNode;
+ trailingIcon?: ReactNode;
classNames?: {
header?: string;
items?: string;
label?: string;
icon?: string;
+ trigger?: string;
+ chevron?: string;
+ trailingIcon?: string;
};
}
export function SidebarNavigationGroup({
className,
label,
+ value,
+ accordion = false,
leadingIcon,
+ trailingIcon,
classNames,
children,
...props
}: SidebarNavigationGroupProps) {
+ const { isCollapsed } = useContext(SidebarContext);
+ const groupValue = value ?? label;
+
+ if (!accordion) {
+ return (
+
+
+ {leadingIcon && (
+
+ {leadingIcon}
+
+ )}
+
+ {label}
+
+ {trailingIcon ? (
+
+ {trailingIcon}
+
+ ) : null}
+
+
+ {children}
+
+
+ );
+ }
+
return (
-
- {leadingIcon && (
-
- {leadingIcon}
-
- )}
-
- {label}
-
-
-
- {children}
-
+
+
+
+ {leadingIcon && (
+
+ {leadingIcon}
+
+ )}
+
+ {label}
+
+
+
+ {trailingIcon ? (
+
+ {trailingIcon}
+
+ ) : null}
+
+
+
+ {children}
+
+
+
+
);
}
diff --git a/packages/raystack/components/sidebar/sidebar.module.css b/packages/raystack/components/sidebar/sidebar.module.css
index 5dca08c66..8509dd2a8 100644
--- a/packages/raystack/components/sidebar/sidebar.module.css
+++ b/packages/raystack/components/sidebar/sidebar.module.css
@@ -208,9 +208,28 @@
}
.nav-group-header {
- padding: var(--rs-space-3) var(--rs-space-3);
+ display: flex;
+ height: var(--rs-space-7);
+ padding: var(--rs-space-2) var(--rs-space-3);
+ margin-bottom: var(--rs-space-1);
+ justify-content: space-between;
+ align-items: center;
+ align-self: stretch;
color: var(--rs-color-foreground-base-secondary);
margin-top: var(--rs-space-4);
+ border-radius: var(--rs-radius-2);
+}
+
+.nav-group-accordion-item .nav-group-header:hover {
+ background-color: var(--rs-color-background-base-primary-hover);
+}
+
+.nav-group-header-with-trailing:hover {
+ background-color: var(--rs-color-background-base-primary-hover);
+}
+
+.nav-group-accordion-item .nav-group-header {
+ padding: 0;
}
.nav-group-header:first-child {
@@ -226,11 +245,70 @@
letter-spacing: var(--rs-letter-spacing-small);
}
+.nav-group-trigger {
+ display: flex;
+ width: auto;
+ flex: 1;
+ height: 100%;
+ border: 0;
+ padding: var(--rs-space-2) var(--rs-space-2) var(--rs-space-2) var(--rs-space-3);
+ background: transparent;
+ align-items: center;
+ gap: var(--rs-space-2);
+ cursor: pointer;
+ color: inherit;
+ text-align: left;
+}
+
+.nav-group-chevron {
+ width: var(--rs-space-4);
+ height: var(--rs-space-4);
+ color: var(--rs-color-foreground-base-secondary);
+ transform: rotate(-90deg);
+ transition: transform 0.2s ease;
+ flex-shrink: 0;
+}
+
+.nav-group-trigger[data-panel-open] .nav-group-chevron {
+ transform: rotate(0deg);
+}
+
+.nav-group-trailing-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--rs-space-2) var(--rs-space-3);
+ color: var(--rs-color-foreground-base-secondary);
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+.nav-group-header:hover .nav-group-trailing-icon {
+ opacity: 1;
+}
+
.nav-group-items {
gap: var(--rs-space-2);
width: 100%;
}
+.nav-group-accordion,
+.nav-group-accordion-item,
+.nav-group-panel {
+ width: 100%;
+}
+
+.nav-group-panel {
+ height: var(--accordion-panel-height);
+ overflow: hidden;
+ transition: height 0.2s ease;
+}
+
+.nav-group-panel[data-starting-style],
+.nav-group-panel[data-ending-style] {
+ height: 0;
+}
+
/* Hide group header text when collapsed but show a separator line in its place */
.root[data-closed] .nav-group-header {
visibility: hidden;
@@ -259,12 +337,17 @@
/* Keep in flow (no display: none) so header row height is preserved */
}
+.root[data-closed] .nav-group-chevron {
+ display: none;
+}
+
@media (prefers-reduced-motion: reduce) {
.root,
.nav-item,
.nav-text,
- .resizeHandle {
+ .resizeHandle,
+ .nav-group-panel {
transition: none;
}
}
\ No newline at end of file