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

bash
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.

Expanded
Collapsed

States

The four states of an item and a sub-item: default, hover, selected and focus.

Default
Hover
Selected
Focus

Items 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

PropTypeDefaultDescription
collapsedboolean—Collapses it to icons only, when you control it.
defaultCollapsedbooleanfalseStarts collapsed, when you do not control it.
onCollapsedChange(collapsed) => void—Called with the new state when it collapses or expands.
childrenReactNode—The header, content, plan card and footer, in that order. The plan card is hidden while the sidebar is collapsed.
classNamestring—Extra classes for the aside element.

SidebarHeader

PropTypeDefaultDescription
logoReactNode—The full logo, shown while expanded. In Figma the English logo is 90px wide and the Arabic logo 28px tall.
logoIconReactNode—The logo's icon alone, shown while collapsed. Figma sets it at 28px.
collapseLabelstring"Collapse sidebar"The toggle's accessible name while expanded.
expandLabelstring"Expand sidebar"The toggle's accessible name while collapsed.
classNamestring—Extra classes for the header.

SidebarContent

PropTypeDefaultDescription
aria-labelstring—Names the navigation landmark.
childrenReactNode—The SidebarGroup elements.
classNamestring—Extra classes for the nav element.

SidebarGroup

PropTypeDefaultDescription
labelReactNode—The section title. When collapsed, it becomes a divider and stays the group's accessible name.
childrenReactNode—The SidebarItem elements.
classNamestring—Extra classes for the group.

SidebarItem

PropTypeDefaultDescription
labelReactNode—The item's text. When collapsed, it is the tooltip and the accessible name.
iconReactNode—A 16px icon before the label. When collapsed, only the icon shows, so give every item one.
selectedbooleanfalseMarks the current page. Set it on one item only.
badgebooleanfalseShows the notification dot at the end of the row.
badgeLabelstring"New"The dot's name for screen readers.
hrefstring—Renders the item as a link to this address.
renderReactElement—Renders this element in place of the link, such as a router's Link.
openboolean—Shows or hides its sub-items, when you control it.
defaultOpenboolean—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.
childrenReactNode—The SidebarSubItem elements, one level deep. With any, the item opens and closes them instead of linking.
classNamestring—Extra classes for the item's link or button.

SidebarSubItem

PropTypeDefaultDescription
labelReactNode—The sub-item's text.
selectedbooleanfalseMarks the current page. Its parent opens to show it, and shows as selected when the sidebar is collapsed.
badgebooleanfalseShows the notification dot at the end of the row.
badgeLabelstring"New"The dot's name for screen readers.
hrefstring—Renders the sub-item as a link to this address.
renderReactElement—Renders this element in place of the link, such as a router's Link.
onClick(event) => void—Called when the sub-item is pressed.
classNamestring—Extra classes for the sub-item's link or button.

SidebarPlanCard

PropTypeDefaultDescription
titleReactNode—The plan's name.
descriptionReactNode—A line under the name.
footerReactNode—The line under the divider.
footerIconReactNode—A 16px icon before footer.
classNamestring—Extra classes for the card.

SidebarFooter

PropTypeDefaultDescription
childrenReactNode—The SidebarProfile.
classNamestring—Extra classes for the footer.

SidebarProfile

PropTypeDefaultDescription
namestring—The person's name. When collapsed, it is the button's accessible name.
descriptionReactNode—A line under the name, such as the workspace.
avatarAvatarProps—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.
classNamestring—Extra classes for the profile button.

Usage

An app sidebar

tsx
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

tsx
"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

tsx
"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

tsx
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>
  )
}