Building dynamic sidebars is far from trivial. A maintainable and scalable solution requires a modern, well-structured approach — especially when dealing with multiple user roles and navigation scenarios. Without solid architecture, hardcoded components quickly become unmanageable as your application grows.
Instead of relying on hardcoded items, you can define a sidebar configuration as data and render items dynamically based on user roles. Here’s a clean and extensible approach using TypeScript:
type SidebarItem = {
id: string
title: string
icon: keyof typeof Icons
to: string
allowedRoles: UserRole[]
activeUrls?: string[]
}
const sidebarItems: SidebarItem[] = [
{
id: "dashboard",
title: t("labels.dashboard"),
icon: "dashboard",
to: "/dashboard",
allowedRoles: [UserRole.COLLABORATOR, UserRole.USER, UserRole.WORKER],
},
{
id: "myJobs",
title: t("labels.myJobs"),
icon: "briefcase",
to: "/my-jobs/current",
activeUrls: ["/my-jobs/current", "/my-jobs/draft", "/my-jobs/completed"],
allowedRoles: [UserRole.COLLABORATOR, UserRole.USER, UserRole.WORKER],
},
{
id: "exploreServices",
title: t("labels.exploreServices"),
icon: "search",
to: "/post-a-job",
allowedRoles: [UserRole.USER],
},
{
id: "myProfile",
title: t("labels.myProfile"),
icon: "user",
to: "/my-profile",
allowedRoles: [UserRole.WORKER],
},
]
The SidebarItem type defines each item's structure — including id, title, icon, destination route (to), and the allowedRoles that determine visibility. The optional activeUrls helps match nested routes to highlight the correct item.
🔧
Iconsis a key-value object where each key (e.g., "dashboard", "briefcase") maps to a corresponding icon component. This allows dynamic rendering likeconst Icon = Icons[item.icon].
By defining each item in a structured array, you're free to map over them and render only what the current user is allowed to see:
{sidebarItems.map(item => {
const Icon = Icons[item.icon]
return (
item.allowedRoles?.includes(user?.role as UserRole) && (
<Tooltip key={item.to}>
<TooltipTrigger asChild>
<Button
asChild
variant="ghost"
className={cn(
"text-sm",
open && "justify-start",
(location.pathname === item.to ||
item.activeUrls?.includes(location.pathname)) &&
"bg-accent"
)}
>
<Link to={item.to}>
<Icon className={cn("h-5 w-5 shrink-0", open && "mr-2")} />
<span className={open ? "block" : "hidden"}>
{item.title}
</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent side="right" className={open ? "hidden" : ""}>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
)
)
})}
- This conditionally renders each sidebar item only if the current user's role is included in the
allowedRolesarray for that item. -
item.allowedRoles?.includes(...)checks whetheruser.roleexists in theallowedRolesarray. The optional chaining (?.) prevents errors ifallowedRolesis undefined. -
user?.roleasUserRolesafely accesses the user's role and asserts its type in TypeScript.
This setup is flexible enough to support localization, permissions, tooltips, role conditions, active state tracking, and more — all without making your component hard to read or extend.
Top comments (0)