npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xsolla/xui-b2b-sidebar

v0.214.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, collapsed sta

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, collapsed state and label translations stay in sync.

Installation

npm install @xsolla/xui-b2b-sidebar

Imports

import {
  Sidebar,
  SidebarProvider,
  useSidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuCollapsible,
  SidebarMenuSub,
  SidebarTrigger,
  SidebarChatButton,
  SidebarCollapsed,
  SidebarPinnedList,
  getPinnedCollapsedItem,
  DEFAULT_SIDEBAR_LABELS,
  resolveSidebarLabels,
  type SidebarProps,
  type SidebarProviderProps,
  type SidebarMenuItemProps,
  type SidebarMenuCollapsibleProps,
  type SidebarCollapsedProps,
  type SidebarChatButtonProps,
  type SidebarTriggerProps,
  type SidebarPinnedItem,
  type SidebarPinnedListProps,
  type SidebarLabels,
  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>
  );
}

Localisation

Menu labels, group headings and popover titles are consumer-supplied props, so they are localisable by construction. The strings the sidebar renders on its own behalf — the chat button, the collapse/expand toggle, the pin controls, the "Beta" tag and the navigation landmark — are collected in a single SidebarLabels map. Override any subset once on SidebarProvider:

import { SidebarProvider } from "@xsolla/xui-b2b-sidebar";
import { useTranslation } from "react-i18next";

function LocalisedShell({ children }: { children: React.ReactNode }) {
  const { t } = useTranslation();

  return (
    <SidebarProvider
      pathname={location.pathname}
      labels={{
        navigation: t("sidebar.navigation"),
        expandSidebar: t("sidebar.expand"),
        collapseSidebar: t("sidebar.collapse"),
        chat: t("sidebar.chat"),
        openChat: t("sidebar.openChat"),
        pin: t("sidebar.pin"),
        unpin: t("sidebar.unpin"),
        pinItem: t("sidebar.pinItem"),
        unpinItem: t("sidebar.unpinItem"),
        beta: t("sidebar.beta"),
        pinned: t("sidebar.pinned"),
      }}
    >
      {children}
    </SidebarProvider>
  );
}

Every key is optional — omitted keys keep their English default from DEFAULT_SIDEBAR_LABELS.

Label keys

| Key | Default | Where it appears | | ----------------- | -------------------- | ----------------------------------------------------------------- | | navigation | "Sidebar navigation" | aria-label on the root role="navigation" landmark. | | expandSidebar | "Expand sidebar" | aria-label on the toggle while collapsed (both modes). | | collapseSidebar | "Collapse sidebar" | aria-label on the toggle while expanded. | | chat | "Chat" | Visible text on the expanded-mode SidebarChatButton. | | openChat | "Open chat" | aria-label on the collapsed-mode icon-only chat button. | | pin | "Pin" | Pin-toggle tooltip while the item is unpinned. | | unpin | "Unpin" | Pin-toggle tooltip while the item is pinned. | | pinItem | "Pin item" | Pin-toggle aria-label while unpinned. | | unpinItem | "Unpin item" | Pin-toggle aria-label while pinned. | | beta | "Beta" | Trailing tag rendered when beta is set on a menu item. | | pinned | "Pinned" | Default title of the collapsed-mode pinned-shortcuts popover. |

Precedence

Per-instance props beat the provider, which beats the defaults:

component prop  >  SidebarProvider labels  >  DEFAULT_SIDEBAR_LABELS

Use a per-instance prop only when one occurrence needs to differ — e.g. two sidebars on one page needing distinct landmark names:

<Sidebar navigationLabel={t("sidebar.secondaryNavigation")}>{/* ... */}</Sidebar>

getPinnedCollapsedItem

This helper is a plain function, not a component, so it cannot read provider context. Pass the translated title through options.label:

const pinnedItem = getPinnedCollapsedItem(pinnedItems, { label: t("sidebar.pinned") });

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. | | labels | Partial<SidebarLabels> | English | Translations for the strings the sidebar renders itself. See Localisation. | | 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;
  labels: SidebarLabels;
};

expandedId / onExpandedIdChange coordinate which SidebarMenuCollapsible is open — only one at a time. labels is always fully resolved (provider overrides merged over the defaults), so consumers can read it without null checks.

<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. | | navigationLabel | string | labels.navigation | aria-label for the navigation landmark. | | 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. | | onPinToggle | (e) => void | — | Renders a pin/unpin toggle on hover (always visible when pinned). | | dragHandle | boolean | — | On a pinned item, swaps the pin glyph for a drag handle on row hover. | | 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). | | pinLabel | string | labels.pin | Pin-toggle tooltip while unpinned. | | unpinLabel | string | labels.unpin | Pin-toggle tooltip while pinned. | | pinAriaLabel | string | labels.pinItem | Pin-toggle aria-label while unpinned. | | unpinAriaLabel | string | labels.unpinItem | Pin-toggle aria-label while pinned. | | betaLabel | string | labels.beta | Text inside the trailing tag when beta is set. |

<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. | | chatAriaLabel | string | labels.openChat | aria-label for the icon-only chat button. | | expandLabel | string | labels.expandSidebar | aria-label for the expand toggle. |

<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.

| Prop | Type | Default | Description | | --------------- | -------- | ------------------------ | ------------------------------------ | | expandLabel | string | labels.expandSidebar | aria-label used while collapsed. | | collapseLabel | string | labels.collapseSidebar | aria-label used while expanded. |

<SidebarChatButton>

Branded chat button for the footer.

| Prop | Type | Default | Description | | --------- | ------------ | -------------- | -------------------------------------- | | onClick | () => void | — | Click handler. | | badge | boolean | false | Show the unread-indicator dot. | | label | string | labels.chat | Visible button text. |

<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

interface SidebarLabels {
  navigation: string;
  expandSidebar: string;
  collapseSidebar: string;
  chat: string;
  openChat: string;
  pin: string;
  unpin: string;
  pinItem: string;
  unpinItem: string;
  beta: string;
  pinned: string;
}

declare const DEFAULT_SIDEBAR_LABELS: SidebarLabels;
declare const resolveSidebarLabels: (
  overrides?: Partial<SidebarLabels>
) => SidebarLabels;

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" with an aria-label from labels.navigation (default "Sidebar navigation", per-instance override via navigationLabel). The expanded and collapsed panes are toggled via aria-hidden so only one is announced at a time.
  • SidebarTrigger and the collapsed-mode toggle have aria-pressed and a context-aware aria-label (labels.expandSidebar / labels.collapseSidebar).
  • The pin toggle exposes aria-pressed plus labels.pinItem / labels.unpinItem as its aria-label, and a tooltip carrying labels.pin / labels.unpin.
  • Every one of those strings is translatable through the provider's labels prop — assistive-technology users get the app's language, not English.
  • Each SidebarMenuCollapsible header is a <button> with aria-expanded and aria-controls linking 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 from widthCollapsed; the wrapper animates width and cross-fades the two views via opacity.
  • Only one SidebarMenuCollapsible is open at a time; the section auto-expands when pathname matches one of matchPaths (a leading /<merchantId> numeric prefix is stripped before matching).
  • Hover popovers in collapsed mode are rendered into a portal on document.body and positioned with viewport-clamped position: fixed math.
  • Active route detection lives in the consumer's linkComponent — apply activeClassName when the link matches the current route.
  • labels overrides are merged over the defaults once per provider render and serialised for memoisation, so passing an inline object literal does not thrash the context.