@xsolla/xui-b2b-sidebar
v0.186.1
Published
A composable navigation sidebar for B2B admin surfaces. Renders an expanded panel with grouped menu items and a parallel collapsed icon strip with hover popovers, both driven by the same `SidebarProvider` so the active route, link component, and collapsed
Readme
Sidebar
A composable navigation sidebar for B2B admin surfaces. Renders an expanded panel with grouped menu items and a parallel collapsed icon strip with hover popovers, both driven by the same SidebarProvider so the active route, link component, and collapsed state stay in sync.
Installation
npm install @xsolla/xui-b2b-sidebarImports
import {
Sidebar,
SidebarProvider,
useSidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarMenu,
SidebarMenuItem,
SidebarMenuCollapsible,
SidebarMenuSub,
SidebarTrigger,
SidebarChatButton,
SidebarCollapsed,
type SidebarProps,
type SidebarProviderProps,
type SidebarMenuItemProps,
type SidebarMenuCollapsibleProps,
type SidebarCollapsedProps,
type SidebarItemType,
type SidebarLinkProps,
type SidebarLinkActiveCheck,
type SidebarLinkClickHandler,
} from "@xsolla/xui-b2b-sidebar";Quick start
Wrap the sidebar in SidebarProvider, supplying the current pathname, the controlled collapsed state, and your app's link component (React Router, Next.js Link, etc.). The default link is a plain <a> if you omit linkComponent.
import { useState } from "react";
import { Home, Wallet, Settings } from "@xsolla/xui-icons-base";
import {
Sidebar,
SidebarProvider,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarMenu,
SidebarMenuItem,
SidebarMenuCollapsible,
SidebarMenuSub,
SidebarTrigger,
} from "@xsolla/xui-b2b-sidebar";
function AppShell({ pathname }: { pathname: string }) {
const [collapsed, setCollapsed] = useState(false);
return (
<SidebarProvider
collapsed={collapsed}
onCollapsedChange={setCollapsed}
pathname={pathname}
>
<Sidebar>
<SidebarContent>
<SidebarGroup label="Main">
<SidebarMenu>
<SidebarMenuItem
to="/dashboard"
icon={<Home size={18} variant="line" aria-hidden />}
label="Dashboard"
/>
<SidebarMenuCollapsible
icon={<Wallet size={18} variant="line" aria-hidden />}
label="Finance"
matchPaths={["/finance"]}
>
<SidebarMenuSub>
<SidebarMenuItem
to="/finance/payouts"
label="Payouts"
isNested
/>
<SidebarMenuItem
to="/finance/reports"
label="Reports"
isNested
/>
</SidebarMenuSub>
</SidebarMenuCollapsible>
</SidebarMenu>
</SidebarGroup>
<SidebarGroup label="Workspace">
<SidebarMenu>
<SidebarMenuItem
to="/settings"
icon={<Settings size={18} variant="line" aria-hidden />}
label="Settings"
/>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarTrigger />
</SidebarFooter>
</Sidebar>
</SidebarProvider>
);
}API Reference
None of the sidebar components extend ThemeOverrideProps — they read theme via useResolvedTheme() from the surrounding provider. Wrap the app in a theme provider if you need to override mode or product context.
<SidebarProvider>
| Prop | Type | Default | Description |
| ------------------- | --------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------- |
| testID | string | — | Test ID for testing frameworks. On web this renders as data-testid; on React Native it renders as testID. |
| collapsed | boolean | uncontrolled | Controlled collapsed state. Omit to use the internal state. |
| onCollapsedChange | (collapsed: boolean) => void | — | Called when the user toggles via SidebarTrigger or the collapsed-mode toggle. |
| pathname | string | "" | Current route, used for active-state matching and matchPaths auto-expand. |
| linkComponent | ComponentType<SidebarLinkProps> | plain <a> | Router link component. |
| children | ReactNode | — | Sidebar tree. |
useSidebar()
Read the provider's state from anywhere inside the tree.
function useSidebar(): {
collapsed: boolean;
onCollapsedChange: (collapsed: boolean) => void;
pathname: string;
LinkComponent: React.ComponentType<SidebarLinkProps>;
expandedId: string | null;
onExpandedIdChange: (id: string | null) => void;
};expandedId / onExpandedIdChange coordinate which SidebarMenuCollapsible is open — only one at a time.
<Sidebar>
| Prop | Type | Default | Description |
| ---------------------- | ------------------- | ------- | ------------------------------------------------------ |
| collapsedItems | SidebarItemType[] | [] | Top icons in the collapsed strip. |
| collapsedToolItems | SidebarItemType[] | [] | Tool icons rendered after a spacer. |
| collapsedBottomItems | SidebarItemType[] | [] | Bottom icons (e.g. Settings, Billing). |
| showChat | boolean | true | Render the chat button in the collapsed footer. |
| onChatClick | () => void | — | Click handler for the chat button. |
| chatBadge | boolean | false | Show an unread-indicator dot on the chat button. |
| children | ReactNode | — | The expanded tree (SidebarContent, SidebarFooter). |
<SidebarMenuItem>
| Prop | Type | Default | Description |
| ----------------- | ------------------------- | ------- | ----------------------------------------------------------------------------------- |
| to | string | — | Route URL passed through to linkComponent. |
| label | ReactNode | — | Text label. |
| icon | ReactNode | — | Leading icon (hidden when isNested). |
| exact | boolean | — | Exact-match flag forwarded to the link component. |
| external | boolean | — | Marks the link as external (sets target/rel). |
| hasExternalIcon | boolean | — | Shows the external-link icon at the trailing edge. |
| target | string \| null | — | Anchor target. |
| onClick | SidebarLinkClickHandler | — | Click handler. |
| dataId | string | — | data-id attribute for tests/analytics. |
| isActive | SidebarLinkActiveCheck | — | Custom active-check forwarded to the link. |
| isPinned | boolean | — | Renders the icon at 12px to indicate a pinned item. |
| showBadge | boolean | — | Shows a small alert dot next to the label. |
| hasTooltip | boolean | — | Truncates labels longer than 20 chars and shows the full text on hover. |
| beta | boolean | — | Renders a "Beta" tag at the trailing edge. |
| multiLine | boolean | — | Allows the label to wrap to multiple lines. |
| extra | ReactNode | — | Slot rendered between the icon and the label. |
| isNested | boolean | false | Set when rendered inside SidebarMenuSub (drops the icon, indents under the rail). |
<SidebarMenuCollapsible>
| Prop | Type | Default | Description |
| ------------ | ----------- | ------- | ------------------------------------------------------------------------- |
| icon | ReactNode | — | Leading icon. |
| label | ReactNode | — | Section label. |
| dataId | string | — | data-id attribute. Also seeds the stable id used by aria-controls. |
| matchPaths | string[] | [] | Route prefixes that auto-expand this section. |
| children | ReactNode | — | Nested items, typically a SidebarMenuSub of SidebarMenuItem isNested. |
<SidebarCollapsed>
Lower-level collapsed-strip renderer. The <Sidebar> component composes this for you; reach for it when you need to render the collapsed strip outside the standard layout.
| Prop | Type | Default | Description |
| ------------------ | ------------------- | ------- | -------------------------------------------------- |
| items | SidebarItemType[] | — | Top icon items. |
| toolItems | SidebarItemType[] | [] | Tool icons after a spacer. |
| bottomItems | SidebarItemType[] | [] | Bottom icons. |
| onToggleCollapse | () => void | — | Click handler for the toggle button in the footer. |
| onChatClick | () => void | — | Click handler for the chat button. |
| showChat | boolean | true | Render the chat button. |
| chatBadge | boolean | false | Show the unread dot. |
<SidebarContent>
Scrollable body container rendered inside <Sidebar>. Adds the configured padding and a hover-visible scrollbar.
<SidebarFooter>
Bordered footer row for the bottom of the sidebar; typically wraps the chat button and trigger.
<SidebarTrigger>
Collapse/expand toggle button. Reads/writes state from the surrounding SidebarProvider.
<SidebarChatButton>
Branded chat button for the footer. Props: onClick: () => void, badge?: boolean.
<SidebarGroup>
Section container with an optional uppercase label. Props: label?: ReactNode, children: ReactNode.
<SidebarMenu>
Flex-column wrapper around SidebarMenuItem / SidebarMenuCollapsible children.
<SidebarMenuSub>
Fragment used as a structural marker inside collapsibles to group nested menu items.
Types
type SidebarItemType = {
to?: string;
label: ReactNode;
icon?: ReactNode;
dataId?: string;
exact?: boolean;
external?: boolean;
hasExternalIcon?: boolean;
target?: string | null;
onClick?: SidebarLinkClickHandler;
hasTooltip?: boolean;
beta?: boolean;
multiLine?: boolean;
isPinned?: boolean;
showBadge?: boolean;
reverse?: boolean; // reserved for legacy reversed-layout items
isActive?: SidebarLinkActiveCheck;
children?: SidebarItemType[];
privileges?: string[];
visibility?: string;
disallowedIntegrationTypes?: string[];
extra?: ReactNode;
};
interface SidebarLinkProps {
to?: string;
exact?: boolean;
external?: boolean;
target?: string | null;
className?: string;
activeClassName?: string;
isActive?: SidebarLinkActiveCheck;
onClick?: SidebarLinkClickHandler;
dataId?: string;
"aria-label"?: string;
children: ReactNode;
}
type SidebarLinkActiveCheck = (
match: unknown,
location: { pathname: string; search?: string; hash?: string }
) => boolean;
type SidebarLinkClickHandler = (event?: React.MouseEvent<HTMLElement>) => void;Examples
Custom link component
Supply your router's link via the linkComponent prop on SidebarProvider. The component receives to, exact, external, target, className, activeClassName, isActive, onClick, dataId, and renders children.
import { NavLink } from "react-router-dom";
import type { SidebarLinkProps } from "@xsolla/xui-b2b-sidebar";
const RouterLink: React.FC<SidebarLinkProps> = ({
to,
exact,
className,
activeClassName,
children,
onClick,
dataId,
}) => (
<NavLink
to={to ?? "#"}
end={exact}
className={({ isActive }) =>
[className, isActive && activeClassName].filter(Boolean).join(" ")
}
data-id={dataId}
onClick={onClick}
>
{children}
</NavLink>
);
<SidebarProvider linkComponent={RouterLink} pathname={location.pathname}>
{/* ... */}
</SidebarProvider>;Auto-expanding sections
Pass matchPaths to SidebarMenuCollapsible. When the current pathname starts with any of the listed prefixes, the section auto-expands. Only one collapsible can be open at a time, so navigating between sections collapses the others.
import { Wallet } from "@xsolla/xui-icons-base";
import {
SidebarMenuCollapsible,
SidebarMenuSub,
SidebarMenuItem,
} from "@xsolla/xui-b2b-sidebar";
<SidebarMenuCollapsible
icon={<Wallet size={18} variant="line" aria-hidden />}
label="Finance"
matchPaths={["/finance", "/finance/payouts", "/finance/reports"]}
>
<SidebarMenuSub>
<SidebarMenuItem to="/finance/payouts" label="Payouts" isNested />
<SidebarMenuItem to="/finance/reports" label="Reports" isNested />
</SidebarMenuSub>
</SidebarMenuCollapsible>;Item flags
import { Pin, Graph, Layer } from "@xsolla/xui-icons-base";
import { SidebarMenuItem } from "@xsolla/xui-b2b-sidebar";
<>
<SidebarMenuItem
to="/pinned"
label="Pinned report"
icon={<Pin size={18} aria-hidden />}
isPinned
showBadge
/>
<SidebarMenuItem
to="/labs"
label="New analytics"
icon={<Graph size={18} aria-hidden />}
beta
/>
<SidebarMenuItem
to="https://docs.example.com"
label="Documentation"
icon={<Layer size={18} aria-hidden />}
external
hasExternalIcon
target="_blank"
/>
</>;Collapsed icon strip
Because the collapsed layout is fundamentally different (icons only, hover popovers), pass icon-strip items separately as collapsedItems, collapsedToolItems (rendered after a spacer in the main scroll area), and collapsedBottomItems (rendered above the chat/toggle footer). Items with children open a hover popover listing the children; items without children navigate directly on click (the icon is rendered as a link), and show a label tooltip on hover for discoverability.
import { Home, Wallet, Settings } from "@xsolla/xui-icons-base";
import { Sidebar, type SidebarItemType } from "@xsolla/xui-b2b-sidebar";
const mainNavItems: SidebarItemType[] = [
{
to: "/dashboard",
label: "Dashboard",
icon: <Home size={18} variant="line" aria-hidden />,
},
{
label: "Finance",
icon: <Wallet size={18} variant="line" aria-hidden />,
children: [
{ to: "/finance/payouts", label: "Payouts" },
{ to: "/finance/reports", label: "Reports" },
],
},
];
<Sidebar
collapsedItems={mainNavItems}
collapsedBottomItems={[
{
to: "/settings",
label: "Settings",
icon: <Settings size={18} aria-hidden />,
},
]}
onChatClick={() => openSupportChat()}
chatBadge={hasUnreadMessages}
>
{/* expanded tree */}
</Sidebar>;Accessibility
- The root sidebar is
role="navigation"witharia-label="Sidebar navigation". The expanded and collapsed panes are toggled viaaria-hiddenso only one is announced at a time. SidebarTriggerand the collapsed-mode toggle havearia-pressedand a context-awarearia-label("Expand sidebar"/"Collapse sidebar").- Each
SidebarMenuCollapsibleheader is a<button>witharia-expandedandaria-controlslinking to its region. - Collapsed icon items with children expose
aria-haspopup="menu"; their popovers are keyboard-reachable (focus opens, blur closes after 150ms, Escape closes immediately). Leaf items (no children) render as native links and navigate on click or Enter. - Truncated labels (
hasTooltip) fall back to a Tooltip with the full text.
Behaviour
- Expanded width is sourced from
theme.sizing.sidebar().widthExpanded, collapsed fromwidthCollapsed; the wrapper animateswidthand cross-fades the two views via opacity. - Only one
SidebarMenuCollapsibleis open at a time; the section auto-expands whenpathnamematches one ofmatchPaths(a leading/<merchantId>numeric prefix is stripped before matching). - Hover popovers in collapsed mode are rendered into a portal on
document.bodyand positioned with viewport-clampedposition: fixedmath. - Active route detection lives in the consumer's
linkComponent— applyactiveClassNamewhen the link matches the current route.
