Skip to main content

Sidebar Architecture

The sidebar architecture in this template is designed as a centralized state system that manages responsive navigation, user preferences, and layout persistence. It leverages React Context to synchronize the sidebar's behavior across the application, ensuring a consistent experience between desktop and mobile viewports.

State Orchestration and Persistence

The core of the sidebar system is the SidebarProvider found in frontend/src/components/ui/sidebar.tsx. This component acts as the state orchestrator, managing two distinct types of visibility:

  1. Desktop State (open): Controls whether the sidebar is expanded or collapsed into an icon-only view.
  2. Mobile State (openMobile): Controls the visibility of the sidebar as an overlay (Sheet) on smaller screens.

A key design choice in SidebarProvider is the use of browser cookies for state persistence. This ensures that a user's preference for an expanded or collapsed sidebar is maintained across page reloads without requiring a backend database call.

// frontend/src/components/ui/sidebar.tsx

const getInitialOpen = () => {
if (typeof document === "undefined") return defaultOpen

const cookie = document.cookie
.split("; ")
.find((c) => c.startsWith(`${SIDEBAR_COOKIE_NAME}=`))

if (!cookie) return defaultOpen

return cookie.split("=")[1] === "true"
}

const [_open, _setOpen] = React.useState(getInitialOpen)

const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
// ... logic to update state
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open],
)

The SidebarContextProps interface defines the API available to any component within the provider, including the state (either "expanded" or "collapsed"), which is used to drive Tailwind CSS data attributes for styling.

Responsive Behavior

The architecture differentiates between desktop and mobile environments using the useIsMobile hook. This distinction dictates the underlying DOM structure:

  • Mobile: The sidebar is rendered inside a Radix UI Sheet component, providing an accessible modal overlay that slides in from the side.
  • Desktop: The sidebar is a persistent div that can be toggled between full width (SIDEBAR_WIDTH) and icon width (SIDEBAR_WIDTH_ICON).

In frontend/src/components/Sidebar/Main.tsx, the Main component consumes this context to handle navigation events. On mobile, clicking a link must explicitly close the sidebar to reveal the content, a behavior managed by the handleMenuClick function:

// frontend/src/components/Sidebar/Main.tsx

export function Main({ items }: MainProps) {
const { isMobile, setOpenMobile } = useSidebar()

const handleMenuClick = () => {
if (isMobile) {
setOpenMobile(false)
}
}

// ... rendering logic
}

Navigation items are defined using the Item type, which standardizes the requirements for a sidebar link: an icon from lucide-react, a title, and a destination path.

The implementation uses the "asChild" pattern from @radix-ui/react-slot. This allows SidebarMenuButton to act as a wrapper for the RouterLink from TanStack Router, merging their props and ensuring that the navigation remains a single semantic anchor tag while inheriting the sidebar's interactive styles.

Loading States

For asynchronous content or initial application boot, the SidebarMenuSkeleton provides a visual placeholder. It uses a randomized width for the text skeleton to create a more natural "shimmer" effect that mimics varying label lengths:

// frontend/src/components/ui/sidebar.tsx

function SidebarMenuSkeleton({ showIcon = false, ...props }) {
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])

return (
<div data-sidebar="menu-skeleton" className="...">
{showIcon && <Skeleton className="size-4 rounded-md" />}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
style={{ "--skeleton-width": width } as React.CSSProperties}
/>
</div>
)
}

User Context Integration

The sidebar also serves as a primary entry point for user-specific actions. The UserInfoProps interface in frontend/src/components/Sidebar/User.tsx defines the data required to render the user profile section, typically located in the SidebarFooter. This component integrates with the useSidebar hook to ensure that user details are either fully displayed or truncated/hidden when the sidebar is in its collapsed state.

Design Tradeoffs

  • Cookie Persistence: While using cookies allows for state persistence without a database, it introduces a dependency on the document object, which requires the getInitialOpen check to prevent errors during potential Server-Side Rendering (SSR) or static generation phases.
  • Hardcoded Breakpoints: The responsive logic relies on a fixed 768px breakpoint. While consistent with Tailwind's md prefix, it means the sidebar's transition from a persistent element to a mobile drawer is not configurable via props.
  • Context Dependency: Components like SidebarTrigger and Main are tightly coupled to the SidebarProvider. Using them outside of this provider will result in a runtime error, as enforced by the useSidebar hook's safety check.