diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx
index 2725361f4..008b3a39f 100644
--- a/apps/www/src/app/examples/page.tsx
+++ b/apps/www/src/app/examples/page.tsx
@@ -98,7 +98,7 @@ const Page = () => {
Dashboard
-
+ }>
Analytics
diff --git a/apps/www/src/components/demo/demo.tsx b/apps/www/src/components/demo/demo.tsx
index 83d2b8d66..58ec4b009 100644
--- a/apps/www/src/components/demo/demo.tsx
+++ b/apps/www/src/components/demo/demo.tsx
@@ -8,6 +8,12 @@ import {
UploadIcon
} from '@radix-ui/react-icons';
import * as Apsara from '@raystack/apsara';
+import {
+ BellIcon,
+ FilterIcon,
+ OrganizationIcon,
+ SidebarIcon
+} from '@raystack/apsara/icons';
import dayjs from 'dayjs';
import { Home, Info, Laugh, X } from 'lucide-react';
import NextLink from 'next/link';
@@ -24,6 +30,10 @@ export default function Demo(props: DemoProps) {
data,
scope = {
...Apsara,
+ BellIcon,
+ FilterIcon,
+ OrganizationIcon,
+ SidebarIcon,
DataTableDemo,
LinearMenuDemo,
PopoverColorPicker,
diff --git a/apps/www/src/components/preview/preview.module.css b/apps/www/src/components/preview/preview.module.css
index 1121a7bca..33c3884a4 100644
--- a/apps/www/src/components/preview/preview.module.css
+++ b/apps/www/src/components/preview/preview.module.css
@@ -1,5 +1,5 @@
.preview {
- padding: 40px 20px;
+ padding: 0;
width: 100%;
height: 100%;
display: flex;
diff --git a/apps/www/src/content/docs/components/sidebar/demo.ts b/apps/www/src/content/docs/components/sidebar/demo.ts
index 8278e96f6..cfb8dc34e 100644
--- a/apps/www/src/content/docs/components/sidebar/demo.ts
+++ b/apps/www/src/content/docs/components/sidebar/demo.ts
@@ -1,38 +1,63 @@
'use client';
+const mainAreaStyle = `{{ flex: 1, border: '2px dashed var(--rs-color-border-base-secondary)', margin: 'var(--rs-space-4)', boxSizing: 'border-box' }}`;
+
+const sidebarLayout = (sidebar: string) =>
+ `
+ ${sidebar.trim()}
+
+`;
+
+const sidebarLayoutRight = (sidebar: string) =>
+ `
+
+ ${sidebar.trim()}
+`;
+
export const preview = {
type: 'code',
- code: `
+ code: sidebarLayout(`
-
+
Apsara
- }>
- } active>
+
+ } active>
Dashboard
- } disabled>
+ }>
+ Analytics
+
+ }>
Settings
+
+ }>
+ Reports
+
+ }>
+ Activities
+
+
- }>
+ }>
Help
- }>
- Help
+ }>
+ Help & Support
- `
+ `)
};
export const positionDemo = {
@@ -40,39 +65,51 @@ export const positionDemo = {
tabs: [
{
name: 'Left',
- code: `
+ code: sidebarLayout(`
-
+
Apsara
- } active>Dashboard
- } disabled>Settings
+
+ } active>Dashboard
+ }>Analytics
+ }>Settings
+
+
+ }>Help
+
- `
+ `)
},
{
name: 'Right',
- code: `
+ code: sidebarLayoutRight(`
-
+
Apsara
- } active>Dashboard
- } disabled>Settings
+
+ } active>Dashboard
+ }>Analytics
+ }>Settings
+
+
+ }>Help
+
- `
+ `)
}
]
};
@@ -82,92 +119,162 @@ export const stateDemo = {
tabs: [
{
name: 'Expanded',
- code: `
+ code: sidebarLayout(`
-
+
Apsara
- } active>Dashboard
- } disabled>Settings
+
+ } active>Dashboard
+ }>Analytics
+ }>Settings
+
+
+ }>Help
+
- `
+ `)
},
{
name: 'Collapsed',
- code: `
+ code: sidebarLayout(`
-
+
Apsara
- } active>Dashboard
- } disabled>Settings
+
+ } active>Dashboard
+ }>Analytics
+ }>Settings
+
+
+ }>Help
+
- `
+ `)
},
{
name: 'Uncontrolled',
- code: `
+ code: sidebarLayout(`
-
+
Apsara
- } active>Dashboard
- } disabled>Settings
+
+ } active>Dashboard
+ }>Analytics
+ }>Settings
+
+
+ }>Help
+
- `
+ `)
},
{
name: 'Uncontrolled (default open)',
- code: `
+ code: sidebarLayout(`
-
+
Apsara
- } active>Dashboard
- } disabled>Settings
+
+ } active>Dashboard
+ }>Analytics
+ }>Settings
+
+
+ }>Help
+
- `
+ `)
}
]
};
export const tooltipDemo = {
type: 'code',
- code: `
-
+
+
+ Apsara
+
+
+
+
+ } active>Dashboard
+ }>Analytics
+ }>Settings
+
+
+ }>Help
+
+
+ `)
+};
+
+export const collapsibleDemo = {
+ type: 'code',
+ code: sidebarLayout(`
+
+
+
+
+
+ Apsara
+
+
+
+
+ } active>Dashboard
+ }>Analytics
+
+
+ `)
+};
+
+export const hideTooltipDemo = {
+ type: 'code',
+ code: sidebarLayout(`
+
+
+
+
Apsara
- } active>Dashboard
- } disabled>Settings
+
+ } active>Dashboard
+ }>Settings
+
- `
+ `)
};
diff --git a/apps/www/src/content/docs/components/sidebar/index.mdx b/apps/www/src/content/docs/components/sidebar/index.mdx
index 02a9522fb..7b58eb0d5 100644
--- a/apps/www/src/content/docs/components/sidebar/index.mdx
+++ b/apps/www/src/content/docs/components/sidebar/index.mdx
@@ -4,7 +4,14 @@ description: A collapsible side navigation panel component.
source: packages/raystack/components/sidebar
---
-import { preview, positionDemo, stateDemo, tooltipDemo } from "./demo.ts";
+import {
+ preview,
+ positionDemo,
+ stateDemo,
+ tooltipDemo,
+ collapsibleDemo,
+ hideTooltipDemo
+} from "./demo.ts";
@@ -17,9 +24,12 @@ import { Sidebar } from "@raystack/apsara";
-
-
-
+
+
+ Item
+
+
+
```
@@ -27,7 +37,7 @@ import { Sidebar } from "@raystack/apsara";
### Root
-Groups all parts of the sidebar navigation.
+Groups all parts of the sidebar navigation. Use `collapsible={false}` to hide the resize handle and prevent toggling. Use `hideCollapsedItemTooltip` to disable tooltips on items when the sidebar is collapsed.
@@ -35,16 +45,24 @@ Groups all parts of the sidebar navigation.
The header section is a container component that accepts all `div` props. It's commonly used to create a header with an icon and title.
+### Main
+
+The main section wraps navigation groups and items. It accepts all `div` props and scrolls when content overflows.
+
### Group
### Item
-*Note: `leadingIcon` is optional and will show a fallback avatar only in collapsed state. You can pass `<>>` to render truly nothing.*
+*Note: `leadingIcon` is optional and will show a fallback avatar only in collapsed state. You can pass `<>>` to render truly nothing. Use the `as` prop to render as a custom element (e.g. a router `Link`).*
+### Footer
+
+The footer section is a container that accepts all `div` props. It's commonly used for secondary links (e.g. Help, Preferences) and stays at the bottom of the sidebar.
+
## Examples
### Position
@@ -69,15 +87,29 @@ You can use Sidebar as a controlled component to conditionally render different
+### Non-collapsible
+
+Set `collapsible={false}` to hide the resize handle and prevent the sidebar from being collapsed or expanded.
+
+
+
+### Hide item tooltips when collapsed
+
+Set `hideCollapsedItemTooltip` to disable tooltips on navigation items when the sidebar is collapsed. Useful when item labels are redundant with the collapse tooltip or you want a cleaner collapsed state.
+
+
+
## Accessibility
The Sidebar implements the following accessibility features:
+- **Reduced motion** — Respects the user's motion preferences. When the system "Reduce motion" setting is enabled, collapse/expand and hover transitions are disabled so the sidebar updates without animation.
+
- Proper ARIA roles and attributes
- `role="navigation"` for the main sidebar
- `role="banner"` for the header
- - `role="menuitem"` for navigation items
+ - `role="list"` for item containers (Main groups, Footer) and `role="listitem"` for navigation items
- `aria-expanded` to indicate sidebar state
- `aria-current="page"` for active items
- `aria-disabled="true"` for disabled items
diff --git a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx
index bbbc6e3e7..a56486e53 100644
--- a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx
+++ b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx
@@ -199,7 +199,7 @@ describe('Sidebar', () => {
it('has proper ARIA attributes', () => {
render();
- const footer = screen.getByRole('group', { name: 'Footer navigation' });
+ const footer = screen.getByRole('list', { name: 'Footer navigation' });
expect(footer).toBeInTheDocument();
});
});
@@ -246,10 +246,14 @@ describe('Sidebar', () => {
expect(item).toHaveAttribute('aria-disabled', 'true');
});
- it('hides text when collapsed', () => {
+ it('hides text when collapsed and sets aria-label for screen readers', () => {
render();
expect(screen.queryByText(DASHBOARD_ITEM_TEXT)).not.toBeInTheDocument();
+ const dashboardLink = screen.getByRole('listitem', {
+ name: DASHBOARD_ITEM_TEXT
+ });
+ expect(dashboardLink).toHaveAttribute('aria-label', DASHBOARD_ITEM_TEXT);
});
});
diff --git a/packages/raystack/components/sidebar/sidebar-item.tsx b/packages/raystack/components/sidebar/sidebar-item.tsx
index 85d70bd6a..0dac1fab0 100644
--- a/packages/raystack/components/sidebar/sidebar-item.tsx
+++ b/packages/raystack/components/sidebar/sidebar-item.tsx
@@ -63,9 +63,12 @@ export const SidebarItem = forwardRef(
className: cx(styles['nav-item'], classNames?.root),
'data-active': active,
'data-disabled': disabled,
- role: 'menuitem',
+ role: 'listitem',
'aria-current': active ? 'page' : undefined,
'aria-disabled': disabled,
+ ...(isCollapsed && typeof children === 'string'
+ ? { 'aria-label': children }
+ : {}),
...props
},
<>
@@ -76,7 +79,7 @@ export const SidebarItem = forwardRef(
variant='soft'
color='neutral'
fallback={children[0].toUpperCase()}
- style={{ cursor: 'pointer' }}
+ className={styles['nav-fallback-avatar']}
/>
) : null}
@@ -93,8 +96,9 @@ export const SidebarItem = forwardRef(
if (isCollapsed && !hideCollapsedItemTooltip) {
return (
-
- {content}
+
+
+ {children}
);
}
diff --git a/packages/raystack/components/sidebar/sidebar-misc.tsx b/packages/raystack/components/sidebar/sidebar-misc.tsx
index 8de58bca9..981860802 100644
--- a/packages/raystack/components/sidebar/sidebar-misc.tsx
+++ b/packages/raystack/components/sidebar/sidebar-misc.tsx
@@ -29,7 +29,7 @@ export const SidebarFooter = forwardRef<
ref={ref}
className={cx(styles.footer, className)}
direction='column'
- role='group'
+ role='list'
aria-label='Footer navigation'
{...props}
>
diff --git a/packages/raystack/components/sidebar/sidebar.module.css b/packages/raystack/components/sidebar/sidebar.module.css
index ed084adf8..42e4ac51b 100644
--- a/packages/raystack/components/sidebar/sidebar.module.css
+++ b/packages/raystack/components/sidebar/sidebar.module.css
@@ -25,7 +25,7 @@
}
.root[data-closed] {
- width: 57px;
+ width: var(--rs-space-12);
}
.header {
@@ -47,7 +47,7 @@
}
.root[data-closed] .main {
- align-items: center;
+ align-items: flex-start;
}
.root[data-closed] [data-collapse-hidden] {
@@ -74,7 +74,7 @@
}
.root[data-closed] .nav-item {
- justify-content: center;
+ justify-content: flex-start;
}
.nav-item:hover {
@@ -106,6 +106,10 @@
flex-shrink: 0;
}
+.nav-fallback-avatar {
+ cursor: pointer;
+}
+
.nav-text {
color: var(--rs-color-foreground-base-primary);
font-size: var(--rs-font-size-small);
@@ -129,7 +133,7 @@
position: absolute;
top: 0;
right: 0;
- width: 4px;
+ width: var(--rs-space-2);
height: 100%;
cursor: ew-resize;
transition: background-color 0.2s ease;
@@ -159,7 +163,7 @@
}
.root[data-closed] .nav-group {
- align-items: center;
+ align-items: flex-start;
}
.nav-group-header {
@@ -186,8 +190,25 @@
width: 100%;
}
+/* Hide group header visually when collapsed but reserve vertical space to prevent shift */
+.root[data-closed] .nav-group-header {
+ visibility: hidden;
+}
+
.root[data-closed] .nav-group-label {
width: 0;
+ min-width: 0;
+ overflow: hidden;
opacity: 0;
- display: none;
+ visibility: hidden;
+ /* Keep in flow (no display: none) so header row height is preserved */
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .root,
+ .nav-item,
+ .nav-text,
+ .resizeHandle {
+ transition: none;
+ }
}