Sidebar
App navigation that folds to icons: titled groups of links, sub-items under a parent, and the signed-in person at the foot. Expanded and collapsed, as in the Figma set, with an optional plan card.
Installation
npx @smicolon/fasla-ui@latest add sidebar
Run init once per project before adding components. See Installation.
Sidebar is built on Base UI, so it needs the @base-ui/react package, and it uses avatar and tooltip. add installs them with the component, and so does the shadcn CLI.
Preview
Collapse the sidebar from the button in its header, then point at an icon to see its label.
Expanded and collapsed
The sidebar expanded, with labels and section titles, and collapsed to a column of icons.
States
The four states of an item and a sub-item: default, hover, selected and focus.
ItemSub-item
DefaultHoverSelectedFocusItems and sub-items
Items with an icon and a notification dot, and sub-items that open and close under their parent.
Plan card
The optional plan card, shown above the profile at the foot of the sidebar.
Accessibility
- The sidebar is an
aside element, and SidebarContent is a nav element. Give the nav an aria-label when the page has another navigation landmark. selected sets aria-current="page" on the item's link, so a screen reader announces the current page.- The toggle in the header is named Collapse sidebar or Expand sidebar, in Arabic on an Arabic page, and reports its state with
aria-expanded. Set collapseLabel and expandLabel to change the names. - An item with sub-items is a button with
aria-expanded. Enter and the Space key open and close it. - When the sidebar is collapsed, each icon keeps its label as its accessible name. The tooltip opens on hover and on keyboard focus, and Escape closes it.
- Keyboard focus draws Select's ring: a
ring border with a 3px ring halo at 50%. In forced colours it draws a 2px Highlight outline instead. - The notification dot is named New, or جديد on an Arabic page. Set
badgeLabel to say more, such as how many updates there are. - The width and the sub-items ease open and closed over 180ms. When reduced motion is on, they change at once.
- The sidebar takes the page's direction. On an Arabic page it sits on the right and every inset mirrors, and the toggle and chevrons flip. Direction needs no prop.
Props
Sidebar
| Prop | Type | Default | Description |
|---|
collapsed | boolean | — | Collapses it to icons only, when you control it. |
defaultCollapsed | boolean | false | Starts collapsed, when you do not control it. |
onCollapsedChange | (collapsed) => void | — | Called with the new state when it collapses or expands. |
children | ReactNode | — | The header, content, plan card and footer, in that order. The plan card is hidden while the sidebar is collapsed. |
className | string | — | Extra classes for the aside element. |
SidebarHeader
| Prop | Type | Default | Description |
|---|
logo | ReactNode | — | The full logo, shown while expanded. In Figma the English logo is 90px wide and the Arabic logo 28px tall. |
logoIcon | ReactNode | — | The logo's icon alone, shown while collapsed. Figma sets it at 28px. |
collapseLabel | string | "Collapse sidebar" | The toggle's accessible name while expanded. |
expandLabel | string | "Expand sidebar" | The toggle's accessible name while collapsed. |
className | string | — | Extra classes for the header. |
SidebarContent
| Prop | Type | Default | Description |
|---|
aria-label | string | — | Names the navigation landmark. |
children | ReactNode | — | The SidebarGroup elements. |
className | string | — | Extra classes for the nav element. |
SidebarGroup
| Prop | Type | Default | Description |
|---|
label | ReactNode | — | The section title. When collapsed, it becomes a divider and stays the group's accessible name. |
children | ReactNode | — | The SidebarItem elements. |
className | string | — | Extra classes for the group. |
SidebarItem
| Prop | Type | Default | Description |
|---|
label | ReactNode | — | The item's text. When collapsed, it is the tooltip and the accessible name. |
icon | ReactNode | — | A 16px icon before the label. When collapsed, only the icon shows, so give every item one. |
selected | boolean | false | Marks the current page. Set it on one item only. |
badge | boolean | false | Shows the notification dot at the end of the row. |
badgeLabel | string | "New" | The dot's name for screen readers. |
href | string | — | Renders the item as a link to this address. |
render | ReactElement | — | Renders this element in place of the link, such as a router's Link. |
open | boolean | — | Shows or hides its sub-items, when you control it. |
defaultOpen | boolean | — | Shows its sub-items on first render. By default it is open when one of them is selected. |
onOpenChange | (open) => void | — | Called when its sub-items open or close. |
onClick | (event) => void | — | Called when the item is pressed. |
children | ReactNode | — | The SidebarSubItem elements, one level deep. With any, the item opens and closes them instead of linking. |
className | string | — | Extra classes for the item's link or button. |
SidebarSubItem
| Prop | Type | Default | Description |
|---|
label | ReactNode | — | The sub-item's text. |
selected | boolean | false | Marks the current page. Its parent opens to show it, and shows as selected when the sidebar is collapsed. |
badge | boolean | false | Shows the notification dot at the end of the row. |
badgeLabel | string | "New" | The dot's name for screen readers. |
href | string | — | Renders the sub-item as a link to this address. |
render | ReactElement | — | Renders this element in place of the link, such as a router's Link. |
onClick | (event) => void | — | Called when the sub-item is pressed. |
className | string | — | Extra classes for the sub-item's link or button. |
SidebarPlanCard
| Prop | Type | Default | Description |
|---|
title | ReactNode | — | The plan's name. |
description | ReactNode | — | A line under the name. |
footer | ReactNode | — | The line under the divider. |
footerIcon | ReactNode | — | A 16px icon before footer. |
className | string | — | Extra classes for the card. |
SidebarFooter
| Prop | Type | Default | Description |
|---|
children | ReactNode | — | The SidebarProfile. |
className | string | — | Extra classes for the footer. |
SidebarProfile
| Prop | Type | Default | Description |
|---|
name | string | — | The person's name. When collapsed, it is the button's accessible name. |
description | ReactNode | — | A line under the name, such as the workspace. |
avatar | AvatarProps | — | The Avatar's props, such as src. It shows at 32px. |
onClick | (event) => void | — | Called when the profile is pressed, for example to open an account menu. |
className | string | — | Extra classes for the profile button. |
Usage
An app sidebar
import { LayoutDashboardIcon, SettingsIcon, ShoppingCartIcon } from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarHeader,
SidebarItem,
SidebarProfile,
SidebarSubItem,
} from "@/components/ui/sidebar"
export function AppSidebar() {
return (
<Sidebar>
<SidebarHeader
logo={<img src="/logo.svg" alt="Fasla" className="h-7" />}
logoIcon={<img src="/logo-icon.svg" alt="Fasla" className="size-7" />}
/>
<SidebarContent aria-label="Main">
<SidebarGroup label="Store">
<SidebarItem icon={<LayoutDashboardIcon strokeWidth={1.5} />} label="Dashboard" href="/dashboard" />
<SidebarItem icon={<ShoppingCartIcon strokeWidth={1.5} />} label="Orders">
<SidebarSubItem label="All orders" href="/orders" selected />
<SidebarSubItem label="Returns" href="/orders/returns" />
</SidebarItem>
</SidebarGroup>
<SidebarGroup label="Account">
<SidebarItem icon={<SettingsIcon strokeWidth={1.5} />} label="Settings" href="/settings" />
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarProfile name="Nadia Rahman" description="Fasla" avatar={{ src: "/avatar.png" }} />
</SidebarFooter>
</Sidebar>
)
}
Control the collapsed state
"use client"
import { useState } from "react"
import { LayoutDashboardIcon } from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarHeader,
SidebarItem,
} from "@/components/ui/sidebar"
export function CollapsibleSidebar() {
// Keep it in your layout state, or in a cookie, to remember it between pages.
const [collapsed, setCollapsed] = useState(false)
return (
<Sidebar collapsed={collapsed} onCollapsedChange={setCollapsed}>
<SidebarHeader logo={<img src="/logo.svg" alt="Fasla" className="h-7" />} />
<SidebarContent>
<SidebarGroup label="Store">
<SidebarItem icon={<LayoutDashboardIcon strokeWidth={1.5} />} label="Dashboard" href="/dashboard" />
</SidebarGroup>
</SidebarContent>
</Sidebar>
)
}
Link with your router
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { LayoutDashboardIcon, ShoppingCartIcon } from "lucide-react"
import { Sidebar, SidebarContent, SidebarGroup, SidebarItem } from "@/components/ui/sidebar"
const links = [
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboardIcon },
{ href: "/orders", label: "Orders", icon: ShoppingCartIcon },
]
export function RouterSidebar() {
const pathname = usePathname()
return (
<Sidebar>
<SidebarContent>
<SidebarGroup label="Store">
{links.map(({ href, label, icon: Icon }) => (
<SidebarItem
key={href}
label={label}
icon={<Icon strokeWidth={1.5} />}
render={<Link href={href} />}
selected={pathname === href}
/>
))}
</SidebarGroup>
</SidebarContent>
</Sidebar>
)
}
Add the plan card
import { CalendarIcon, LayoutDashboardIcon } from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarItem,
SidebarPlanCard,
SidebarProfile,
} from "@/components/ui/sidebar"
export function SidebarWithPlan() {
return (
<Sidebar>
<SidebarContent>
<SidebarGroup label="Store">
<SidebarItem icon={<LayoutDashboardIcon strokeWidth={1.5} />} label="Dashboard" href="/dashboard" />
</SidebarGroup>
</SidebarContent>
<SidebarPlanCard
title="Pro plan"
description="3 team members"
footer="Next renewal: 10 Mar"
footerIcon={<CalendarIcon strokeWidth={1.5} />}
/>
<SidebarFooter>
<SidebarProfile name="Nadia Rahman" description="Fasla" />
</SidebarFooter>
</Sidebar>
)
}