shadimage
mui-treasury
itemsidebarsource: ok · refreshed

Sidebar Overview

A complete sidebar component system with collapsible container, menu items, groups, icons, text, tooltips, and rail

npx shadcn add "https://mui-treasury.com/r/sidebar.json"
free

Where it goes

sidebardashboard-shell

Interaction

accordiontooltipdropdown

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@base-ui/react

Registry dependencies

https://www.mui-treasury.com/r/shared.json

Files15 · source cached 10/7/2026, 12:17:26 AM

components/sidebar/index.ts
export * from './sidebar-classes';
export * from './sidebar-container';
export * from './sidebar-group';
export * from './sidebar-group-label';
export * from './sidebar-group-text';
export * from './sidebar-menu-list';
export * from './sidebar-menu-item';
export * from './sidebar-menu-button';
export * from './sidebar-menu-action';
export * from './sidebar-text';
export * from './sidebar-icon';
export * from './sidebar-tooltip';
export * from './sidebar-rail';
export * from './popup-menu';
components/sidebar/popup-menu.tsx
import React from 'react';

import { NavigationMenu } from '@base-ui/react/navigation-menu';
import { SxProps, Theme, styled } from '@mui/material/styles';

import {
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuList,
  SidebarTooltip,
} from './index';

interface PopupMenuItemProps {
  children?: React.ReactNode;
  render: NavigationMenu.Trigger.Props['render'];
  tooltip?: React.ReactNode;
  sx?: SxProps<Theme>;
}

const StyledPortal = styled(NavigationMenu.Portal)({
  display: 'var(--_collapsed, block) var(--_uncollapsed, none)',
  zIndex: 1300,
});

export function PopupMenuList({
  children,
  sidebarSide = 'left',
  ...props
}: React.ComponentProps<typeof NavigationMenu.Root> & {
  sidebarSide?: 'left' | 'right';
}) {
  const [container, setContainer] = React.useState<HTMLElement | null>(null);
  const triggerCallbackRef = React.useCallback((node: HTMLElement | null) => {
    if (node) {
      setContainer(
        (node.closest('.EdgeSidebar-R') ||
          node.closest('.EdgeSidebar')) as HTMLElement,
      );
    }
  }, []);
  return (
    <NavigationMenu.Root orientation="vertical" {...props}>
      <NavigationMenu.List
        ref={triggerCallbackRef as React.Ref<HTMLUListElement>}
        render={
          <SidebarMenuList
            sx={{
              '& .PopupMenuItem:has(+ :not(.PopupMenuItem))': {
                // to hide the popup item when uncollapsed, preventing gaps between items
                display: 'var(--_collapsed, flex) var(--_uncollapsed, none)',
              },
              '& .PopupMenuItem + :not(.PopupMenuItem)': {
                // to hide the uncollapsed sibling when in collapsed mode, preventing gaps between items
                display: 'var(--_collapsed, none) var(--_uncollapsed, flex)',
              },
            }}
          />
        }
      >
        {children}
      </NavigationMenu.List>

      <StyledPortal container={container || undefined}>
        <NavigationMenu.Positioner
          side={sidebarSide === 'left' ? 'right' : 'left'}
          align="start"
        >
          <NavigationMenu.Popup render={<MenuPopup />}>
            <NavigationMenu.Viewport />
          </NavigationMenu.Popup>
        </NavigationMenu.Positioner>
      </StyledPortal>
    </NavigationMenu.Root>
  );
}

export const PopupMenuItem = function PopupMenuItem({
  render,
  children,
  tooltip,
  sx,
}: PopupMenuItemProps) {
  return (
    <NavigationMenu.Item
      render={<SidebarMenuItem className="PopupMenuItem" sx={sx} />}
    >
      {tooltip ? (
        <SidebarTooltip
          title={tooltip}
          placement="right"
          disableTouchListener
          disableHoverListener={!!children}
          slotProps={{
            popper: {
              sx: {
                '[data-base-ui-portal] ~ &, &:has(~ [data-base-ui-portal])': {
                  display: 'none !important',
                },
              },
            },
          }}
        >
          <NavigationMenu.Trigger render={render} />
        </SidebarTooltip>
      ) : (
        <NavigationMenu.Trigger render={render} />
      )}
      {children}
    </NavigationMenu.Item>
  );
};

export function PopupMenuContent({
  children,
  ...props
}: NavigationMenu.Content.Props) {
  return (
    <NavigationMenu.Content className="PopupMenuContent" {...props}>
      {children}
    </NavigationMenu.Content>
  );
}

const MenuPopup = styled(SidebarMenuList)(({ theme }) => ({
  '--_collapsed': 'var(--__,)',
  '--_uncollapsed': 'var(--__)',
  minWidth: 160,
  padding: theme.spacing(0.5),
  border: '1px solid',
  borderColor: (theme.vars || theme).palette.divider,
  borderRadius: `calc(4px + ${(theme.vars || theme).shape.borderRadius})`,
  backgroundColor: (theme.vars || theme).palette.background.paper,
  boxShadow: (theme.vars || theme).shadows[1],
  '&:has(.PopupMenuContent:empty)': {
    display: 'none',
  },
}));

export function PopupMenuLink({
  children,
  component,
  ...props
}: NavigationMenu.Link.Props & {
  children: React.ReactNode;
  component?: React.ElementType;
}) {
  return (
    <NavigationMenu.Link
      render={<SidebarMenuButton component={component} />}
      {...props}
    >
      {children}
    </NavigationMenu.Link>
  );
}
components/sidebar/sidebar-classes.ts
export const sidebarClasses = {
  container: 'SidebarContainer',
  group: 'SidebarGroup',
  groupLabel: 'SidebarGroupLabel',
  groupText: 'SidebarGroupText',
  menu: 'SidebarMenuList',
  menuItem: 'SidebarMenuItem',
  menuButton: 'SidebarMenuButton',
  menuAction: 'SidebarMenuAction',
  text: 'SidebarText',
  icon: 'SidebarIcon',
  tooltip: 'SidebarTooltip',
  rail: 'SidebarRail',
  popupMenu: 'PopupMenu',
};
components/sidebar/sidebar-container.tsx
'use client';

import React, { useMemo } from 'react';

import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';

import type {
  ForwardStyledProps,
  OverridableComponent,
} from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarContainerProps = ForwardStyledProps<{
  shrink?: string;
  component?: React.ElementType;
}>;

const StyledSidebarContainer = styled('div', {
  name: 'SidebarContainer',
  slot: 'root',
})<{ ownerState: Required<Pick<SidebarContainerProps, 'shrink'>> }>(
  memoTheme(() => ({
    containerType: 'inline-size',
    display: 'flex',
    flexDirection: 'column',
    flex: 1,
    minHeight: 0,
    overflowY: 'auto',
    '--_collapsed': 'var(--_)',
    '--_uncollapsed': 'var(--__,)',
    variants: [
      {
        props: () => true,
        style: ({
          shrink,
        }: Required<Pick<SidebarContainerProps, 'shrink'>>) => ({
          '& > *': {
            [`@container (min-width: ${shrink})`]: {
              '--_collapsed': 'var(--__,)',
              '--_uncollapsed': 'var(--_)',
            },
          },
        }),
      },
    ],
  })),
);

export const SidebarContainer = React.forwardRef<
  HTMLDivElement,
  SidebarContainerProps & React.ComponentPropsWithoutRef<'div'>
>(function SidebarContainer(
  { className, shrink = '100px', component, ...props },
  ref,
) {
  const ownerState = useMemo(() => ({ shrink }), [shrink]);
  return (
    <StyledSidebarContainer
      ref={ref}
      className={`${sidebarClasses.container} ${className || ''}`}
      ownerState={ownerState}
      as={component}
      {...props}
    />
  );
}) as OverridableComponent<SidebarContainerProps, 'div'>;
components/sidebar/sidebar-group-label.tsx
'use client';

import React from 'react';

import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';

import type {
  ForwardStyledProps,
  OverridableComponent,
} from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarGroupLabelProps = ForwardStyledProps<{
  component?: React.ElementType;
}>;

const StyledSidebarGroupLabel = styled('div', {
  name: 'SidebarGroupLabel',
  slot: 'root',
})(
  memoTheme(({ theme }) => ({
    position: 'relative',
    textOverflow: 'ellipsis',
    transition: 'var(--tsn, var(--_uncollapsed, opacity 0.4s))',
    opacity: 'var(--_uncollapsed, 1) var(--_collapsed, 0)',
    overflow: 'hidden',
    fontSize: 'var(--item-fs, 0.75rem)',
    lineHeight: 'var(--item-lh, 1.125rem)',
    minHeight: 'var(--item-h, 2rem)',
    display: 'flex',
    alignItems: 'center',
    color: (theme.vars || theme).palette.text.secondary,
    paddingInline: 'var(--item-px, 0.5rem)',
    paddingBlock: 'var(--item-py, 0.25rem)',
  })),
);

export const SidebarGroupLabel = React.forwardRef<
  HTMLDivElement,
  SidebarGroupLabelProps & React.ComponentPropsWithoutRef<'div'>
>(function SidebarGroupLabel({ className, component, ...props }, ref) {
  return (
    <StyledSidebarGroupLabel
      ref={ref}
      className={`${sidebarClasses.groupLabel} ${className || ''}`}
      as={component}
      {...props}
    />
  );
}) as OverridableComponent<SidebarGroupLabelProps, 'div'>;
components/sidebar/sidebar-group-text.tsx
'use client';

import React from 'react';

import { styled } from '@mui/material/styles';

import type { ForwardStyledProps } from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarGroupTextProps = ForwardStyledProps;

const StyledSidebarGroupText = styled('div', {
  name: 'SidebarGroupText',
  slot: 'root',
})({
  transition:
    'var(--tsn, grid-template-rows 0.4s, opacity 0.4s var(--_uncollapsed, 0.2s))',
  opacity: 'var(--_collapsed, 0) var(--_uncollapsed, 1)',
  display: 'grid',
  whiteSpace: 'nowrap',
  gridTemplateRows: 'var(--_collapsed, 0fr) var(--_uncollapsed, 1fr)',
  '& > *': {
    overflow: 'hidden',
  },
});

export const SidebarGroupText = React.forwardRef<
  HTMLDivElement,
  SidebarGroupTextProps & React.ComponentPropsWithoutRef<'div'>
>(function SidebarGroupText({ className, ...props }, ref) {
  return (
    <StyledSidebarGroupText
      ref={ref}
      className={`${sidebarClasses.groupText} ${className || ''}`}
      {...props}
    />
  );
});
components/sidebar/sidebar-group.tsx
'use client';

import React from 'react';

import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';

import type {
  ForwardStyledProps,
  OverridableComponent,
} from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarGroupProps = ForwardStyledProps<{
  component?: React.ElementType;
}>;

const StyledSidebarGroup = styled('div', {
  name: 'SidebarGroup',
  slot: 'root',
})(
  memoTheme(({ theme }) => ({
    display: 'flex',
    flexDirection: 'column',
    padding: theme.spacing(1),
  })),
);

export const SidebarGroup = React.forwardRef<
  HTMLDivElement,
  SidebarGroupProps & React.ComponentPropsWithoutRef<'div'>
>(function SidebarGroup({ className, component, ...props }, ref) {
  return (
    <StyledSidebarGroup
      ref={ref}
      className={`${sidebarClasses.group} ${className || ''}`}
      as={component}
      {...props}
    />
  );
}) as OverridableComponent<SidebarGroupProps, 'div'>;
components/sidebar/sidebar-icon.tsx
'use client';

import React, { useMemo } from 'react';

import { Breakpoint, styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';

import type { ForwardStyledProps } from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarIconProps = ForwardStyledProps<{
  size?: string | Partial<Record<Breakpoint, string>>;
  shrinkSize?: string | Partial<Record<Breakpoint, string>>;
}>;

const StyledSidebarIcon = styled('span', {
  name: 'SidebarIcon',
  slot: 'root',
})<{ ownerState: SidebarIconProps }>(
  {
    '--_mt': '0px',
    height: 'var(--icon-size, 1rem)',
    width: 'var(--icon-size, 1rem)',
    transition: 'var(--tsn, cubic-bezier(0.4, 0, 0.2, 1) 0.15s)',
    flex: 'none',
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    marginTop: 'var(--_mt)',
    '& svg': {
      width: '100%',
      height: '100%',
    },
    '&:has(+ .SidebarGroupText)': {
      '--_mt': 'max(18px - var(--icon-size, 1rem), 0px)',
    },
  },
  memoTheme(({ theme }) => ({
    variants: [
      {
        props: ({ size }: SidebarIconProps) => !!size,
        style: ({ size }: Required<SidebarIconProps>) => {
          const toSize = (v: string) => ({
            '--icon-size': v,
            height: v,
            width: v,
            [`.${sidebarClasses.menuItem}:has(&)`]: {
              '--icon-w': v,
            },
          });
          if (typeof size === 'string') {
            return toSize(size);
          }
          const result: Record<string, unknown> = {};
          for (const bp of Object.keys(size) as Breakpoint[]) {
            const val = size[bp];
            if (!val) continue;
            if (bp === 'xs') {
              Object.assign(result, toSize(val));
            } else {
              result[theme.breakpoints.up(bp)] = toSize(val);
            }
          }
          return result;
        },
      },
      {
        props: ({ shrinkSize }: SidebarIconProps) => !!shrinkSize,
        style: ({ shrinkSize }: Required<SidebarIconProps>) => {
          const toCollapsible = (v: string) => ({
            width: `var(--_collapsed, ${v}) var(--_uncollapsed, var(--icon-size, 1rem))`,
            height: `var(--_collapsed, ${v}) var(--_uncollapsed, var(--icon-size, 1rem))`,
          });
          if (typeof shrinkSize === 'string') {
            return toCollapsible(shrinkSize);
          }
          const result: Record<string, unknown> = {};
          for (const bp of Object.keys(shrinkSize) as Breakpoint[]) {
            const val = shrinkSize[bp];
            if (!val) continue;
            if (bp === 'xs') {
              Object.assign(result, toCollapsible(val));
            } else {
              result[theme.breakpoints.up(bp)] = toCollapsible(val);
            }
          }
          return result;
        },
      },
    ],
  })),
);

export const SidebarIcon = React.forwardRef<
  HTMLSpanElement,
  SidebarIconProps & React.ComponentPropsWithoutRef<'span'>
>(function SidebarIcon({ className, size, shrinkSize, ...props }, ref) {
  const ownerState = useMemo(() => ({ size, shrinkSize }), [size, shrinkSize]);
  return (
    <StyledSidebarIcon
      ref={ref}
      className={`${sidebarClasses.icon} ${className || ''}`}
      ownerState={ownerState}
      {...props}
    />
  );
});
components/sidebar/sidebar-menu-action.tsx
'use client';

import React, { useMemo } from 'react';

import ButtonBase, { ButtonBaseProps } from '@mui/material/ButtonBase';
import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';

import type {
  ForwardStyledProps,
  OverridableComponent,
} from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarMenuActionProps = ForwardStyledProps<{
  hoverAppear?: boolean;
  _before?: React.ReactNode;
  component?: React.ElementType;
}>;

const StyledSidebarMenuAction = styled(ButtonBase, {
  name: 'SidebarMenuAction',
  slot: 'root',
})<{ ownerState: SidebarMenuActionProps }>(
  memoTheme(({ theme }) => ({
    '--_ms': 'initial',
    flex: 'none', // prevent action from shrinking when the text is long and the sidebar is collapsing.
    marginInlineStart: 'var(--_ms)',
    width: 'var(--action-size, 1.5rem)',
    height: 'var(--action-size, 1.5rem)',
    display: 'inline-flex',
    visibility:
      'var(--_collapsed, hidden) var(--_uncollapsed, visible)' as never,
    justifyContent: 'center',
    alignItems: 'center',
    alignSelf: 'center',
    color: (theme.vars || theme).palette.text.secondary,
    transition: 'var(--tsn, right 0.4s)',
    borderRadius: (theme.vars || theme).shape.borderRadius,
    '&:first-of-type': {
      '--_ms': 'auto',
    },
    '&:focus-visible, &:has(:focus-visible )': {
      outline: '2px solid',
      outlineColor: (theme.vars || theme).palette.text.primary,
      outlineOffset: '2px',
    },
    '@media (hover: hover)': {
      '&:hover': {
        backgroundColor: (theme.vars || theme).palette.action.hover,
      },
    },
    '& svg': {
      width: 'var(--icon-size, 1rem)',
      height: 'var(--icon-size, 1rem)',
    },
    variants: [
      {
        props: { hoverAppear: true },
        style: {
          opacity: 0,
          '&:hover, &:focus-visible': {
            opacity: 1,
          },
          [`.${sidebarClasses.menuButton}:hover ~ &, .${sidebarClasses.groupLabel} > &`]:
            {
              opacity: 1,
            },
        },
      },
    ],
  })),
);

export const SidebarMenuAction = React.forwardRef<
  HTMLButtonElement,
  SidebarMenuActionProps & ButtonBaseProps
>(function SidebarMenuAction(
  { className, hoverAppear, _before, component, children, ...props },
  ref,
) {
  const ownerState = useMemo(() => ({ hoverAppear }), [hoverAppear]);
  return (
    <StyledSidebarMenuAction
      ref={ref}
      className={`${sidebarClasses.menuAction} ${className || ''}`}
      ownerState={ownerState}
      as={
        component ??
        (className?.includes('CollapsibleTrigger') ? 'label' : undefined)
      }
      {...props}
    >
      {_before}
      {children}
    </StyledSidebarMenuAction>
  );
}) as OverridableComponent<SidebarMenuActionProps, typeof ButtonBase>;
components/sidebar/sidebar-menu-button.tsx
'use client';

import React, { useMemo } from 'react';

import ButtonBase, { ButtonBaseProps } from '@mui/material/ButtonBase';
import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';

import type {
  ForwardStyledProps,
  OverridableComponent,
} from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarMenuButtonProps = ForwardStyledProps<{
  active?: boolean;
  hideWhen?: 'collapsed' | 'uncollapsed';
  _before?: React.ReactNode;
  component?: React.ElementType;
}>;

const StyledSidebarMenuButton = styled(ButtonBase, {
  name: 'SidebarMenuButton',
  slot: 'root',
})<{ ownerState: Pick<SidebarMenuButtonProps, 'active' | 'hideWhen'> }>(
  memoTheme(({ theme }) => ({
    '--_items': 'center',
    minWidth: 0,
    textAlign: 'left',
    alignItems: 'var(--_items)',
    justifyContent: 'initial',
    flex: 1,
    fontSize: 'var(--item-fs, 0.875rem)',
    lineHeight: 'var(--item-lh, 1.25rem)',
    minHeight: 'var(--item-h, 2rem)',
    borderRadius: (theme.vars || theme).shape.borderRadius,
    color: (theme.vars || theme).palette.text.secondary,
    cursor: 'pointer',
    userSelect: 'none', // prevent text highlighting when clicking rapidly or from collapsible trigger
    transition:
      'var(--tsn, min-height var(--_collapsed, 0.2s) var(--_uncollapsed, 0.3s), padding var(--_collapsed, 0.2s) var(--_uncollapsed, 0.3s), gap var(--_collapsed, 0s 0.4s) var(--_uncollapsed, 0s))',
    '--item-gap': 'var(--_collapsed, 0px) var(--_uncollapsed, 0.5rem)',
    display: 'flex',
    gap: 'var(--item-gap)',
    whiteSpace: 'nowrap', // required to prevent text jumping when collapsing/uncollapsing
    paddingInline:
      'var(--_collapsed, var(--shrink-px, var(--item-px, 0.5rem))) var(--_uncollapsed, var(--item-px, 0.5rem))',
    paddingBlock:
      'var(--_collapsed, var(--shrink-py, var(--item-py, 0.375rem))) var(--_uncollapsed, var(--item-py, 0.375rem))',
    '&:focus-visible': {
      outline: '2px solid',
      outlineColor: (theme.vars || theme).palette.text.primary,
    },
    '&:is(label)': {
      userSelect: 'none',
      '&:has(input:focus-visible)': {
        outline: `2px solid ${(theme.vars || theme).palette.primary.main}`,
      },
    },
    '@media (hover: hover)': {
      '&:hover': {
        color: (theme.vars || theme).palette.text.primary,
        backgroundColor: (theme.vars || theme).palette.action.hover,
      },
    },
    '&:has(.SidebarIcon + .SidebarGroupText)': {
      '--_items': 'flex-start',
    },
    variants: [
      {
        props: { active: true },
        style: {
          color: (theme.vars || theme).palette.text.primary,
          backgroundColor: (theme.vars || theme).palette.action.hover,
        },
      },
      {
        props: { hideWhen: 'collapsed' },
        style: {
          display: 'var(--_collapsed, none) var(--_uncollapsed, flex)',
        },
      },
      {
        props: { hideWhen: 'uncollapsed' },
        style: {
          display: 'var(--_collapsed, flex) var(--_uncollapsed, none)',
        },
      },
    ],
  })),
);

export const SidebarMenuButton = React.forwardRef<
  HTMLButtonElement,
  SidebarMenuButtonProps & ButtonBaseProps
>(function SidebarMenuButton(
  { className, active, hideWhen, _before, component, children, ...props },
  ref,
) {
  const ownerState = useMemo(() => ({ active, hideWhen }), [active, hideWhen]);
  return (
    <StyledSidebarMenuButton
      ref={ref}
      className={`${sidebarClasses.menuButton} ${className || ''}`}
      ownerState={ownerState}
      as={
        component ??
        (className?.includes('CollapsibleTrigger') ? 'label' : undefined)
      }
      {...props}
    >
      {_before}
      {children}
    </StyledSidebarMenuButton>
  );
}) as OverridableComponent<SidebarMenuButtonProps, typeof ButtonBase>;
components/sidebar/sidebar-menu-item.tsx
'use client';

import React from 'react';

import { styled } from '@mui/material/styles';

import type {
  ForwardStyledProps,
  OverridableComponent,
} from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarMenuItemProps = ForwardStyledProps<{
  component?: React.ElementType;
}>;

const StyledSidebarMenuItem = styled('div', {
  name: 'SidebarMenuItem',
  slot: 'root',
})({
  display: 'flex',
  flexDirection: 'column',
  position: 'relative',
  '&:has(.SidebarMenuAction)': {
    flexDirection: 'row',
  },
});

export const SidebarMenuItem = React.forwardRef<
  HTMLDivElement,
  SidebarMenuItemProps & React.ComponentPropsWithoutRef<'div'>
>(function SidebarMenuItem({ className, component, ...props }, ref) {
  return (
    <StyledSidebarMenuItem
      ref={ref}
      className={`${sidebarClasses.menuItem} ${className || ''}`}
      as={component}
      {...props}
    />
  );
}) as OverridableComponent<SidebarMenuItemProps, 'div'>;
components/sidebar/sidebar-menu-list.tsx
'use client';

import React, { useMemo } from 'react';

import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';

import type {
  ForwardStyledProps,
  OverridableComponent,
} from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';
import { SidebarMenuButton } from './sidebar-menu-button';

export { SidebarMenuButton };

export type SidebarMenuProps = ForwardStyledProps<{
  nested?: boolean;
  noLine?: boolean;
  component?: React.ElementType;
}>;

export const sidebarMenuListRelaxedStyles = {
  gap: '0.5rem',
  '--item-gap': '0.75rem',
  '--item-h': '2.5rem',
  '--item-fs': '1rem',
  '--item-lh': '1.5rem',
  '--item-px': '0.75rem',
  '--item-py': '0.5rem',
  '--action-size': '2rem',
  '--icon-size': '1.25rem',
};

const StyledSidebarMenuList = styled('div', {
  name: 'SidebarMenuList',
  slot: 'root',
})<{ ownerState: SidebarMenuProps }>(
  {
    display: 'flex',
    flexDirection: 'column',
    gap: '0.25rem',
  },
  memoTheme(({ theme }) => ({
    variants: [
      {
        props: { nested: true },
        style: {
          '--item-py': '0.1875rem', // 3px sweet spot for >1 line of text to have some padding.
          '--item-h': 'calc(var(--item-lh, 1.25rem) + 0.5rem)',
          paddingBlock: '0.25rem',
          position: 'relative',
          marginLeft:
            'calc(var(--icon-w, 1rem) + 2*var(--item-gap, 0.5rem) - 0.5rem)',
          marginRight: 0,
          '&::before': {
            content: '""',
            display: 'block',
            position: 'absolute',
            width: '1px',
            top: '2px',
            bottom: '2px',
            left: 'calc(var(--icon-w, 1rem)/-2 - var(--item-gap, 0.5rem) + 0.5rem)',
            background: (theme.vars || theme).palette.divider,
          },
        },
      },
      {
        props: { nested: true, noLine: true },
        style: {
          '&::before': {
            display: 'none',
          },
        },
      },
    ],
  })),
);

export const SidebarMenuList = React.forwardRef<
  HTMLDivElement,
  SidebarMenuProps & React.ComponentPropsWithoutRef<'div'>
>(function SidebarMenuList(
  { className, nested, noLine, component, ...props },
  ref,
) {
  const ownerState = useMemo(() => ({ nested, noLine }), [nested, noLine]);
  return (
    <StyledSidebarMenuList
      ref={ref}
      className={`${sidebarClasses.menu} ${className || ''}`}
      ownerState={ownerState}
      as={component}
      {...props}
    />
  );
}) as OverridableComponent<SidebarMenuProps, 'div'>;
components/sidebar/sidebar-rail.tsx
'use client';

import React from 'react';

import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';

import type {
  ForwardStyledProps,
  OverridableComponent,
} from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarRailProps = ForwardStyledProps<{
  component?: React.ElementType;
}>;

const StyledSidebarRail = styled('button', {
  name: 'SidebarRail',
  slot: 'root',
})(
  memoTheme(({ theme }) => ({
    '--indicator-w': '6px',
    '--touch-size': '20px',
    '--offset': 'calc(-1 * var(--touch-size)/2)',
    position: 'absolute',
    top: 0,
    bottom: 0,
    right: 'var(--anchorLeft, var(--offset)) var(--anchorRight, unset)',
    left: 'var(--anchorLeft, unset) var(--anchorRight, var(--offset))',
    zIndex: 20,
    width: 'var(--touch-size)',
    textAlign: 'center',
    cursor:
      'var(--_collapsed, e-resize) var(--_uncollapsed, w-resize)' as never,
    display: 'inline-flex',
    justifyContent: 'center',
    border: 'none',
    padding: 0,
    background: 'transparent',
    '&::after': {
      content: '""',
      width: 'var(--indicator-w)',
      height: '100px',
      borderRadius: '10px',
      display: 'block',
      position: 'absolute',
      top: '50%',
      transform: 'translateY(-50%)',
      transition: 'background 0.2s',
    },
    '@media (hover: hover)': {
      '&:hover::after': {
        background: (theme.vars || theme).palette.text.tertiary,
        transition: 'background 0.4s 0.2s',
      },
    },
  })),
);

export const SidebarRail = React.forwardRef<
  HTMLButtonElement,
  SidebarRailProps & React.ComponentPropsWithoutRef<'button'>
>(function SidebarRail({ className, component, ...props }, ref) {
  return (
    <StyledSidebarRail
      ref={ref}
      className={`${sidebarClasses.rail} ${className || ''}`}
      as={component}
      {...props}
    />
  );
}) as OverridableComponent<SidebarRailProps, 'button'>;
components/sidebar/sidebar-text.tsx
'use client';

import React from 'react';

import { styled } from '@mui/material/styles';

import type { ForwardStyledProps } from '../../types/shared/component';
import { sidebarClasses } from './sidebar-classes';

export type SidebarTextProps = ForwardStyledProps;

const StyledSidebarText = styled('span', {
  name: 'SidebarText',
  slot: 'root',
})({
  '--_ws': 'initial',
  display: 'block',
  textOverflow: 'ellipsis',
  transition: 'var(--tsn, var(--_uncollapsed, opacity 0.3s))',
  flex: 'var(--_uncollapsed, 1) var(--_collapsed, 0)',
  opacity: 'var(--_uncollapsed, 1) var(--_collapsed, 0)',
  overflow: 'hidden',
  whiteSpace: 'var(--_ws)',
  [`*:where(.${sidebarClasses.text} + &)`]: {
    fontSize: 'calc(var(--item-fs, 0.875rem) - 0.125rem)',
  },
});

export const SidebarText = React.forwardRef<
  HTMLSpanElement,
  SidebarTextProps & React.ComponentPropsWithoutRef<'span'>
>(function SidebarText({ className, ...props }, ref) {
  return (
    <StyledSidebarText
      ref={ref}
      className={`${sidebarClasses.text} ${className || ''}`}
      {...props}
    />
  );
});
components/sidebar/sidebar-tooltip.tsx
'use client';

import React from 'react';

import Tooltip, { type TooltipProps } from '@mui/material/Tooltip';
import { styled } from '@mui/material/styles';

import { sidebarClasses } from './sidebar-classes';

const StyledTitle = styled('span', {
  name: 'SidebarTooltip',
  slot: 'root',
})({
  maxWidth: 80,
  width: 'max-content',
  display: 'flex',
  '[role="tooltip"]:has(&)': {
    display: 'var(--_collapsed, block) var(--_uncollapsed, none)',
  },
});

interface SidebarTooltipProps extends Omit<
  TooltipProps,
  'title' | 'placement'
> {
  title: React.ReactNode;
  placement?: TooltipProps['placement'];
}

const SidebarTooltip = React.forwardRef<HTMLDivElement, SidebarTooltipProps>(
  function SidebarTooltip(
    { title, placement = 'right', slotProps, ...props },
    forwardedRef,
  ) {
    const [container, setContainer] = React.useState<Element | null>(null);
    const innerRef = React.useRef<HTMLDivElement | null>(null);
    React.useEffect(() => {
      if (innerRef.current) {
        setContainer(innerRef.current.closest('.EdgeSidebar'));
      }
    }, []);
    return (
      <Tooltip
        ref={(node: HTMLDivElement | null) => {
          innerRef.current = node;
          if (typeof forwardedRef === 'function') {
            forwardedRef(node);
          } else if (forwardedRef) {
            forwardedRef.current = node;
          }
        }}
        title={
          <StyledTitle className={sidebarClasses.tooltip}>{title}</StyledTitle>
        }
        placement={placement}
        slotProps={{
          ...slotProps,
          popper: {
            container,
            ...(typeof slotProps?.popper === 'object' && !!slotProps?.popper
              ? slotProps.popper
              : {}),
          },
        }}
        {...props}
      />
    );
  },
);

export { SidebarTooltip };

More from mui-treasury

Browse all components in this library →