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 · lightui-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 };