Sidebar
A composable, themeable, and customizable sidebar component.
use leptos::prelude::*; use singlestage::*; #[derive(Clone, PartialEq)] struct Team { name: String, logo: icondata::Icon, plan: String, } #[component] fn TeamSwitcher(teams: StoredValue<Vec<Team>>) -> impl IntoView { let active_team = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-(--sidebar-accent) aria-expanded:text-(--sidebar-accent-foreground)" > {move || { let active_team = &teams.get_value()[active_team.get()]; view! { <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-(--sidebar-primary) text-(--sidebar-primary-foreground)"> { let logo = active_team.logo; icon!(logo, class="size-4") } </div> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {active_team.name.to_owned()} </span> <span class="truncate text-xs"> {active_team.plan.to_owned()} </span> </div> } }} {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" side="bottom" align="center"> <DropdownMenuLabel class="text-xs text-muted-foreground"> "Teams" </DropdownMenuLabel> <For each=move || teams.get_value() key=|team| team.name.clone() children=move |team: Team| { let index = teams .get_value() .iter() .position(|t| t == &team) .unwrap_or_default(); view! { <DropdownMenuItem class="gap-2 p-2" on:click={move |_| { active_team.set(index) }} > <div class="flex size-6 items-center justify-center rounded-md border"> { let logo = team.logo; icon!(logo, class="size-3.5 shrink-0") } </div> {team.name} </DropdownMenuItem> } } /> <DropdownMenuSeparator /> <DropdownMenuItem class="gap-2 p-2"> <div class="flex size-6 items-center justify-center rounded-md border bg-transparent"> {icon!(icondata::LuPlus, class="size-4")} </div> <div class="font-medium text-muted-foreground">"Add team"</div> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, icon: icondata::Icon, is_active: bool, items: Vec<NavSubItem>, } #[component] fn NavMain(items: StoredValue<Vec<NavItem>>) -> impl IntoView { view! { <SidebarGroup> <SidebarGroupLabel>"Platform"</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> <For each=move || items.get_value() key=|item| item.title.clone() children=|item: NavItem| { let tooltip = item.title.clone(); view! { <Collapsible open={item.is_active}> <SidebarMenuItem> <CollapsibleTrigger> <SidebarMenuButton tooltip> <Button class="aria-expanded:[&>svg:last-child]:rotate-90"> { let icon = item.icon; icon!(icon) } <span>{item.title}</span> {icon!( icondata::LuChevronRight, class="ml-auto transition-transform duration-200" )} </Button> </SidebarMenuButton> </CollapsibleTrigger> <CollapsibleContent> <SidebarMenuSub> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(sub_item) > <SidebarMenuSubItem> <SidebarMenuSubButton> <Link href={sub_item.url}> <span>{sub_item.title}</span> </Link> </SidebarMenuSubButton> </SidebarMenuSubItem> </For> </SidebarMenuSub> </CollapsibleContent> </SidebarMenuItem> </Collapsible> } } /> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> } } #[derive(Clone)] struct Project { name: String, icon: icondata::Icon, url: String, } #[component] fn NavProjects(projects: StoredValue<Vec<Project>>) -> impl IntoView { let sidebar = expect_context::<SidebarContext>(); view! { <Show when=move || sidebar.open.get()> <SidebarGroup class="group-data-[collapsible=icon]:hidden"> <SidebarGroupLabel>"Projects"</SidebarGroupLabel> <SidebarMenu> <For each=move || projects.get_value() key=|project| project.name.clone() let(project) > <SidebarMenuItem> <SidebarMenuButton> <Link href={project .url}> { let icon = project.icon; icon!(icon) } <span>{project.name}</span> </Link> </SidebarMenuButton> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuAction show_on_hover=true> {icon!(icondata::FiMoreHorizontal)} <span class="sr-only">"More"</span> </SidebarMenuAction> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" align="end" side="bottom" > <DropdownMenuItem> {icon!(icondata::LuFolder, class="text-muted-foreground")} <span>"View Project"</span> </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuForward, class="text-muted-foreground")} <span>"Share Project"</span> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem> {icon!(icondata::LuTrash2, class="text-(muted-foreground)")} <span>"Delete Project"</span> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </For> <SidebarMenuItem> <SidebarMenuButton> <Button class="text-sidebar-foreground/70"> {icon!(icondata::FiMoreHorizontal)} <span>"More"</span> </Button> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarGroup> </Show> } } #[derive(Clone)] struct User { name: String, email: String, avatar: String, } #[component] fn NavUser(user: StoredValue<User>) -> impl IntoView { view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <Avatar class="h-8 w-8 rounded-lg"> <AvatarImage src={user.get_value().avatar} alt={user.get_value().name} /> <AvatarFallback class="rounded-lg">"SD"</AvatarFallback> </Avatar> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {user.get_value().name} </span> <span class="truncate text-xs">{user.get_value().email}</span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto size-4")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" align="center" side="top"> <DropdownMenuLabel class="p-0 font-normal"> <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <Avatar class="h-8 w-8 rounded-lg"> <AvatarImage src={user.get_value().avatar} alt={user.get_value().name} /> <AvatarFallback class="rounded-lg">"CN"</AvatarFallback> </Avatar> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {user.get_value().name} </span> <span class="truncate text-xs">{user.get_value().email}</span> </div> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> {icon!(icondata::LuSparkles)} "Upgrade to Pro" </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> {icon!(icondata::LuBadgeCheck)} "Account" </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuCreditCard)} "Billing" </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuBell)} "Notifications" </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuItem>{icon!(icondata::LuLogOut)} "Log out"</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[component] fn AppSidebar() -> impl IntoView { let user = StoredValue::new(User { name: "Sofia Davis".to_string(), email: "m@example.com".to_string(), avatar: "/avatar-1.png".to_string(), }); let teams = StoredValue::new(vec![ Team { name: "Acme Inc".to_string(), logo: icondata::LuGalleryVerticalEnd, plan: "Enterprise".to_string(), }, Team { name: "Acme Corp.".to_string(), logo: icondata::LuAudioWaveform, plan: "Startup".to_string(), }, Team { name: "Evil Corp.".to_string(), logo: icondata::LuCommand, plan: "Free".to_string(), }, ]); let items = StoredValue::new(vec![ NavItem { title: "Playground".to_string(), icon: icondata::LuSquareTerminal, is_active: true, items: vec![ NavSubItem { title: "History".to_string(), url: "url".to_string(), }, NavSubItem { title: "Starred".to_string(), url: "url".to_string(), }, NavSubItem { title: "Settings".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Models".to_string(), icon: icondata::LuBot, is_active: false, items: vec![ NavSubItem { title: "Genesis".to_string(), url: "url".to_string(), }, NavSubItem { title: "Explorer".to_string(), url: "url".to_string(), }, NavSubItem { title: "Quantum".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Documentation".to_string(), icon: icondata::LuBookOpen, is_active: false, items: vec![ NavSubItem { title: "Introduction".to_string(), url: "url".to_string(), }, NavSubItem { title: "Get Started".to_string(), url: "url".to_string(), }, NavSubItem { title: "Tutorials".to_string(), url: "url".to_string(), }, NavSubItem { title: "Changelog".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Settings".to_string(), icon: icondata::LuSettings2, is_active: false, items: vec![ NavSubItem { title: "General".to_string(), url: "url".to_string(), }, NavSubItem { title: "Team".to_string(), url: "url".to_string(), }, NavSubItem { title: "Billing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Limits".to_string(), url: "url".to_string(), }, ], }, ]); let projects = StoredValue::new(vec![ Project { name: "Design Engineering".to_string(), url: "url".to_string(), icon: icondata::LuFrame, }, Project { name: "Sales & Marketing".to_string(), url: "url".to_string(), icon: icondata::FiPieChart, }, Project { name: "Travel".to_string(), url: "url".to_string(), icon: icondata::LuMap, }, ]); view! { <Sidebar collapsible="icon"> <SidebarHeader> <TeamSwitcher teams /> </SidebarHeader> <SidebarContent> <NavMain items /> <NavProjects projects /> </SidebarContent> <SidebarFooter> <NavUser user /> </SidebarFooter> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarIconExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 border-b px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link href="#">"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Sections
A simple sidebar with navigation grouped by section
use leptos::prelude::*; use singlestage::*; #[component] fn VersionSwitcher(versions: StoredValue<Vec<String>>) -> impl IntoView { let selected_version = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> {icon!(icondata::LuGalleryVerticalEnd, class="size-4")} </div> <div class="flex flex-col gap-0.5 leading-none"> <span class="font-medium">"Documentation"</span> <span> "v" {move || { versions.get_value()[selected_version.get()].to_owned() }} </span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58" align="center" side="bottom"> <For each=move || versions.get_value() key=|version| version.clone() children=move |version: String| { let index = versions .get_value() .iter() .position(|v| v == &version) .unwrap_or_default(); view! { <DropdownMenuItem on:click=move |_| { selected_version.set(index) }> "v"{version.clone()}" " <Show when=move || { version == versions.get_value()[selected_version.get()] }>{icon!(icondata::LuCheck, class="ml-auto")}</Show> </DropdownMenuItem> } } /> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[component] fn SearchForm() -> impl IntoView { view! { <form> <SidebarGroup class="py-0"> <SidebarGroupContent class="relative"> <Label label_for="search" class="sr-only"> "Search" </Label> <InputGroup> <Input id="search" placeholder="Search the docs..." class="pl-8" /> <InputGroupAddon> {icon!( icondata::LuSearch, class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" )} </InputGroupAddon> </InputGroup> </SidebarGroupContent> </SidebarGroup> </form> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, items: Vec<NavSubItem>, } #[component] fn AppSidebar() -> impl IntoView { let versions = StoredValue::new(vec![ "1.0.1".to_string(), "1.1.0-alpha".to_string(), "2.0.0-beta1".to_string(), ]); let nav_main = StoredValue::new(vec![ NavItem { title: "Getting Started".to_string(), items: vec![ NavSubItem { title: "Installation".to_string(), url: "url".to_string(), }, NavSubItem { title: "Project Structure".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Build Your Application".to_string(), items: vec![ NavSubItem { title: "Routing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Data Fetching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Rendering".to_string(), url: "url".to_string(), }, NavSubItem { title: "Caching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Styling".to_string(), url: "url".to_string(), }, NavSubItem { title: "Optimizing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Configuring".to_string(), url: "url".to_string(), }, NavSubItem { title: "Testing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Authentication".to_string(), url: "url".to_string(), }, NavSubItem { title: "Deploying".to_string(), url: "url".to_string(), }, NavSubItem { title: "Upgrading".to_string(), url: "url".to_string(), }, NavSubItem { title: "Examples".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "API Reference".to_string(), items: vec![ NavSubItem { title: "Components".to_string(), url: "url".to_string(), }, NavSubItem { title: "File Conventions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Functions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Leptos Options".to_string(), url: "url".to_string(), }, NavSubItem { title: "CLI".to_string(), url: "url".to_string(), }, NavSubItem { title: "Edge Runtime".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Architecture".to_string(), items: vec![ NavSubItem { title: "Accessibility".to_string(), url: "url".to_string(), }, NavSubItem { title: "Fast Refresh".to_string(), url: "url".to_string(), }, NavSubItem { title: "Cargo Leptos".to_string(), url: "url".to_string(), }, NavSubItem { title: "Supported Browsers".to_string(), url: "url".to_string(), }, NavSubItem { title: "Wasm Bindgen".to_string(), url: "url".to_string(), }, ], }, ]); view! { <Sidebar> <SidebarHeader> <VersionSwitcher versions /> <SearchForm /> </SidebarHeader> <SidebarContent> <For each=move || nav_main.get_value() key=|item| item.title.clone() let(item)> <SidebarGroup> <SidebarGroupLabel>{item.title}</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(item) > <SidebarMenuItem> <SidebarMenuButton> <Link href={item.url}>{item.title}</Link> </SidebarMenuButton> </SidebarMenuItem> </For> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> </For> </SidebarContent> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarSectionsExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 border-b px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link>"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 rounded-xl md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Collapsible Sections
A sidebar with collapsible sections
use leptos::prelude::*; use singlestage::*; #[component] fn VersionSwitcher(versions: StoredValue<Vec<String>>) -> impl IntoView { let selected_version = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> {icon!(icondata::LuGalleryVerticalEnd, class="size-4")} </div> <div class="flex flex-col gap-0.5 leading-none"> <span class="font-medium">"Documentation"</span> <span> "v" {move || { versions.get_value()[selected_version.get()].to_owned() }} </span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58" align="center" side="bottom"> <For each=move || versions.get_value() key=|version| version.clone() children=move |version: String| { let index = versions .get_value() .iter() .position(|v| v == &version) .unwrap_or_default(); view! { <DropdownMenuItem on:click=move |_| { selected_version.set(index) }> "v"{version.clone()}" " <Show when=move || { version == versions.get_value()[selected_version.get()] }>{icon!(icondata::LuCheck, class="ml-auto")}</Show> </DropdownMenuItem> } } /> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[component] fn SearchForm() -> impl IntoView { view! { <form> <SidebarGroup class="py-0"> <SidebarGroupContent class="relative"> <Label label_for="search" class="sr-only"> "Search" </Label> <InputGroup> <Input id="search" placeholder="Search the docs..." class="pl-8" /> <InputGroupAddon> {icon!( icondata::LuSearch, class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" )} </InputGroupAddon> </InputGroup> </SidebarGroupContent> </SidebarGroup> </form> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, items: Vec<NavSubItem>, } #[component] fn AppSidebar() -> impl IntoView { let versions = StoredValue::new(vec![ "1.0.1".to_string(), "1.1.0-alpha".to_string(), "2.0.0-beta1".to_string(), ]); let nav_main = StoredValue::new(vec![ NavItem { title: "Getting Started".to_string(), items: vec![ NavSubItem { title: "Installation".to_string(), url: "url".to_string(), }, NavSubItem { title: "Project Structure".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Build Your Application".to_string(), items: vec![ NavSubItem { title: "Routing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Data Fetching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Rendering".to_string(), url: "url".to_string(), }, NavSubItem { title: "Caching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Styling".to_string(), url: "url".to_string(), }, NavSubItem { title: "Optimizing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Configuring".to_string(), url: "url".to_string(), }, NavSubItem { title: "Testing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Authentication".to_string(), url: "url".to_string(), }, NavSubItem { title: "Deploying".to_string(), url: "url".to_string(), }, NavSubItem { title: "Upgrading".to_string(), url: "url".to_string(), }, NavSubItem { title: "Examples".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "API Reference".to_string(), items: vec![ NavSubItem { title: "Components".to_string(), url: "url".to_string(), }, NavSubItem { title: "File Conventions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Functions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Leptos Options".to_string(), url: "url".to_string(), }, NavSubItem { title: "CLI".to_string(), url: "url".to_string(), }, NavSubItem { title: "Edge Runtime".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Architecture".to_string(), items: vec![ NavSubItem { title: "Accessibility".to_string(), url: "url".to_string(), }, NavSubItem { title: "Fast Refresh".to_string(), url: "url".to_string(), }, NavSubItem { title: "Cargo Leptos".to_string(), url: "url".to_string(), }, NavSubItem { title: "Supported Browsers".to_string(), url: "url".to_string(), }, NavSubItem { title: "Wasm Bindgen".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Community".to_string(), items: vec![NavSubItem { title: "Contribution Guide".to_string(), url: "url".to_string(), }], }, ]); view! { <Sidebar> <SidebarHeader> <VersionSwitcher versions /> <SearchForm /> </SidebarHeader> <SidebarContent> <For each=move || nav_main.get_value() key=|item| item.title.clone() let(item)> <Collapsible open=true title=item.title.clone()> <SidebarGroup> <CollapsibleTrigger> <SidebarMenuButton> <Button class="aria-expanded:[&_svg:last-child]:rotate-90"> {item.title} " " {icon!( icondata::LuChevronRight, class="ml-auto transition-transform" )} </Button> </SidebarMenuButton> </CollapsibleTrigger> <CollapsibleContent> <SidebarGroupContent> <SidebarMenu> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(item) > <SidebarMenuItem> <SidebarMenuButton> <Link href={item.url}>{item.title}</Link> </SidebarMenuButton> </SidebarMenuItem> </For> </SidebarMenu> </SidebarGroupContent> </CollapsibleContent> </SidebarGroup> </Collapsible> </For> </SidebarContent> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarCollapsibleSectionsExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 border-b px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link>"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Submenus
A sidebar with submenus
use leptos::prelude::*; use singlestage::*; #[component] fn VersionSwitcher(versions: StoredValue<Vec<String>>) -> impl IntoView { let selected_version = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> {icon!(icondata::LuGalleryVerticalEnd, class="size-4")} </div> <div class="flex flex-col gap-0.5 leading-none"> <span class="font-medium">"Documentation"</span> <span> "v" {move || { versions.get_value()[selected_version.get()].to_owned() }} </span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58" align="center" side="bottom"> <For each=move || versions.get_value() key=|version| version.clone() children=move |version: String| { let index = versions .get_value() .iter() .position(|v| v == &version) .unwrap_or_default(); view! { <DropdownMenuItem on:click=move |_| { selected_version.set(index) }> "v"{version.clone()}" " <Show when=move || { version == versions.get_value()[selected_version.get()] }>{icon!(icondata::LuCheck, class="ml-auto")}</Show> </DropdownMenuItem> } } /> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, items: Vec<NavSubItem>, } #[component] fn AppSidebar() -> impl IntoView { let versions = StoredValue::new(vec![ "1.0.1".to_string(), "1.1.0-alpha".to_string(), "2.0.0-beta1".to_string(), ]); let nav_main = StoredValue::new(vec![ NavItem { title: "Getting Started".to_string(), items: vec![ NavSubItem { title: "Installation".to_string(), url: "url".to_string(), }, NavSubItem { title: "Project Structure".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Build Your Application".to_string(), items: vec![ NavSubItem { title: "Routing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Data Fetching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Rendering".to_string(), url: "url".to_string(), }, NavSubItem { title: "Caching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Styling".to_string(), url: "url".to_string(), }, NavSubItem { title: "Optimizing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Configuring".to_string(), url: "url".to_string(), }, NavSubItem { title: "Testing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Authentication".to_string(), url: "url".to_string(), }, NavSubItem { title: "Deploying".to_string(), url: "url".to_string(), }, NavSubItem { title: "Upgrading".to_string(), url: "url".to_string(), }, NavSubItem { title: "Examples".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "API Reference".to_string(), items: vec![ NavSubItem { title: "Components".to_string(), url: "url".to_string(), }, NavSubItem { title: "File Conventions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Functions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Leptos Options".to_string(), url: "url".to_string(), }, NavSubItem { title: "CLI".to_string(), url: "url".to_string(), }, NavSubItem { title: "Edge Runtime".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Architecture".to_string(), items: vec![ NavSubItem { title: "Accessibility".to_string(), url: "url".to_string(), }, NavSubItem { title: "Fast Refresh".to_string(), url: "url".to_string(), }, NavSubItem { title: "Cargo Leptos".to_string(), url: "url".to_string(), }, NavSubItem { title: "Supported Browsers".to_string(), url: "url".to_string(), }, NavSubItem { title: "Wasm Bindgen".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Community".to_string(), items: vec![NavSubItem { title: "Contribution Guide".to_string(), url: "url".to_string(), }], }, ]); view! { <Sidebar> <SidebarHeader> <VersionSwitcher versions /> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarMenu> <For each=move || nav_main.get_value() key=|item| item.title.clone() let(item) > <SidebarMenuItem> <SidebarMenuButton> <Button>{item.title}</Button> </SidebarMenuButton> </SidebarMenuItem> <SidebarMenuSub> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(sub_item) > <SidebarMenuSubItem> <SidebarMenuSubButton> <Link href={sub_item.url}> <span>{sub_item.title}</span> </Link> </SidebarMenuSubButton> </SidebarMenuSubItem> </For> </SidebarMenuSub> </For> </SidebarMenu> </SidebarGroup> </SidebarContent> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarSubmenusExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 border-b px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link>"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Floating Submenus
A floating sidebar with submenus
use leptos::prelude::*; use singlestage::*; #[component] fn VersionSwitcher(versions: StoredValue<Vec<String>>) -> impl IntoView { let selected_version = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> {icon!(icondata::LuGalleryVerticalEnd, class="size-4")} </div> <div class="flex flex-col gap-0.5 leading-none"> <span class="font-medium">"Documentation"</span> <span> "v" {move || { versions.get_value()[selected_version.get()].to_owned() }} </span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58" align="center" side="bottom"> <For each=move || versions.get_value() key=|version| version.clone() children=move |version: String| { let index = versions .get_value() .iter() .position(|v| v == &version) .unwrap_or_default(); view! { <DropdownMenuItem on:click=move |_| { selected_version.set(index) }> "v"{version.clone()}" " <Show when=move || { version == versions.get_value()[selected_version.get()] }>{icon!(icondata::LuCheck, class="ml-auto")}</Show> </DropdownMenuItem> } } /> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, items: Vec<NavSubItem>, } #[component] fn AppSidebar() -> impl IntoView { let versions = StoredValue::new(vec![ "1.0.1".to_string(), "1.1.0-alpha".to_string(), "2.0.0-beta1".to_string(), ]); let nav_main = StoredValue::new(vec![ NavItem { title: "Getting Started".to_string(), items: vec![ NavSubItem { title: "Installation".to_string(), url: "url".to_string(), }, NavSubItem { title: "Project Structure".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Build Your Application".to_string(), items: vec![ NavSubItem { title: "Routing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Data Fetching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Rendering".to_string(), url: "url".to_string(), }, NavSubItem { title: "Caching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Styling".to_string(), url: "url".to_string(), }, NavSubItem { title: "Optimizing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Configuring".to_string(), url: "url".to_string(), }, NavSubItem { title: "Testing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Authentication".to_string(), url: "url".to_string(), }, NavSubItem { title: "Deploying".to_string(), url: "url".to_string(), }, NavSubItem { title: "Upgrading".to_string(), url: "url".to_string(), }, NavSubItem { title: "Examples".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "API Reference".to_string(), items: vec![ NavSubItem { title: "Components".to_string(), url: "url".to_string(), }, NavSubItem { title: "File Conventions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Functions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Leptos Options".to_string(), url: "url".to_string(), }, NavSubItem { title: "CLI".to_string(), url: "url".to_string(), }, NavSubItem { title: "Edge Runtime".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Architecture".to_string(), items: vec![ NavSubItem { title: "Accessibility".to_string(), url: "url".to_string(), }, NavSubItem { title: "Fast Refresh".to_string(), url: "url".to_string(), }, NavSubItem { title: "Cargo Leptos".to_string(), url: "url".to_string(), }, NavSubItem { title: "Supported Browsers".to_string(), url: "url".to_string(), }, NavSubItem { title: "Wasm Bindgen".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Community".to_string(), items: vec![NavSubItem { title: "Contribution Guide".to_string(), url: "url".to_string(), }], }, ]); view! { <Sidebar variant="floating"> <SidebarHeader> <VersionSwitcher versions /> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarMenu class="gap-2"> <For each=move || nav_main.get_value() key=|item| item.title.clone() let(item) > <SidebarMenuItem> <SidebarMenuButton> <Button class="font-medium">{item.title}</Button> </SidebarMenuButton> </SidebarMenuItem> <SidebarMenuSub class="ml-0 border-l-0 px-1.5"> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(sub_item) > <SidebarMenuSubItem> <SidebarMenuSubButton> <Link href={sub_item.url}> <span>{sub_item.title}</span> </Link> </SidebarMenuSubButton> </SidebarMenuSubItem> </For> </SidebarMenuSub> </For> </SidebarMenu> </SidebarGroup> </SidebarContent> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarFloatingSubmenusExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link>"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Collapsible Submenus
A floating sidebar with collapsible submenus
use leptos::prelude::*; use singlestage::*; #[component] fn VersionSwitcher(versions: StoredValue<Vec<String>>) -> impl IntoView { let selected_version = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> {icon!(icondata::LuGalleryVerticalEnd, class="size-4")} </div> <div class="flex flex-col gap-0.5 leading-none"> <span class="font-medium">"Documentation"</span> <span> "v" {move || { versions.get_value()[selected_version.get()].to_owned() }} </span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58" align="center" side="bottom"> <For each=move || versions.get_value() key=|version| version.clone() children=move |version: String| { let index = versions .get_value() .iter() .position(|v| v == &version) .unwrap_or_default(); view! { <DropdownMenuItem on:click=move |_| { selected_version.set(index) }> "v"{version.clone()}" " <Show when=move || { version == versions.get_value()[selected_version.get()] }>{icon!(icondata::LuCheck, class="ml-auto")}</Show> </DropdownMenuItem> } } /> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, items: Vec<NavSubItem>, open: bool, } #[component] fn AppSidebar() -> impl IntoView { let versions = StoredValue::new(vec![ "1.0.1".to_string(), "1.1.0-alpha".to_string(), "2.0.0-beta1".to_string(), ]); let nav_main = StoredValue::new(vec![ NavItem { title: "Getting Started".to_string(), open: false, items: vec![ NavSubItem { title: "Installation".to_string(), url: "url".to_string(), }, NavSubItem { title: "Project Structure".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Build Your Application".to_string(), open: true, items: vec![ NavSubItem { title: "Routing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Data Fetching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Rendering".to_string(), url: "url".to_string(), }, NavSubItem { title: "Caching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Styling".to_string(), url: "url".to_string(), }, NavSubItem { title: "Optimizing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Configuring".to_string(), url: "url".to_string(), }, NavSubItem { title: "Testing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Authentication".to_string(), url: "url".to_string(), }, NavSubItem { title: "Deploying".to_string(), url: "url".to_string(), }, NavSubItem { title: "Upgrading".to_string(), url: "url".to_string(), }, NavSubItem { title: "Examples".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "API Reference".to_string(), open: false, items: vec![ NavSubItem { title: "Components".to_string(), url: "url".to_string(), }, NavSubItem { title: "File Conventions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Functions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Leptos Options".to_string(), url: "url".to_string(), }, NavSubItem { title: "CLI".to_string(), url: "url".to_string(), }, NavSubItem { title: "Edge Runtime".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Architecture".to_string(), open: false, items: vec![ NavSubItem { title: "Accessibility".to_string(), url: "url".to_string(), }, NavSubItem { title: "Fast Refresh".to_string(), url: "url".to_string(), }, NavSubItem { title: "Cargo Leptos".to_string(), url: "url".to_string(), }, NavSubItem { title: "Supported Browsers".to_string(), url: "url".to_string(), }, NavSubItem { title: "Wasm Bindgen".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Community".to_string(), open: false, items: vec![NavSubItem { title: "Contribution Guide".to_string(), url: "url".to_string(), }], }, ]); view! { <Sidebar> <SidebarHeader> <VersionSwitcher versions /> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarMenu> <For each=move || nav_main.get_value() key=|item| item.title.clone() children=|item: NavItem| { let open = RwSignal::new(item.open); view! { <Collapsible open> <CollapsibleTrigger> <SidebarMenuItem> <SidebarMenuButton> <Button> {item.title} " " <Show when=move || { open.get() } fallback=move || { view! { {icon!(icondata::LuPlus, class="ml-auto")} } } > {icon!(icondata::LuMinus, class="ml-auto")} </Show> </Button> </SidebarMenuButton> </SidebarMenuItem> </CollapsibleTrigger> <CollapsibleContent> <SidebarMenuSub> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(sub_item) > <SidebarMenuSubItem> <SidebarMenuSubButton> <Link href={sub_item.url}> <span>{sub_item.title}</span> </Link> </SidebarMenuSubButton> </SidebarMenuSubItem> </For> </SidebarMenuSub> </CollapsibleContent> </Collapsible> } } /> </SidebarMenu> </SidebarGroup> </SidebarContent> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarCollapsibleSubmenusExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 border-b px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link>"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Submenu Dropdown
A sidebar with submenus as dropdowns
use leptos::prelude::*; use singlestage::*; #[component] fn VersionSwitcher(versions: StoredValue<Vec<String>>) -> impl IntoView { let selected_version = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> {icon!(icondata::LuGalleryVerticalEnd, class="size-4")} </div> <div class="flex flex-col gap-0.5 leading-none"> <span class="font-medium">"Documentation"</span> <span> "v" {move || { versions.get_value()[selected_version.get()].to_owned() }} </span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58" align="center" side="bottom"> <For each=move || versions.get_value() key=|version| version.clone() children=move |version: String| { let index = versions .get_value() .iter() .position(|v| v == &version) .unwrap_or_default(); view! { <DropdownMenuItem on:click=move |_| { selected_version.set(index) }> "v"{version.clone()}" " <Show when=move || { version == versions.get_value()[selected_version.get()] }>{icon!(icondata::LuCheck, class="ml-auto")}</Show> </DropdownMenuItem> } } /> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[component] fn SidebarOptInForm() -> impl IntoView { view! { <Card class="gap-2 py-4 shadow-none"> <CardHeader class="px-4"> <CardTitle class="text-sm">"Subscribe to our newsletter"</CardTitle> <CardDescription> "Opt-in to receive updates and news about the sidebar." </CardDescription> </CardHeader> <CardContent class="px-4"> <form> <div class="grid gap-2.5"> <Input input_type="email" placeholder="Email" /> <Button class="w-full bg-sidebar-primary text-sidebar-primary-foreground shadow-none" size="sm" > "Subscribe" </Button> </div> </form> </CardContent> </Card> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, items: Vec<NavSubItem>, } #[component] fn AppSidebar() -> impl IntoView { let versions = StoredValue::new(vec![ "1.0.1".to_string(), "1.1.0-alpha".to_string(), "2.0.0-beta1".to_string(), ]); let nav_main = StoredValue::new(vec![ NavItem { title: "Getting Started".to_string(), items: vec![ NavSubItem { title: "Installation".to_string(), url: "url".to_string(), }, NavSubItem { title: "Project Structure".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Build Your Application".to_string(), items: vec![ NavSubItem { title: "Routing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Data Fetching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Rendering".to_string(), url: "url".to_string(), }, NavSubItem { title: "Caching".to_string(), url: "url".to_string(), }, NavSubItem { title: "Styling".to_string(), url: "url".to_string(), }, NavSubItem { title: "Optimizing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Configuring".to_string(), url: "url".to_string(), }, NavSubItem { title: "Testing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Authentication".to_string(), url: "url".to_string(), }, NavSubItem { title: "Deploying".to_string(), url: "url".to_string(), }, NavSubItem { title: "Upgrading".to_string(), url: "url".to_string(), }, NavSubItem { title: "Examples".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "API Reference".to_string(), items: vec![ NavSubItem { title: "Components".to_string(), url: "url".to_string(), }, NavSubItem { title: "File Conventions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Functions".to_string(), url: "url".to_string(), }, NavSubItem { title: "Leptos Options".to_string(), url: "url".to_string(), }, NavSubItem { title: "CLI".to_string(), url: "url".to_string(), }, NavSubItem { title: "Edge Runtime".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Architecture".to_string(), items: vec![ NavSubItem { title: "Accessibility".to_string(), url: "url".to_string(), }, NavSubItem { title: "Fast Refresh".to_string(), url: "url".to_string(), }, NavSubItem { title: "Cargo Leptos".to_string(), url: "url".to_string(), }, NavSubItem { title: "Supported Browsers".to_string(), url: "url".to_string(), }, NavSubItem { title: "Wasm Bindgen".to_string(), url: "url".to_string(), }, ], }, ]); view! { <Sidebar> <SidebarHeader> <VersionSwitcher versions /> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarMenu> <For each=move || nav_main.get_value() key=|item| item.title.clone() let(item) > <DropdownMenu> <SidebarMenuItem> <DropdownMenuTrigger> <SidebarMenuButton> <Button> {item.title} " " {icon!(icondata::FiMoreHorizontal, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent align="start" side="right"> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(item) > <DropdownMenuItem as_child=true> <Link href={item.url}>{item.title}</Link> </DropdownMenuItem> </For> </DropdownMenuContent> </SidebarMenuItem> </DropdownMenu> </For> </SidebarMenu> </SidebarGroup> </SidebarContent> <SidebarFooter> <div class="p-1"> <SidebarOptInForm /> </div> </SidebarFooter> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarSubmenuDropdownExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 border-b px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link>"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Icons
A sidebar that collapses to icons
use leptos::prelude::*; use singlestage::*; #[derive(Clone, PartialEq)] struct Team { name: String, logo: icondata::Icon, plan: String, } #[component] fn TeamSwitcher(teams: StoredValue<Vec<Team>>) -> impl IntoView { let active_team = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-(--sidebar-accent) aria-expanded:text-(--sidebar-accent-foreground)" > {move || { let active_team = &teams.get_value()[active_team.get()]; view! { <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-(--sidebar-primary) text-(--sidebar-primary-foreground)"> { let logo = active_team.logo; icon!(logo, class="size-4") } </div> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {active_team.name.to_owned()} </span> <span class="truncate text-xs"> {active_team.plan.to_owned()} </span> </div> } }} {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" side="bottom" align="center"> <DropdownMenuLabel class="text-xs text-muted-foreground"> "Teams" </DropdownMenuLabel> <For each=move || teams.get_value() key=|team| team.name.clone() children=move |team: Team| { let index = teams .get_value() .iter() .position(|t| t == &team) .unwrap_or_default(); view! { <DropdownMenuItem class="gap-2 p-2" on:click={move |_| { active_team.set(index) }} > <div class="flex size-6 items-center justify-center rounded-md border"> { let logo = team.logo; icon!(logo, class="size-3.5 shrink-0") } </div> {team.name} </DropdownMenuItem> } } /> <DropdownMenuSeparator /> <DropdownMenuItem class="gap-2 p-2"> <div class="flex size-6 items-center justify-center rounded-md border bg-transparent"> {icon!(icondata::LuPlus, class="size-4")} </div> <div class="font-medium text-muted-foreground">"Add team"</div> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, icon: icondata::Icon, is_active: bool, items: Vec<NavSubItem>, } #[component] fn NavMain(items: StoredValue<Vec<NavItem>>) -> impl IntoView { view! { <SidebarGroup> <SidebarGroupLabel>"Platform"</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> <For each=move || items.get_value() key=|item| item.title.clone() children=|item: NavItem| { let tooltip = item.title.clone(); view! { <Collapsible open={item.is_active}> <SidebarMenuItem> <CollapsibleTrigger> <SidebarMenuButton tooltip> <Button class="aria-expanded:[&>svg:last-child]:rotate-90"> { let icon = item.icon; icon!(icon) } <span>{item.title}</span> {icon!( icondata::LuChevronRight, class="ml-auto transition-transform duration-200" )} </Button> </SidebarMenuButton> </CollapsibleTrigger> <CollapsibleContent> <SidebarMenuSub> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(sub_item) > <SidebarMenuSubItem> <SidebarMenuSubButton> <Link href={sub_item.url}> <span>{sub_item.title}</span> </Link> </SidebarMenuSubButton> </SidebarMenuSubItem> </For> </SidebarMenuSub> </CollapsibleContent> </SidebarMenuItem> </Collapsible> } } /> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> } } #[derive(Clone)] struct Project { name: String, icon: icondata::Icon, url: String, } #[component] fn NavProjects(projects: StoredValue<Vec<Project>>) -> impl IntoView { let sidebar = expect_context::<SidebarContext>(); view! { <Show when=move || sidebar.open.get()> <SidebarGroup class="group-data-[collapsible=icon]:hidden"> <SidebarGroupLabel>"Projects"</SidebarGroupLabel> <SidebarMenu> <For each=move || projects.get_value() key=|project| project.name.clone() let(project) > <SidebarMenuItem> <SidebarMenuButton> <Link href={project .url}> { let icon = project.icon; icon!(icon) } <span>{project.name}</span> </Link> </SidebarMenuButton> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuAction show_on_hover=true> {icon!(icondata::FiMoreHorizontal)} <span class="sr-only">"More"</span> </SidebarMenuAction> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" align="end" side="bottom" > <DropdownMenuItem> {icon!(icondata::LuFolder, class="text-muted-foreground")} <span>"View Project"</span> </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuForward, class="text-muted-foreground")} <span>"Share Project"</span> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem> {icon!(icondata::LuTrash2, class="text-(muted-foreground)")} <span>"Delete Project"</span> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </For> <SidebarMenuItem> <SidebarMenuButton> <Button class="text-sidebar-foreground/70"> {icon!(icondata::FiMoreHorizontal)} <span>"More"</span> </Button> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarGroup> </Show> } } #[derive(Clone)] struct User { name: String, email: String, avatar: String, } #[component] fn NavUser(user: StoredValue<User>) -> impl IntoView { view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <Avatar class="h-8 w-8 rounded-lg"> <AvatarImage src={user.get_value().avatar} alt={user.get_value().name} /> <AvatarFallback class="rounded-lg">"SD"</AvatarFallback> </Avatar> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {user.get_value().name} </span> <span class="truncate text-xs">{user.get_value().email}</span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto size-4")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" align="center" side="top"> <DropdownMenuLabel class="p-0 font-normal"> <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <Avatar class="h-8 w-8 rounded-lg"> <AvatarImage src={user.get_value().avatar} alt={user.get_value().name} /> <AvatarFallback class="rounded-lg">"CN"</AvatarFallback> </Avatar> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {user.get_value().name} </span> <span class="truncate text-xs">{user.get_value().email}</span> </div> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> {icon!(icondata::LuSparkles)} "Upgrade to Pro" </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> {icon!(icondata::LuBadgeCheck)} "Account" </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuCreditCard)} "Billing" </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuBell)} "Notifications" </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuItem>{icon!(icondata::LuLogOut)} "Log out"</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[component] fn AppSidebar() -> impl IntoView { let user = StoredValue::new(User { name: "Sofia Davis".to_string(), email: "m@example.com".to_string(), avatar: "/avatar-1.png".to_string(), }); let teams = StoredValue::new(vec![ Team { name: "Acme Inc".to_string(), logo: icondata::LuGalleryVerticalEnd, plan: "Enterprise".to_string(), }, Team { name: "Acme Corp.".to_string(), logo: icondata::LuAudioWaveform, plan: "Startup".to_string(), }, Team { name: "Evil Corp.".to_string(), logo: icondata::LuCommand, plan: "Free".to_string(), }, ]); let items = StoredValue::new(vec![ NavItem { title: "Playground".to_string(), icon: icondata::LuSquareTerminal, is_active: true, items: vec![ NavSubItem { title: "History".to_string(), url: "url".to_string(), }, NavSubItem { title: "Starred".to_string(), url: "url".to_string(), }, NavSubItem { title: "Settings".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Models".to_string(), icon: icondata::LuBot, is_active: false, items: vec![ NavSubItem { title: "Genesis".to_string(), url: "url".to_string(), }, NavSubItem { title: "Explorer".to_string(), url: "url".to_string(), }, NavSubItem { title: "Quantum".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Documentation".to_string(), icon: icondata::LuBookOpen, is_active: false, items: vec![ NavSubItem { title: "Introduction".to_string(), url: "url".to_string(), }, NavSubItem { title: "Get Started".to_string(), url: "url".to_string(), }, NavSubItem { title: "Tutorials".to_string(), url: "url".to_string(), }, NavSubItem { title: "Changelog".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Settings".to_string(), icon: icondata::LuSettings2, is_active: false, items: vec![ NavSubItem { title: "General".to_string(), url: "url".to_string(), }, NavSubItem { title: "Team".to_string(), url: "url".to_string(), }, NavSubItem { title: "Billing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Limits".to_string(), url: "url".to_string(), }, ], }, ]); let projects = StoredValue::new(vec![ Project { name: "Design Engineering".to_string(), url: "url".to_string(), icon: icondata::LuFrame, }, Project { name: "Sales & Marketing".to_string(), url: "url".to_string(), icon: icondata::FiPieChart, }, Project { name: "Travel".to_string(), url: "url".to_string(), icon: icondata::LuMap, }, ]); view! { <Sidebar collapsible="icon"> <SidebarHeader> <TeamSwitcher teams /> </SidebarHeader> <SidebarContent> <NavMain items /> <NavProjects projects /> </SidebarContent> <SidebarFooter> <NavUser user /> </SidebarFooter> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarIconExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 border-b px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link href="#">"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Secondary Navigation
An inset sidebar with secondary navigation
use leptos::prelude::*; use singlestage::*; #[derive(Clone, PartialEq)] struct Team { name: String, logo: icondata::Icon, plan: String, } #[component] fn TeamSwitcher(teams: StoredValue<Vec<Team>>) -> impl IntoView { let active_team = RwSignal::new(0); view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-(--sidebar-accent) aria-expanded:text-(--sidebar-accent-foreground)" > {move || { let active_team = &teams.get_value()[active_team.get()]; view! { <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-(--sidebar-primary) text-(--sidebar-primary-foreground)"> { let logo = active_team.logo; icon!(logo, class="size-4") } </div> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {active_team.name.to_owned()} </span> <span class="truncate text-xs"> {active_team.plan.to_owned()} </span> </div> } }} {icon!(icondata::LuChevronsUpDown, class="ml-auto")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" side="bottom" align="center"> <DropdownMenuLabel class="text-xs text-muted-foreground"> "Teams" </DropdownMenuLabel> <For each=move || teams.get_value() key=|team| team.name.clone() children=move |team: Team| { let index = teams .get_value() .iter() .position(|t| t == &team) .unwrap_or_default(); view! { <DropdownMenuItem class="gap-2 p-2" on:click={move |_| { active_team.set(index) }} > <div class="flex size-6 items-center justify-center rounded-md border"> { let logo = team.logo; icon!(logo, class="size-3.5 shrink-0") } </div> {team.name} </DropdownMenuItem> } } /> <DropdownMenuSeparator /> <DropdownMenuItem class="gap-2 p-2"> <div class="flex size-6 items-center justify-center rounded-md border bg-transparent"> {icon!(icondata::LuPlus, class="size-4")} </div> <div class="font-medium text-muted-foreground">"Add team"</div> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[derive(Clone)] struct NavSubItem { title: String, url: String, } #[derive(Clone)] struct NavItem { title: String, icon: icondata::Icon, is_active: bool, items: Vec<NavSubItem>, } #[component] fn NavMain(items: StoredValue<Vec<NavItem>>) -> impl IntoView { view! { <SidebarGroup> <SidebarGroupLabel>"Platform"</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> <For each=move || items.get_value() key=|item| item.title.clone() children=|item: NavItem| { let tooltip = item.title.clone(); view! { <Collapsible open={item.is_active}> <SidebarMenuItem> <CollapsibleTrigger> <SidebarMenuButton tooltip> <Button class="aria-expanded:[&>svg:last-child]:rotate-90"> { let icon = item.icon; icon!(icon) } <span>{item.title}</span> {icon!( icondata::LuChevronRight, class="ml-auto transition-transform duration-200" )} </Button> </SidebarMenuButton> </CollapsibleTrigger> <CollapsibleContent> <SidebarMenuSub> <For each=move || item.items.to_owned() key=|item| item.title.clone() let(sub_item) > <SidebarMenuSubItem> <SidebarMenuSubButton> <Link href={sub_item.url}> <span>{sub_item.title}</span> </Link> </SidebarMenuSubButton> </SidebarMenuSubItem> </For> </SidebarMenuSub> </CollapsibleContent> </SidebarMenuItem> </Collapsible> } } /> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> } } #[derive(Clone)] struct Project { name: String, icon: icondata::Icon, url: String, } #[component] fn NavProjects(projects: StoredValue<Vec<Project>>) -> impl IntoView { let sidebar = expect_context::<SidebarContext>(); view! { <Show when=move || sidebar.open.get()> <SidebarGroup class="group-data-[collapsible=icon]:hidden"> <SidebarGroupLabel>"Projects"</SidebarGroupLabel> <SidebarMenu> <For each=move || projects.get_value() key=|project| project.name.clone() let(project) > <SidebarMenuItem> <SidebarMenuButton> <Link href={project .url}> { let icon = project.icon; icon!(icon) } <span>{project.name}</span> </Link> </SidebarMenuButton> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuAction show_on_hover=true> {icon!(icondata::FiMoreHorizontal)} <span class="sr-only">"More"</span> </SidebarMenuAction> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" align="end" side="bottom" > <DropdownMenuItem> {icon!(icondata::LuFolder, class="text-muted-foreground")} <span>"View Project"</span> </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuForward, class="text-muted-foreground")} <span>"Share Project"</span> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem> {icon!(icondata::LuTrash2, class="text-(muted-foreground)")} <span>"Delete Project"</span> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </For> <SidebarMenuItem> <SidebarMenuButton> <Button class="text-sidebar-foreground/70"> {icon!(icondata::FiMoreHorizontal)} <span>"More"</span> </Button> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarGroup> </Show> } } #[derive(Clone)] struct NavSecondary { title: String, url: String, icon: icondata::Icon, } #[component] fn NavSecondary(items: StoredValue<Vec<NavSecondary>>) -> impl IntoView { view! { <SidebarGroup class="mt-auto"> <SidebarGroupContent> <SidebarMenu> <For each=move || items.get_value() key=|item| item.title.clone() let(item)> <SidebarMenuItem> <SidebarMenuButton> <Link size="sm" href=item.url> { let icon = item.icon; icon!(icon) } <span>{item.title}</span> </Link> </SidebarMenuButton> </SidebarMenuItem> </For> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> } } #[derive(Clone)] struct User { name: String, email: String, avatar: String, } #[component] fn NavUser(user: StoredValue<User>) -> impl IntoView { view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <Avatar class="h-8 w-8 rounded-lg"> <AvatarImage src={user.get_value().avatar} alt={user.get_value().name} /> <AvatarFallback class="rounded-lg">"SD"</AvatarFallback> </Avatar> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {user.get_value().name} </span> <span class="truncate text-xs">{user.get_value().email}</span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto size-4")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent class="min-w-58 rounded-lg" align="center" side="top"> <DropdownMenuLabel class="p-0 font-normal"> <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <Avatar class="h-8 w-8 rounded-lg"> <AvatarImage src={user.get_value().avatar} alt={user.get_value().name} /> <AvatarFallback class="rounded-lg">"CN"</AvatarFallback> </Avatar> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {user.get_value().name} </span> <span class="truncate text-xs">{user.get_value().email}</span> </div> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> {icon!(icondata::LuSparkles)} "Upgrade to Pro" </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> {icon!(icondata::LuBadgeCheck)} "Account" </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuCreditCard)} "Billing" </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuBell)} "Notifications" </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuItem>{icon!(icondata::LuLogOut)} "Log out"</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[component] fn AppSidebar() -> impl IntoView { let user = StoredValue::new(User { name: "Sofia Davis".to_string(), email: "m@example.com".to_string(), avatar: "/avatar-1.png".to_string(), }); let teams = StoredValue::new(vec![ Team { name: "Acme Inc".to_string(), logo: icondata::LuGalleryVerticalEnd, plan: "Enterprise".to_string(), }, Team { name: "Acme Corp.".to_string(), logo: icondata::LuAudioWaveform, plan: "Startup".to_string(), }, Team { name: "Evil Corp.".to_string(), logo: icondata::LuCommand, plan: "Free".to_string(), }, ]); let items = StoredValue::new(vec![ NavItem { title: "Playground".to_string(), icon: icondata::LuSquareTerminal, is_active: true, items: vec![ NavSubItem { title: "History".to_string(), url: "url".to_string(), }, NavSubItem { title: "Starred".to_string(), url: "url".to_string(), }, NavSubItem { title: "Settings".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Models".to_string(), icon: icondata::LuBot, is_active: false, items: vec![ NavSubItem { title: "Genesis".to_string(), url: "url".to_string(), }, NavSubItem { title: "Explorer".to_string(), url: "url".to_string(), }, NavSubItem { title: "Quantum".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Documentation".to_string(), icon: icondata::LuBookOpen, is_active: false, items: vec![ NavSubItem { title: "Introduction".to_string(), url: "url".to_string(), }, NavSubItem { title: "Get Started".to_string(), url: "url".to_string(), }, NavSubItem { title: "Tutorials".to_string(), url: "url".to_string(), }, NavSubItem { title: "Changelog".to_string(), url: "url".to_string(), }, ], }, NavItem { title: "Settings".to_string(), icon: icondata::LuSettings2, is_active: false, items: vec![ NavSubItem { title: "General".to_string(), url: "url".to_string(), }, NavSubItem { title: "Team".to_string(), url: "url".to_string(), }, NavSubItem { title: "Billing".to_string(), url: "url".to_string(), }, NavSubItem { title: "Limits".to_string(), url: "url".to_string(), }, ], }, ]); let projects = StoredValue::new(vec![ Project { name: "Design Engineering".to_string(), url: "url".to_string(), icon: icondata::LuFrame, }, Project { name: "Sales & Marketing".to_string(), url: "url".to_string(), icon: icondata::FiPieChart, }, Project { name: "Travel".to_string(), url: "url".to_string(), icon: icondata::LuMap, }, ]); let secondary = StoredValue::new(vec![ NavSecondary { title: "Support".to_string(), url: "url".to_string(), icon: icondata::LuLifeBuoy, }, NavSecondary { title: "Feedback".to_string(), url: "url".to_string(), icon: icondata::LuSend, }, ]); view! { <Sidebar variant="inset"> <SidebarHeader> <TeamSwitcher teams /> </SidebarHeader> <SidebarContent> <NavMain items /> <NavProjects projects /> <NavSecondary items=secondary /> </SidebarContent> <SidebarFooter> <NavUser user /> </SidebarFooter> <SidebarRail /> </Sidebar> } } #[component] pub fn SidebarSecondaryExample() -> impl IntoView { let is_mobile = RwSignal::new(false); view! { <SidebarProvider is_mobile> <AppSidebar /> <SidebarInset> <header class="flex h-16 shrink-0 items-center gap-2 border-b px-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link href="#">"Build Your Application"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Data Fetching"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> <Skeleton class="aspect-video" /> </div> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> } }
Nested
Collapsible nested sidebars
use leptos::prelude::*; use singlestage::*; #[derive(Clone)] struct User { name: String, email: String, avatar: String, } #[component] fn NavUser(user: StoredValue<User>) -> impl IntoView { view! { <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> <SidebarMenuButton> <Button size="lg" class="aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground" > <Avatar class="h-8 w-8 rounded-lg"> <AvatarImage src={user.get_value().avatar} alt={user.get_value().name} /> <AvatarFallback class="rounded-lg">"SD"</AvatarFallback> </Avatar> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {user.get_value().name} </span> <span class="truncate text-xs">{user.get_value().email}</span> </div> {icon!(icondata::LuChevronsUpDown, class="ml-auto size-4")} </Button> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent align="end" side="right-top"> <DropdownMenuLabel class="p-0 font-normal"> <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <Avatar class="h-8 w-8 rounded-lg"> <AvatarImage src={user.get_value().avatar} alt={user.get_value().name} /> <AvatarFallback class="rounded-lg">"CN"</AvatarFallback> </Avatar> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium"> {user.get_value().name} </span> <span class="truncate text-xs">{user.get_value().email}</span> </div> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> {icon!(icondata::LuSparkles)} "Upgrade to Pro" </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> {icon!(icondata::LuBadgeCheck)} "Account" </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuCreditCard)} "Billing" </DropdownMenuItem> <DropdownMenuItem> {icon!(icondata::LuBell)} "Notifications" </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuItem>{icon!(icondata::LuLogOut)} "Log out"</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> } } #[derive(Clone)] struct NavItem { title: String, icon: icondata::Icon, } #[component] fn OuterSidebar( active: RwSignal<String>, items: StoredValue<Vec<NavItem>>, user: StoredValue<User>, ) -> impl IntoView { view! { <Sidebar collapsible="icon" default_open=false> <SidebarHeader> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton> <Link size="lg" href="url" class="md:h-8 md:p-0"> <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-(--sidebar-primary) text-(--sidebar-primary-foreground)"> {icon!(icondata::LuCommand, class="size-4")} </div> </Link> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarGroupContent class="px-1.5 md:px-0"> <SidebarMenu> <For each=move || items.get_value() key=|item| item.title.clone() children=move |item: NavItem| { let is_active = &item.title == &active.get(); view! { <SidebarMenuItem> <SidebarMenuButton tooltip=item.title> <Button class=format!( "{}", if is_active { "bg-(--sidebar-accent) text-(--sidebar-accent-foreground)" } else { "" }, )> { let icon = item.icon; icon!(icon) } </Button> </SidebarMenuButton> </SidebarMenuItem> } } /> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> </SidebarContent> <SidebarFooter> <NavUser user /> </SidebarFooter> </Sidebar> } } #[derive(Clone)] struct Mail { name: String, email: String, subject: String, date: String, teaser: String, } #[component] fn InnerSidebar(active: RwSignal<String>, mails: StoredValue<Vec<Mail>>) -> impl IntoView { view! { <Sidebar class="rounded-none!"> <SidebarHeader class="gap-3.5 border-b p-4"> <div class="flex w-full items-center justify-between"> <div class="text-base font-medium text-foreground">{move || active.get()}</div> <Label class="flex items-center gap-2 text-sm"> <span>"Unreads"</span> <Switch class="shadow-none" /> </Label> </div> <Input placeholder="Type to search..." /> </SidebarHeader> <SidebarContent> <SidebarGroup class="px-0"> <SidebarGroupContent> <For each=move || mails.get_value() key=|mail| mail.subject.clone() children=move |mail: Mail| { let initials = format!( "{}{}", &mail.name[0..1], &mail.name.split(' ').last().unwrap_or_default()[0..1], ); view! { <Item variant="default" size="xs"> <ItemMedia class="mb-auto"> <Avatar size="xs"> <AvatarFallback>{initials}</AvatarFallback> </Avatar> </ItemMedia> <ItemContent> <ItemTitle class="w-full"> <span class="text-xs">{mail.name}</span> <span class="ml-auto text-xs text-(--muted-foreground)"> {mail.date} </span> </ItemTitle> <div class="font-medium text-xs">{mail.subject}</div> <div class="line-clamp-2 text-xs whitespace-break-spaces pt-1"> {mail.teaser} </div> </ItemContent> </Item> <Separator /> } } /> </SidebarGroupContent> </SidebarGroup> </SidebarContent> </Sidebar> } } #[component] pub fn SidebarNestedExample() -> impl IntoView { let user = StoredValue::new(User { name: "Sofia Davis".to_string(), email: "m@example.com".to_string(), avatar: "/avatar-1.png".to_string(), }); let items = StoredValue::new(vec![ NavItem { title: "Inbox".to_string(), icon: icondata::LuInbox, }, NavItem { title: "Drafts".to_string(), icon: icondata::LuFile, }, NavItem { title: "Sent".to_string(), icon: icondata::LuSend, }, NavItem { title: "Junk".to_string(), icon: icondata::LuArchiveX, }, NavItem { title: "Trash".to_string(), icon: icondata::LuTrash2, }, ]); let mails = StoredValue::new(vec![ Mail { name: "William Smith".to_string(), email: "williamsmith@example.com".to_string(), subject: "Meeting Tomorrow".to_string(), date: "09:34 AM".to_string(), teaser: "Hi team, just a reminder about our meeting tomorrow at 10 AM.\nPlease come prepared with your project updates.".to_string(), }, Mail { name: "Alice Smith".to_string(), email: "alicesmith@example.com".to_string(), subject: "Re: Project Update".to_string(), date: "Yesterday".to_string(), teaser: "Thanks for the update. The progress looks great so far.\nLet's schedule a call to discuss the next steps.".to_string(), }, Mail { name: "Bob Johnson".to_string(), email: "bobjohnson@example.com".to_string(), subject: "Weekend Plans".to_string(), date: "2 days ago".to_string(), teaser: "Hey everyone! I'm thinking of organizing a team outing this weekend.\nWould you be interested in a hiking trip or a beach day?".to_string(), }, Mail { name: "Emily Davis".to_string(), email: "emilydavis@example.com".to_string(), subject: "Re: Question about Budget".to_string(), date: "2 days ago".to_string(), teaser: "I've reviewed the budget numbers you sent over.\nCan we set up a quick call to discuss some potential adjustments?".to_string(), }, Mail { name: "Michael Wilson".to_string(), email: "michaelwilson@example.com".to_string(), subject: "Important Announcement".to_string(), date: "1 week ago".to_string(), teaser: "Please join us for an all-hands meeting this Friday at 3 PM.\nWe have some exciting news to share about the company's future.".to_string(), }, Mail { name: "Sarah Brown".to_string(), email: "sarahbrown@example.com".to_string(), subject: "Re: Feedback on Proposal".to_string(), date: "1 week ago".to_string(), teaser: "Thank you for sending over the proposal. I've reviewed it and have some thoughts.\nCould we schedule a meeting to discuss my feedback in detail?".to_string(), }, Mail { name: "David Lee".to_string(), email: "davidlee@example.com".to_string(), subject: "New Project Idea".to_string(), date: "1 week ago".to_string(), teaser: "I've been brainstorming and came up with an interesting project concept.\nDo you have time this week to discuss its potential impact and feasibility?".to_string(), }, Mail { name: "Olivia Wilson".to_string(), email: "oliviawilson@example.com".to_string(), subject: "Vacation Plans".to_string(), date: "1 week ago".to_string(), teaser: "Just a heads up that I'll be taking a two-week vacation next month.\nI'll make sure all my projects are up to date before I leave.".to_string(), }, Mail { name: "James Martin".to_string(), email: "jamesmartin@example.com".to_string(), subject: "Re: Conference Registration".to_string(), date: "1 week ago".to_string(), teaser: "I've completed the registration for the upcoming tech conference.\nLet me know if you need any additional information from my end.".to_string(), }, Mail { name: "Sophia White".to_string(), email: "sophiawhite@example.com".to_string(), subject: "Team Dinner".to_string(), date: "1 week ago".to_string(), teaser: "To celebrate our recent project success, I'd like to organize a team dinner.\nAre you available next Friday evening? Please let me know your preferences.".to_string(), }, ]); let is_mobile = RwSignal::new(false); let active = RwSignal::new(items.get_value()[0].title.clone()); view! { <SidebarProvider is_mobile> <OuterSidebar active items user /> <SidebarProvider style="--sidebar-width: 300px"> <InnerSidebar active mails /> <SidebarInset> <header class="sticky top-0 flex shrink-0 items-center gap-2 border-b bg-background p-2"> <SidebarTrigger /> <Separator orientation="vertical" class="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <Show when=move || !is_mobile.get()> <BreadcrumbItem> <BreadcrumbLink> <Link>"All Inboxes"</Link> </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> </Show> <BreadcrumbItem> <BreadcrumbPage>"Inbox"</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <Skeleton class="h-24" /> <Skeleton class="flex-1 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> </SidebarProvider> } }
Anatomy
Import all parts and piece them together.
use leptos::prelude::*; use singlestage::Sidebar::*; #[component] pub fn SidebarAnatomy() -> impl IntoView { view! { <SidebarProvider> <Sidebar> <SidebarHeader /> <SidebarContent> <SidebarGroup> <SidebarGroupLabel /> <SidebarGroupContent> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton /> </SidebarMenuItem> <SidebarMenuItem> <Collapsible> <CollapsibleTrigger> <SidebarMenuButton /> </CollapsibleTrigger> <CollapsibleContent> <SidebarMenuSub> <SidebarMenuSubItem> <SidebarMenuSubButton /> </SidebarMenuSubItem> </SidebarMenuSub> </CollapsibleContent> </Collapsible> </SidebarMenuItem> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> </SidebarContent> <SidebarFooter /> <SidebarRail /> </Sidebar> <main> <SidebarTrigger /> </main> </SidebarProvider> } }
API Reference
SidebarContent
A scrollable wrapper displayed in the middle of the sidebar.
SidebarGroup
Creates a section in the content area of the sidebar that groups similar items.
SidebarGroupContent
Wraps the content of a sidebar group.
SidebarMenuButton
Wraps content displayed in the click area of a SidebarMenuItem.
SidebarMenuSub
Wraps a submenu within a SidebarMenu.
SidebarProvider
Takes in initial state and provides SidebarContext to its children.
| Name | Type | Default | Description |
|---|---|---|---|
| hidden | bool | false | Reactive signal coupled to the sidebar's hidden state. |
| side | String | "left" | Reactive signal coupled to which side of its container the sidebar renders on. |