Skip to content

Instantly share code, notes, and snippets.

@damnordicus
Last active February 3, 2026 02:56
Show Gist options
  • Select an option

  • Save damnordicus/9e30e22cd96862800aeb12c95a5bb1d3 to your computer and use it in GitHub Desktop.

Select an option

Save damnordicus/9e30e22cd96862800aeb12c95a5bb1d3 to your computer and use it in GitHub Desktop.
app sidebar component using recursive collapsible components
<script lang="ts">
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import Separator from "./ui/separator/separator.svelte";
import { DoorOpenIcon, User, ChevronDown, LogIn } from "@lucide/svelte";
import { useSidebar } from "$lib/components/ui/sidebar/index.js";
import { Collapsible } from "bits-ui";
import { navItems, type NavItem } from "$lib/utils";
const settingsMenu = [
{
title: "Admin",
url: "/admin",
icon: User,
},
{
title: "Login",
url: "/login",
icon: LogIn
}
];
const { setOpenMobile } = useSidebar();
// Calculate padding based on depth (in rem units)
function getIndentClass(depth: number): string {
const paddings = ['pl-2', 'pl-6', 'pl-10', 'pl-14', 'pl-18'];
return paddings[depth] || paddings[paddings.length - 1];
}
</script>
<Sidebar.Root class="md:hidden">
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Application</Sidebar.GroupLabel>
<Sidebar.GroupContent>
{#each navItems as item (item.title)}
{@render renderNavItem(item, 0)}
{/each}
</Sidebar.GroupContent>
</Sidebar.Group>
<Separator />
<Sidebar.Group>
<Sidebar.GroupLabel>Settings</Sidebar.GroupLabel>
<Sidebar.GroupContent>
{#each settingsMenu as item (item.title)}
<a
href={item.url}
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent pl-2"
onclick={() => setOpenMobile(false)}
>
<item.icon class="h-4 w-4 shrink-0" />
<span>{item.title}</span>
</a>
{/each}
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar.Root>
{#snippet renderNavItem(item: NavItem, depth: number)}
{#if item.sub && item.sub.length > 0}
<Collapsible.Root class="group/nav">
<Collapsible.Trigger class="w-full">
<button
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent {getIndentClass(depth)}"
>
{#if item.icon}
<item.icon class="h-4 w-4 shrink-0" />
{/if}
<span class="flex-1 text-left">{item.title}</span>
<ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200 group-data-[state=open]/nav:rotate-180" />
</button>
</Collapsible.Trigger>
<Collapsible.Content>
{#each item.sub as subItem}
{@render renderNavItem(subItem, depth + 1)}
{/each}
</Collapsible.Content>
</Collapsible.Root>
{:else}
<a
href={item.url}
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent {getIndentClass(depth)}"
onclick={() => setOpenMobile(false)}
>
{#if item.icon}
<item.icon class="h-4 w-4 shrink-0" />
{/if}
<span class="flex-1 text-left">{item.title}</span>
</a>
{/if}
{/snippet}
export type NavItem = {
title: string;
url: string;
icon?: typeof HouseIcon;
sub?: NavItem[];
};
export const navItems = [
{
title: "Home",
url: "/",
icon: HouseIcon,
},
{
title: "Facilities",
url: "#",
icon: Building2Icon,
sub: [
{
title: "Fitness Center",
url: "/facilities/fitness-center",
sub: [
{
title: "FAC",
url: "/facilities/fitness-center/fac",
sub: [
{
title: "Score Charts",
url: "/facilities/fitness-center/fac/score-charts"
}
]
}
]
},
{
title: "Nose Dock",
url: "/facilities/nose-dock",
},
{
title: "Jungle",
url: "/facilities/jungle",
},
{
title: "Running Routes",
url: "/facilities/running-routes"
},
]
},
{
title: "Health Promotion",
url: "#",
icon: HeartPlus,
sub: [
{
title: "Fitness",
url: "/health-promotion/fitness",
},
{
title: "Sleep Optimization",
url: "/health-promotion/sleep-optimization",
},
{
title: "Tobacco/ Nicotine Cessation",
url: "/health-promotion/tobacco-nicotine",
},
{
title: "Body Composition",
url: "/health-promotion/body-composition",
},
{
title: "Nutrition",
url: "/health-promotion/nutrition",
sub: [
{
title: "DFAC",
url: "/health-promotion/dfac",
sub: [
{
title: "Go 4 Green",
url: "/health-promotion/dfac/go-4-green",
}
]
}
]
},
]
},
{
title: "Programs",
url: "#",
icon: NotebookText,
sub: [
{
title: "FIP Classes",
url: "/programs/fip-classes",
sub: [
{
title: "Combat Ready",
url: "/programs/fip-classes/combat-ready",
},
{
title: "Cardio",
url: "/programs/fip-classes/cardio",
},
{
title: "Strength",
url: "/programs/fip-classes/strength",
},
]
},
{
title: "Self-Directed",
url: "/programs/self-directed",
sub: [
{
title: "Cardio",
url: "/programs/self-directed/cardio",
},
{
title: "Strength",
url: "/programs/self-directed/strength",
}
]
}
]
},
{
title: "Community",
url: "#",
icon: UsersIcon,
sub: [
{
title: "Running",
url: "/community/running",
},
{
title: "Cycling",
url: "/community/cycling",
},
{
title: "Group X",
url: "/community/group-x",
}
]
},
{
title: "Challenges",
url: "#",
icon: SwordsIcon,
sub: [
{
title: "Fitness Incentive",
url: "/challenges/fitness-incentive",
sub: [
{
title: "Warrior Ready",
url: "/challenges/fitness-incentive/warrior-ready",
}
]
},
{
title: "PFA 100 Club",
url: "/challenges/pfa-100-club"
}
]
}
];
@jmidd2
Copy link

jmidd2 commented Feb 3, 2026

  • Collapsible should be imported from your components not bits-ui
  • <Collapsible.Root class="group/nav"> is using the same group class
  • in <Collapsible.Trigger class="w-full"> you'll want to use the child() snippet so you can grab props and add it to the <button>
  • Potentially the same thing in <Collapsible.Content> inside the #each

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment