# App Sidebar

> A complete app shell: workspace header with the peek cross-fade, search on the menu rows' rhythm with a hover-revealed shortcut chip, collapsible sections with label actions and badges, a user footer, and an inset topbar whose trigger fades back in at its settled position after a pin.

A shadcn/ui block from the [@fluid](https://www.fluidfunctionalism.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/fluid/sidebar-app).

- Page, with a live preview: https://shadcn.exchange/r/fluid/sidebar-app
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/fluid/sidebar-app-light-cc18ae0fe1a68904.png
- Registry item: https://www.fluidfunctionalism.com/r/sidebar-app.json
- Categories: blocks
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/fluid/sidebar-app.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `utils`, `https://www.fluidfunctionalism.com/r/icon-context.json`, `https://www.fluidfunctionalism.com/r/size-context.json`, `https://www.fluidfunctionalism.com/r/sidebar.json`, `https://www.fluidfunctionalism.com/r/tooltip.json`, `https://www.fluidfunctionalism.com/r/dropdown.json`, `https://www.fluidfunctionalism.com/r/sidebar-workspace-header.json`, `https://www.fluidfunctionalism.com/r/sidebar-user-footer.json`, `https://www.fluidfunctionalism.com/r/sidebar-search-field.json`, `https://www.fluidfunctionalism.com/r/sidebar-inset-topbar.json`, `https://www.fluidfunctionalism.com/r/type-scale.json`

## Notes from the registry

Docs & live playground: https://www.fluidfunctionalism.com/docs/sidebar. Craft notes, the color, layout and motion decisions to keep when you change it: https://github.com/mickadesign/fluid-functionalism/blob/main/skills/fluid-functionalism/references/craft.md#sidebar-app.

## Files

### `registry/blocks/sidebar-app/app-sidebar.tsx`

```tsx
"use client";

import { useState } from "react";
import {
  Sidebar,
  SidebarHeader,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarGroupActions,
  SidebarGroupAction,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuButton,
  SidebarMenuBadge,
  type SidebarProps,
} from "@/components/ui/sidebar";
import { Tooltip } from "@/components/ui/tooltip";
import { MenuItem } from "@/components/ui/menu-item";
import { useIcons } from "@/lib/icon-context";
import {
  SidebarWorkspaceHeader,
  WorkspaceTile,
} from "@/components/sidebar-app/workspace-header";
import { SidebarUserFooter } from "@/components/sidebar-app/user-footer";
import { SidebarSearchField } from "@/components/sidebar-app/search-field";
import { NAV_SECTIONS } from "@/components/sidebar-app/nav-data";

// ---------------------------------------------------------------------------
// A complete app sidebar assembled from the workspace-header, search-field,
// and user-footer blocks: brand row with the peek cross-fade, search on the
// rows' rhythm, collapsible sections with label actions, badge-carrying
// rows, and the identity row anchoring the outer edge. Swap the seed data
// in nav-data.ts and the names below for your own.
// ---------------------------------------------------------------------------

export function AppSidebar(props: Omit<SidebarProps, "children">) {
  const [active, setActive] = useState("Home");
  const icons = useIcons();
  const PlusIcon = icons.plus;
  const UserIcon = icons.user;
  const SettingsIcon = icons.settings;
  const ArrowLeftIcon = icons["arrow-left"];

  return (
    <Sidebar variant="inset" {...props}>
      <SidebarHeader>
        <SidebarWorkspaceHeader
          name="Acme Inc"
          tile={<WorkspaceTile>A</WorkspaceTile>}
          checkedIndex={0}
          menu={
            <>
              <MenuItem index={0} label="Acme Inc" checked onSelect={() => {}} />
              <MenuItem index={1} label="Personal" onSelect={() => {}} />
              <MenuItem index={2} icon={PlusIcon} label="New workspace" onSelect={() => {}} />
            </>
          }
        />
        {/* Search + action rows are ONE block: the field reads as the list's
            first row, on the menu rows' own tight rhythm. */}
        <div className="flex flex-col">
          <SidebarSearchField />
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton icon={PlusIcon}>
                New
                {/* shortcut chip, revealed on row hover */}
                <span className="ml-auto inline-flex opacity-0 transition-opacity duration-80 group-hover/menu-item:opacity-100 group-focus-within/menu-item:opacity-100">
                  <kbd className="font-sans text-[length:var(--fs-micro,11px)] leading-[var(--lh-micro,14px)] text-muted-foreground">
                    ⇧⌘O
                  </kbd>
                </span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </div>
      </SidebarHeader>

      <SidebarContent>
        {NAV_SECTIONS.map((section) => (
          <SidebarGroup key={section.label} collapsible>
            <SidebarGroupLabel>{section.label}</SidebarGroupLabel>
            <SidebarGroupActions>
              <Tooltip content="Add item" side="top">
                <SidebarGroupAction aria-label="Add item">
                  <PlusIcon />
                </SidebarGroupAction>
              </Tooltip>
            </SidebarGroupActions>
            <SidebarMenu>
              {section.items.map((item) => (
                <SidebarMenuItem key={item.label}>
                  <SidebarMenuButton
                    icon={icons[item.icon]}
                    isActive={item.label === active}
                    onClick={() => setActive(item.label)}
                  >
                    {item.label}
                  </SidebarMenuButton>
                  {item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
                </SidebarMenuItem>
              ))}
            </SidebarMenu>
          </SidebarGroup>
        ))}
      </SidebarContent>

      <SidebarFooter>
        <SidebarUserFooter
          name="Jane Doe"
          avatar={
            <span className="flex size-5 items-center justify-center rounded-full bg-muted-foreground text-[length:var(--fs-micro-compact,10px)] leading-[var(--lh-micro-compact,12px)] text-background">
              J
            </span>
          }
          menu={
            <>
              <MenuItem index={0} icon={UserIcon} label="Profile" onSelect={() => {}} />
              <MenuItem index={1} icon={SettingsIcon} label="Settings" onSelect={() => {}} />
              <MenuItem index={2} icon={ArrowLeftIcon} label="Log out" onSelect={() => {}} />
            </>
          }
        />
      </SidebarFooter>
    </Sidebar>
  );
}
```

### `registry/blocks/sidebar-app/nav-data.ts`

```ts
import { type IconName } from "@/lib/icon-context";

// ---------------------------------------------------------------------------
// Seed navigation data — replace with your own. Icons are keys into the
// icon context (lucide by default; override via IconProvider).
// ---------------------------------------------------------------------------

export interface NavItem {
  label: string;
  icon: IconName;
  badge?: string;
}

export interface NavSection {
  label: string;
  items: NavItem[];
}

export const NAV_SECTIONS: NavSection[] = [
  {
    label: "Platform",
    items: [
      { label: "Home", icon: "star" },
      { label: "Inbox", icon: "mail", badge: "12" },
      { label: "Calendar", icon: "clock" },
    ],
  },
  {
    label: "Workspace",
    items: [
      { label: "Notifications", icon: "bell", badge: "3" },
      { label: "Members", icon: "users" },
      { label: "Settings", icon: "settings" },
    ],
  },
];
```

### `registry/blocks/sidebar-app/page.tsx`

```tsx
"use client";

import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/sidebar-app/app-sidebar";
import { SidebarInsetTopbar } from "@/components/sidebar-app/inset-topbar";

// ---------------------------------------------------------------------------
// Paste-and-run app shell. peek="hover" floats the collapsed rail back out
// the moment the cursor reaches its edge or the trigger; pinning it open
// from that peek is seamless. State persists to the "sidebar_state" cookie —
// read it in a server layout for a flicker-free default:
//
//   const defaultOpen = (await cookies()).get("sidebar_state")?.value !== "false";
//   <SidebarProvider defaultOpen={defaultOpen}>…
// ---------------------------------------------------------------------------

export default function Page() {
  return (
    <SidebarProvider peek="hover">
      <AppSidebar />
      <SidebarInset>
        <SidebarInsetTopbar />
        <main className="flex flex-1 flex-col gap-4 p-4">
          {/* your page */}
        </main>
      </SidebarInset>
    </SidebarProvider>
  );
}
```
