shadimage

Sidebar

Sidebar layout for Tile UI.

packages/react/src/components/sidebar/sidebar.tsx

import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Slot } from '@radix-ui/react-slot';
import {
	createMediaQueryWatcher,
	getSidebarMenuButtonStyleKeys,
	getSidebarMenuSubButtonStyleKeys,
	getSidebarState,
	matchesSidebarToggleShortcut,
	SIDEBAR_MEDIA_QUERY,
	SIDEBAR_WIDTH,
	SIDEBAR_WIDTH_ICON,
	SIDEBAR_WIDTH_MOBILE,
	sidebarStyleKeys,
} from '../lib/core';
import type { SidebarBaseProps, SidebarContextValue, SidebarMenuButtonSize, SidebarMenuButtonVariant, SidebarProviderBaseProps } from '../lib/core';
import { Button } from '../button';
import { Input } from '../input';
import { Separator } from '../separator';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '../sheet';
import { Skeleton } from '../skeleton';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../tooltip';
import styles from './sidebar.module.scss';

const SidebarContext = createContext<SidebarContextValue | null>(null);

function useSidebar(): SidebarContextValue {
	const context = useContext(SidebarContext);
	if (!context) {
		throw new Error('useSidebar must be used within a <SidebarProvider>.');
	}
	return context;
}

export interface SidebarProviderProps extends React.HTMLAttributes<HTMLDivElement>, SidebarProviderBaseProps {
	children?: React.ReactNode;
}

function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className = '', style, children, ...props }: SidebarProviderProps) {
	const [isMobile, setIsMobile] = useState(false);
	const [openMobile, setOpenMobile] = useState(false);

	const [_open, _setOpen] = useState(defaultOpen);
	const open = openProp ?? _open;
	const setOpen = useCallback(
		(value: boolean | ((value: boolean) => boolean)) => {
			const openState = typeof value === 'function' ? value(open) : value;
			if (setOpenProp) {
				setOpenProp(openState);
			} else {
				_setOpen(openState);
			}
		},
		[setOpenProp, open],
	);

	const toggleSidebar = useCallback(() => {
		return isMobile ? setOpenMobile((value) => !value) : setOpen((value) => !value);
	}, [isMobile, setOpen, setOpenMobile]);

	useEffect(() => {
		const handleKeyDown = (event: KeyboardEvent) => {
			if (matchesSidebarToggleShortcut({ key: event.key, metaKey: event.metaKey, ctrlKey: event.ctrlKey })) {
				event.preventDefault();
				toggleSidebar();
			}
		};
		window.addEventListener('keydown', handleKeyDown);
		return () => window.removeEventListener('keydown', handleKeyDown);
	}, [toggleSidebar]);

	useEffect(() => {
		return createMediaQueryWatcher(SIDEBAR_MEDIA_QUERY, setIsMobile);
	}, []);

	const state = getSidebarState(open);

	const contextValue = useMemo<SidebarContextValue>(
		() => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar }),
		[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
	);

	return (
		<SidebarContext.Provider value={contextValue}>
			<TooltipProvider delayDuration={0}>
				<div
					data-slot="sidebar-wrapper"
					style={{ '--sidebar-width': SIDEBAR_WIDTH, '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, ...style } as React.CSSProperties}
					className={`${styles[sidebarStyleKeys.wrapper]} ${className}`}
					{...props}>
					{children}
				</div>
			</TooltipProvider>
		</SidebarContext.Provider>
	);
}

SidebarProvider.displayName = 'SidebarProvider';

export interface SidebarProps extends React.HTMLAttributes<HTMLDivElement>, SidebarBaseProps {}

function Sidebar({ side = 'left', variant = 'sidebar', collapsible = 'offcanvas', className = '', style, children, ...props }: SidebarProps) {
	const { isMobile, state, openMobile, setOpenMobile } = useSidebar();

	if (collapsible === 'none') {
		return (
			<div data-slot="sidebar" data-side={side} className={`${styles[sidebarStyleKeys.sidebar]} ${className}`} {...props} style={style}>
				{children}
			</div>
		);
	}

	if (isMobile) {
		return (
			<Sheet open={openMobile} onOpenChange={setOpenMobile}>
				<SheetContent
					data-slot="sidebar"
					data-mobile="true"
					side={side}
					className={`${styles[sidebarStyleKeys.sheetContent]} ${className}`}
					style={{ '--sidebar-width': SIDEBAR_WIDTH_MOBILE, ...style } as React.CSSProperties}>
					<SheetHeader className={styles[sidebarStyleKeys.srOnly]}>
						<SheetTitle>Sidebar</SheetTitle>
						<SheetDescription>Displays the mobile sidebar.</SheetDescription>
					</SheetHeader>
					<div className={styles[sidebarStyleKeys.sidebar]}>{children}</div>
				</SheetContent>
			</Sheet>
		);
	}

	return (
		<div
			data-slot="sidebar-container"
			data-state={state}
			data-collapsible={state === 'collapsed' ? collapsible : ''}
			data-variant={variant}
			data-side={side}
			className={styles[sidebarStyleKeys.container]}
			style={style}>
			<div data-slot="sidebar" className={`${styles[sidebarStyleKeys.sidebar]} ${className}`} {...props}>
				{children}
			</div>
		</div>
	);
}

Sidebar.displayName = 'Sidebar';

export interface SidebarTriggerProps extends React.ComponentProps<typeof Button> {}

const SidebarTrigger = React.forwardRef<HTMLButtonElement, SidebarTriggerProps>(({ className = '', onClick, children, ...props }, ref) => {
	const { toggleSidebar } = useSidebar();

	return (
		<Button
			ref={ref}
			data-slot="sidebar-trigger"
			variant="ghost"
			size="icon"
			className={`${styles[sidebarStyleKeys.trigger]} ${className}`}
			onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
				onClick?.(event);
				toggleSidebar();
			}}
			{...props}>
			{children ?? (
				<svg
					xmlns="http://www.w3.org/2000/svg"
					width="24"
					height="24"
					viewBox="0 0 24 24"
					fill="none"
					stroke="currentColor"
					strokeWidth="2"
					strokeLinecap="round"
					strokeLinejoin="round"
					aria-hidden="true">
					<rect width="18" height="18" x="3" y="3" rx="2" />
					<path d="M9 3v18" />
				</svg>
			)}
			<span className={styles[sidebarStyleKeys.srOnly]}>Toggle Sidebar</span>
		</Button>
	);
});
SidebarTrigger.displayName = 'SidebarTrigger';

export interface SidebarRailProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}

const SidebarRail = React.forwardRef<HTMLButtonElement, SidebarRailProps>(({ className = '', ...props }, ref) => {
	const { toggleSidebar } = useSidebar();

	return (
		<button
			ref={ref}
			data-slot="sidebar-rail"
			aria-label="Toggle Sidebar"
			tabIndex={-1}
			onClick={toggleSidebar}
			title="Toggle Sidebar"
			className={`${styles[sidebarStyleKeys.rail] ?? ''} ${className}`}
			{...props}
		/>
	);
});
SidebarRail.displayName = 'SidebarRail';

export interface SidebarInsetProps extends React.HTMLAttributes<HTMLElement> {}

const SidebarInset = React.forwardRef<HTMLElement, SidebarInsetProps>(({ className = '', ...props }, ref) => {
	return <main ref={ref} data-slot="sidebar-inset" className={`${styles[sidebarStyleKeys.inset]} ${className}`} {...props} />;
});
SidebarInset.displayName = 'SidebarInset';

export interface SidebarInputProps extends React.ComponentProps<typeof Input> {}

const SidebarInput = React.forwardRef<HTMLInputElement, SidebarInputProps>(({ className = '', ...props }, ref) => {
	return <Input ref={ref} data-slot="sidebar-input" className={`${styles[sidebarStyleKeys.input]} ${className}`} {...props} />;
});
SidebarInput.displayName = 'SidebarInput';

export interface SidebarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}

const SidebarHeader = React.forwardRef<HTMLDivElement, SidebarHeaderProps>(({ className = '', ...props }, ref) => {
	return <div ref={ref} data-slot="sidebar-header" className={`${styles[sidebarStyleKeys.header]} ${className}`} {...props} />;
});
SidebarHeader.displayName = 'SidebarHeader';

export interface SidebarFooterProps extends React.HTMLAttributes<HTMLDivElement> {}

const SidebarFooter = React.forwardRef<HTMLDivElement, SidebarFooterProps>(({ className = '', ...props }, ref) => {
	return <div ref={ref} data-slot="sidebar-footer" className={`${styles[sidebarStyleKeys.footer]} ${className}`} {...props} />;
});
SidebarFooter.displayName = 'SidebarFooter';

export interface SidebarSeparatorProps extends React.ComponentProps<typeof Separator> {}

const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, SidebarSeparatorProps>(({ className = '', ...props }, ref) => {
	return <Separator ref={ref} data-slot="sidebar-separator" className={`${styles[sidebarStyleKeys.separator]} ${className}`} {...props} />;
});
SidebarSeparator.displayName = 'SidebarSeparator';

export interface SidebarContentProps extends React.HTMLAttributes<HTMLDivElement> {}

const SidebarContent = React.forwardRef<HTMLDivElement, SidebarContentProps>(({ className = '', ...props }, ref) => {
	return <div ref={ref} data-slot="sidebar-content" className={`${styles[sidebarStyleKeys.content]} ${className}`} {...props} />;
});
SidebarContent.displayName = 'SidebarContent';

export interface SidebarGroupProps extends React.HTMLAttributes<HTMLDivElement> {}

const SidebarGroup = React.forwardRef<HTMLDivElement, SidebarGroupProps>(({ className = '', ...props }, ref) => {
	return <div ref={ref} data-slot="sidebar-group" className={`${styles[sidebarStyleKeys.group]} ${className}`} {...props} />;
});
SidebarGroup.displayName = 'SidebarGroup';

export interface SidebarGroupLabelProps extends React.HTMLAttributes<HTMLDivElement> {
	asChild?: boolean;
}

const SidebarGroupLabel = React.forwardRef<HTMLDivElement, SidebarGroupLabelProps>(({ className = '', asChild = false, ...props }, ref) => {
	const Comp = asChild ? Slot : 'div';

	return <Comp ref={ref} data-slot="sidebar-group-label" className={`${styles[sidebarStyleKeys.groupLabel]} ${className}`} {...props} />;
});
SidebarGroupLabel.displayName = 'SidebarGroupLabel';

export interface SidebarGroupActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
	asChild?: boolean;
}

const SidebarGroupAction = React.forwardRef<HTMLButtonElement, SidebarGroupActionProps>(({ className = '', asChild = false, ...props }, ref) => {
	const Comp = asChild ? Slot : 'button';

	return <Comp ref={ref} data-slot="sidebar-group-action" className={`${styles[sidebarStyleKeys.groupAction]} ${className}`} {...props} />;
});
SidebarGroupAction.displayName = 'SidebarGroupAction';

export interface SidebarGroupContentProps extends React.HTMLAttributes<HTMLDivElement> {}

const SidebarGroupContent = React.forwardRef<HTMLDivElement, SidebarGroupContentProps>(({ className = '', ...props }, ref) => {
	return <div ref={ref} data-slot="sidebar-group-content" className={`${styles[sidebarStyleKeys.groupContent]} ${className}`} {...props} />;
});
SidebarGroupContent.displayName = 'SidebarGroupContent';

export interface SidebarMenuProps extends React.HTMLAttributes<HTMLUListElement> {}

const SidebarMenu = React.forwardRef<HTMLUListElement, SidebarMenuProps>(({ className = '', ...props }, ref) => {
	return <ul ref={ref} data-slot="sidebar-menu" className={`${styles[sidebarStyleKeys.menu]} ${className}`} {...props} />;
});
SidebarMenu.displayName = 'SidebarMenu';

export interface SidebarMenuItemProps extends React.HTMLAttributes<HTMLLIElement> {}

const SidebarMenuItem = React.forwardRef<HTMLLIElement, SidebarMenuItemProps>(({ className = '', ...props }, ref) => {
	return <li ref={ref} data-slot="sidebar-menu-item" className={`${styles[sidebarStyleKeys.menuItem]} ${className}`} {...props} />;
});
SidebarMenuItem.displayName = 'SidebarMenuItem';

export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
	asChild?: boolean;
	isActive?: boolean;
	tooltip?: string | React.ComponentProps<typeof TooltipContent>;
	variant?: SidebarMenuButtonVariant;
	size?: SidebarMenuButtonSize;
}

const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(
	({ asChild = false, isActive = false, variant = 'default', size = 'default', tooltip, className = '', ...props }, ref) => {
		const Comp = asChild ? Slot : 'button';
		const styleKeys = getSidebarMenuButtonStyleKeys(variant, size);

		const button = (
			<Comp
				ref={ref}
				data-slot="sidebar-menu-button"
				data-size={size}
				data-active={isActive}
				className={`${styles[styleKeys.base]} ${styles[styleKeys.variant]} ${styles[styleKeys.size]} ${className}`}
				{...props}
			/>
		);

		if (!tooltip) {
			return button;
		}

		const tooltipProps = typeof tooltip === 'string' ? { children: tooltip } : tooltip;

		return (
			<Tooltip>
				<TooltipTrigger asChild>{button}</TooltipTrigger>
				<TooltipContent side="right" {...tooltipProps} />
			</Tooltip>
		);
	},
);
SidebarMenuButton.displayName = 'SidebarMenuButton';

export interface SidebarMenuActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
	asChild?: boolean;
	showOnHover?: boolean;
}

const SidebarMenuAction = React.forwardRef<HTMLButtonElement, SidebarMenuActionProps>(({ className = '', asChild = false, showOnHover = false, ...props }, ref) => {
	const Comp = asChild ? Slot : 'button';

	return <Comp ref={ref} data-slot="sidebar-menu-action" data-show-on-hover={showOnHover} className={`${styles[sidebarStyleKeys.menuAction]} ${className}`} {...props} />;
});
SidebarMenuAction.displayName = 'SidebarMenuAction';

export interface SidebarMenuBadgeProps extends React.HTMLAttributes<HTMLDivElement> {}

const SidebarMenuBadge = React.forwardRef<HTMLDivElement, SidebarMenuBadgeProps>(({ className = '', ...props }, ref) => {
	return <div ref={ref} data-slot="sidebar-menu-badge" className={`${styles[sidebarStyleKeys.menuBadge]} ${className}`} {...props} />;
});
SidebarMenuBadge.displayName = 'SidebarMenuBadge';

export interface SidebarMenuSkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
	showIcon?: boolean;
}

const SidebarMenuSkeleton = React.forwardRef<HTMLDivElement, SidebarMenuSkeletonProps>(({ className = '', showIcon = false, ...props }, ref) => {
	const width = useMemo(() => `${Math.floor(Math.random() * 40) + 50}%`, []);

	return (
		<div ref={ref} data-slot="sidebar-menu-skeleton" className={`${styles[sidebarStyleKeys.skeleton]} ${className}`} {...props}>
			{showIcon && <Skeleton className={styles[sidebarStyleKeys.skeletonIcon]} />}
			<Skeleton className={styles[sidebarStyleKeys.skeletonText]} style={{ '--skeleton-width': width } as React.CSSProperties} />
		</div>
	);
});
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';

export interface SidebarMenuSubProps extends React.HTMLAttributes<HTMLUListElement> {}

const SidebarMenuSub = React.forwardRef<HTMLUListElement, SidebarMenuSubProps>(({ className = '', ...props }, ref) => {
	return <ul ref={ref} data-slot="sidebar-menu-sub" className={`${styles[sidebarStyleKeys.menuSub]} ${className}`} {...props} />;
});
SidebarMenuSub.displayName = 'SidebarMenuSub';

export interface SidebarMenuSubItemProps extends React.HTMLAttributes<HTMLLIElement> {}

const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, SidebarMenuSubItemProps>(({ className = '', ...props }, ref) => {
	return <li ref={ref} data-slot="sidebar-menu-sub-item" className={`${styles[sidebarStyleKeys.menuSubItem]} ${className}`} {...props} />;
});
SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';

export interface SidebarMenuSubButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
	asChild?: boolean;
	size?: 'sm' | 'md';
	isActive?: boolean;
}

const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(({ asChild = false, size = 'md', isActive = false, className = '', ...props }, ref) => {
	const Comp = asChild ? Slot : 'a';
	const styleKeys = getSidebarMenuSubButtonStyleKeys(size);

	return (
		<Comp
			ref={ref}
			data-slot="sidebar-menu-sub-button"
			data-size={size}
			data-active={isActive}
			className={`${styles[styleKeys.base]} ${styles[styleKeys.size]} ${className}`}
			{...props}
		/>
	);
});
SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';

export {
	Sidebar,
	SidebarContent,
	SidebarFooter,
	SidebarGroup,
	SidebarGroupAction,
	SidebarGroupContent,
	SidebarGroupLabel,
	SidebarHeader,
	SidebarInput,
	SidebarInset,
	SidebarMenu,
	SidebarMenuAction,
	SidebarMenuBadge,
	SidebarMenuButton,
	SidebarMenuItem,
	SidebarMenuSkeleton,
	SidebarMenuSub,
	SidebarMenuSubButton,
	SidebarMenuSubItem,
	SidebarProvider,
	SidebarRail,
	SidebarSeparator,
	SidebarTrigger,
	useSidebar,
};
export default Sidebar;
Where it goes
sidebardashboard-shell
Use cases
dashboard

DependenciesBundle weight · light

ui-primitives
@radix-ui/react-slot