Sidebar React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React sidebar for admin apps: grouped links with icons, nested sections, badges, a sliding active pill, an icon rail and a filter.
Click a link and the active pill slides to it on a smooth spring; parents fold their children open with a chevron (a parent with its own page gets a separate chevron button), and collapsible group labels fold whole groups. Cmd/Ctrl+B or the footer button collapses it to a 64px icon rail: labels turn into tooltips, badges into dots, and parents open their children as a menu to the side. With the filter on, typing hides what does not match, opens the sections that do and bolds the matched letters; Escape clears it and arrow-down jumps into the list. An item's shortcut (Home has one in the demo) opens its page from anywhere. With the keyboard, Up / Down move between rows, Home / End jump, Right opens a parent (or steps into it) and Left closes it (or steps back to the parent). Rows darken while pressed and are 44px on touch screens; inside an app shell's phone drawer (mode="sheet") the rows grow and tapping a link closes the drawer. Collapsed and open sections are remembered with persistKey.
Sidebar is a Pro React component in the React navigation components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add sidebar, together with UIProvider, Avatar, Badge, Button, Dropdown Menu and 12 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add sidebarSidebar is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import Sidebar from @/components/ui/sidebar.
import { ChartColumn, FileText, Handshake, Inbox, LayoutDashboard, LifeBuoy, Package, Settings, ShoppingCart, Users } from "lucide-react"
import { Sidebar, type SidebarNavGroup } from "@/components/ui/sidebar"
const groups: SidebarNavGroup[] = [
{
items: [
{ label: "Home", href: "/", icon: <LayoutDashboard /> },
{ label: "Inbox", href: "/inbox", icon: <Inbox />, badge: 4 },
],
},
{
label: "Sales",
items: [
{ label: "Clients", href: "/clients", icon: <Users /> },
{ label: "Offers", href: "/offers", icon: <Handshake />, badge: 12 },
{
label: "Invoices",
href: "/invoices",
icon: <FileText />,
items: [
{ label: "Issued", href: "/invoices/issued" },
{ label: "Paid", href: "/invoices/paid" },
{ label: "Overdue", href: "/invoices/overdue", badge: 3 },
],
},
],
},
{
label: "Operations",
collapsible: true,
items: [
{ label: "Products", href: "/products", icon: <Package /> },
{ label: "Orders", href: "/orders", icon: <ShoppingCart /> },
{ label: "Reports", href: "/reports", icon: <ChartColumn /> },
],
},
{
label: "Account",
items: [
{ label: "Settings", href: "/settings", icon: <Settings /> },
{ label: "Help", href: "https://example.com/help", icon: <LifeBuoy /> },
],
},
]
export function AppSidebar({ pathname }: { pathname: string }) {
return (
<Sidebar
groups={groups}
currentPath={pathname}
persistKey="crm-sidebar"
filter
user={{ name: "Emma Carter", email: "emma.carter@northwind.com", avatar: "/demo/ana-pop.jpg", plan: "Pro" }}
className="h-dvh"
/>
)
}Props
groupsSidebarNavGroup[]The navigation: { id?, label?, items, collapsible?, defaultOpen? } per group. Items are NavItem ({ label, href, icon, activeIcon, badge, shortcut, keywords, external, disabled, match, onSelect, items }), nested one level deep.
itemsSidebarNavItem[]One unlabeled group, for small apps. Ignored when groups is set.
headerReactNodeAbove the navigation: a WorkspaceSwitcher variant="card" or a logo. It adapts to the rail through the group/sidebar CSS contract (data-collapsed="true").
footerReactNodeBelow the navigation, before the collapse button: a UserMenu trigger="card". Replaces the user card.
user{ name: string; email?: string; avatar?: string; plan?: ReactNode }A simple user card in the footer when there is no footer; onUserClick makes it a button.
onUserClick(event: MouseEvent<HTMLButtonElement>) => voidMakes the user card a button.
defaultCollapseddefaultfalsebooleanStarts as the 64px icon rail. Use collapsed + onCollapsedChange to control it; inside an AppShell the shell owns it.
collapsedbooleanControlled collapsed state.
onCollapsedChange(collapsed: boolean) => voidFires when the rail opens or closes (button, shortcut).
valuestringThe active item's key (its id, else href, else label): controlled. Without it the active item follows currentPath, else the sidebar keeps its own (defaultValue, default the first enabled item).
defaultValuestringThe first active item when neither value nor a path is known.
onValueChange(key: string, item: SidebarNavItem) => voidFires on every activation, in all three modes.
currentPathdefault""stringThe path the active item is matched against (the longest matching href wins). Default: the LinkProvider's. The playground's pretend router provides it.
persistKeydefault""stringRemembers collapsed (standalone) and the open sections in local storage under `${persistKey}-collapsed` and `${persistKey}-open`. Empty keeps them in memory.
loadingdefaultfalsebooleanSkeleton rows (icon + line) in place of the items, with aria-busy on the nav.
filterdefaultfalsebooleanA filter field above the navigation. Matches labels and keywords ignoring case and diacritics, opens the sections that hold a match and bolds the matched letters. On the rail it becomes a search button that expands the sidebar.
filterPlaceholderdefault"Search"stringThe filter's placeholder.
filterShortcutdefault""stringA key combo (mod+k notation) that focuses the filter from anywhere, expanding the rail first. Shown as a hint in the field.
defaultFilterValuedefault""stringStarts the filter with a query (uncontrolled), e.g. a search carried over from another page.
onFilterValueChange(value: string) => voidFires as the filter text changes.
filterValuestringThe filter text, controlled.
variantdefault"sidebar""sidebar" | "floating" | "inset"sidebar: flush, a hairline on the end edge. floating: a raised rounded panel 8px off the edges. inset: no edge and no fill, on the app shell's tint.
activeStyledefault"soft""soft" | "raised" | "bar"The active row: a tinted pill, a raised white pill (best on floating or tinted sidebars), or a 3px bar on the start edge over a light tint.
sizedefault"md""sm" | "md"Rows 32 / 36px with 13 / 14px text; 44px on touch screens either way.
widthdefault256numberExpanded width in px (160 to 480). The rail is always 64px; the width animates between the two.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Colors the active indicator, the active row's text and its badge. Neutral is a gray pill with black / white text.
colordefault""stringAny CSS color for the active indicator; replaces the tone.
collapsibledefault"icon""icon" | "none"icon: it can become the 64px rail (button and shortcut). none: always expanded, no toggle.
showToggledefaulttruebooleanThe collapse button at the end of the footer, with a tooltip showing the shortcut.
shortcutdefault"mod+b"stringToggles the rail from anywhere when the sidebar stands alone (mod is Cmd on Apple devices, Ctrl elsewhere). Inside an app shell the shell binds its own. Empty turns it off.
itemShortcutsdefaulttruebooleanBinds each item's shortcut (mod+k notation, shown as a hint on hover and in rail tooltips) to open that page from anywhere, with aria-keyshortcuts on the row. Off leaves them as hints. Sheet mode never binds them (the desktop copy does).
tooltipsdefaulttruebooleanOn the rail every row shows its label (and shortcut) in a tooltip to the side; expanded, only labels cut off by the width do.
flyoutsdefaulttruebooleanOn the rail a parent opens its children as a menu to the side (a bottom sheet on phones). Off, clicking a parent expands the sidebar with that parent open.
modedefault"rail""rail" | "sheet"rail: the desktop column, collapsible. sheet: the content of a phone drawer: never collapsed, 44px rows at 15px, no toggle, and a tap on a link closes the app shell's drawer (the app shell renders its drawer copy this way).
linkComponent(props: LinkComponentProps) => ReactNodeThe router link for this sidebar; default the LinkProvider's, else a plain <a>.
labels{ sidebar?; navigation?; filter?; noResults?: (query) => string; collapse?; expand?; toggleGroup?: (label) => string; clearFilter?; loading? }Every string, English by default: "Sidebar", "Main", "Filter navigation", "Nothing matches “…”", "Collapse sidebar", "Expand sidebar", "Toggle …", "Clear filter", "Loading navigation".
classNamesPartial<Record<"inner" | "header" | "filter" | "content" | "group" | "groupLabel" | "list" | "item" | "link" | "icon" | "label" | "badge" | "indicator" | "sublist" | "empty" | "footer" | "toggle" | "userCard", string>>Extra classes for the inner parts.
classNamestringExtra classes for the <aside>. Set --sidebar-bg to change its surface.
childrenReactNodeReplaces the generated groups inside the scrolling nav (SidebarGroup / SidebarItem).
Parts
SidebarRoot
componentThe <aside> with the state (collapsed, active item, open sections, filter) for the parts below. Takes every Sidebar prop except header, footer, user, filter* and showToggle. Props: SidebarRootProps.
SidebarHeader
componentThe top area (p-3); a hairline appears under it when the list scrolls. Takes div props.
SidebarContent
componentThe scrolling <nav> with fading edges and the arrow-key navigation. Renders the root's groups when it has no children. Takes nav props.
SidebarGroup
componentOne labelled group (a fold-away label when collapsible), for hand-built layouts. Props: { group: SidebarNavGroup; index?: number; children?: ReactNode }.
SidebarItem
componentOne row with its nested children. Props: { item: SidebarNavItem; index?: number; depth?: number; parentKey?: string }.
SidebarFilter
componentThe filter field (a search button on the rail). Props: { placeholder?: string; shortcut?: string } & input props.
SidebarToggle
componentThe collapse button with its tooltip; renders nothing in sheet mode or when collapsible is none. Props: ButtonProps.
SidebarUserCard
componentAvatar, name, plan and email; the avatar alone on the rail. A button with onClick or interactive (a menu trigger). Props: { user: SidebarUser; trailing?: ReactNode; interactive?: boolean } & button props.
useSidebar
hookThe sidebar's state for anything rendered inside it. Signature: () => { collapsed; setCollapsed; toggle; mode; activePath }.
SidebarUser
typeThe footer user card: name, email, avatar and plan.
UseSidebarResult
typeWhat useSidebar returns: collapsed, setCollapsed, toggle, mode and activePath (the active item with its parents, or null).
Examples
The icon rail
defaultCollapsed: tooltips to the side, dots for badges, and Invoices and Settings open their pages as a flyout menu.
Filter
“rest” finds Invoices › Overdue: the section opens and the match is bold. Press F to focus it, Escape to clear.
Floating with a raised pill
variant="floating" + activeStyle="raised" on a tinted page, the white pill sliding between rows.
Bar indicator in the brand tone
activeStyle="bar" + tone="brand": a 3px bar on the start edge that runs along the guide line into nested rows.
Phone drawer (sheet mode)
mode="sheet" inside a drawer: 44px rows at 15px, no toggle, and tapping a link closes the drawer.
Loading
loading renders skeleton rows (icon + line) with aria-busy.
External links and “New” badges
Any node works as a badge; external links get a trailing arrow and a new tab.
Dependencies
Files
- components/ui/sidebar.tsx
FAQ
Is the React Sidebar component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Sidebar?
Run npx wingo-ui@latest add sidebar. It also adds the Wingo UI items it builds on (UIProvider, Avatar, Badge, Button, Dropdown Menu and 12 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/sidebar.
See also
Used in
More React navigation components
Articles using this component
- Shadcn Sidebar Mobile: Fix the Sheet and Add a Tab Bar
Mobile UI in React9 min read
- Nextjs Admin Dashboard Tutorial: 5 Screens, Phone-Ready
Build it: screens and apps23 min read
- React Bottom Navigation Bar in Next.js: Safe Areas, Badges
Mobile UI in React8 min read