# Resizable Sidebar

> A shadcn-inspired application sidebar with a draggable resize handle.

A shadcn/ui block from the [@uicapsule](https://uicapsule.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uicapsule/sidebar).

- Page, with a live preview: https://shadcn.exchange/r/uicapsule/sidebar
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uicapsule/sidebar-light-eeb9ff5480f5e245.png
- Registry item: https://uicapsule.com/r/sidebar.json
- Categories: navigation, minimal
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uicapsule.com/r/sidebar.json
```

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

## Dependencies

- npm packages: `lucide-react`

## Files

### `/dashboard.tsx`

```tsx
import { Fragment } from "react";

export const Dashboard = () => (
  <div>
    <div className="border-b border-[#EEEFF1]">
      <div className="flex items-center justify-between pt-3 pr-[19px] pb-[11px] pl-4">
        <div className="flex items-center gap-x-[7px]">
          <div className="h-5 w-20 rounded bg-gray-200" />
          <div className="size-4 rounded-full bg-gray-200" />
        </div>
        <div className="flex items-center gap-x-[26px]">
          <div className="flex">
            <div className="w-4">
              <div className="size-6 rounded-full bg-gray-200" />
            </div>
            <div className="w-4">
              <div className="size-6 rounded-full bg-gray-200" />
            </div>
            <div className="w-4">
              <div className="size-6 rounded-full bg-gray-200" />
            </div>
            <div className="w-4">
              <div className="size-6 rounded-full bg-gray-200" />
            </div>
          </div>
          <div className="size-4 rounded bg-gray-200" />
        </div>
      </div>
    </div>
    <div className="border-b border-[#EEEFF1]">
      <div className="flex items-center justify-between px-3 pt-2.5 pb-[9px]">
        <div className="h-5 w-24 rounded bg-gray-200" />
        <div className="flex items-center gap-x-2">
          <div className="flex items-center gap-1.5 rounded-sm py-1 pr-2 pl-1.5 shadow-[0px_0px_1px_0px_#E0E0E0,0px_1px_2px_-1px_rgba(24,39,75,0.02),0px_2px_2px_-1px_rgba(24,39,75,0.06)]">
            <div className="h-5 w-16 rounded bg-gray-200" />
          </div>
          <div className="flex items-center rounded-sm py-1 pr-1.5 pl-2 shadow-[0px_0px_1px_0px_#E0E0E0,0px_1px_2px_-1px_rgba(24,39,75,0.02),0px_2px_2px_-1px_rgba(24,39,75,0.06)]">
            <div className="h-5 w-20 rounded bg-gray-200" />
          </div>
        </div>
      </div>
    </div>
    <div className="h-full w-full overflow-hidden">
      <div className="flex h-full w-full flex-col">
        <div className="flex h-[49px] shrink-0 items-center gap-x-[7px] border-b border-[#EEEFF1] px-3">
          <div className="flex h-7 items-center gap-x-1.5 rounded-lg border border-[#E6E7EA] bg-[#FBFBFB] pr-[4.5px] pl-[2.5px]">
            <div className="h-5 w-32 rounded bg-gray-200" />
          </div>
          <div className="h-[21px] w-px rounded-full bg-[#EEEFF1]" />
          <div className="flex h-7 items-center rounded-lg border border-[#E6E7EA] bg-[#FBFBFB] pr-[2.5px] pl-1.5">
            <div className="flex items-center gap-x-[7px]">
              <div className="h-5 w-24 rounded bg-gray-200" />
            </div>
            <div className="mr-1 ml-1.5 h-full w-px bg-[#E6E7EA]" />
            <div className="size-4 rounded bg-gray-200" />
          </div>
        </div>
        <div className="relative h-full w-full flex-1">
          <div className="grid auto-rows-[40px] grid-cols-[204px_minmax(136px,1fr)_minmax(204px,1.5fr)_minmax(136px,1fr)_204px_204px]">
            <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
              <div className="flex h-full w-full items-center justify-between gap-x-1.5 pr-[4.5px] pl-2">
                <div className="flex items-center gap-x-2.5 overflow-hidden">
                  <div className="h-5 w-16 rounded bg-gray-200" />
                </div>
                <div className="h-5 w-5 rounded bg-gray-200" />
              </div>
            </div>
            <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
              <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                <div className="h-5 w-16 rounded bg-gray-200" />
              </div>
            </div>
            <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
              <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                <div className="h-5 w-20 rounded bg-gray-200" />
              </div>
            </div>
            <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
              <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                <div className="h-5 w-18 rounded bg-gray-200" />
              </div>
            </div>
            <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
              <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                <div className="h-5 w-20 rounded bg-gray-200" />
              </div>
            </div>
            <div className="overflow-hidden border-b border-[#EEEFF1]">
              <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                <div className="h-5 w-16 rounded bg-gray-200" />
              </div>
            </div>
            {Array.from({ length: 20 }, (_, index) => (
              <Fragment key={index}>
                <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
                  <div className="flex h-full w-full items-center justify-between gap-x-1.5 pr-[4.5px] pl-2">
                    <div className="flex items-center gap-x-2.5 overflow-hidden">
                      <div className="size-4 rounded bg-gray-200" />
                      <div className="h-5 w-24 rounded bg-gray-200" />
                    </div>
                    <div className="h-5 w-5 rounded bg-gray-200" />
                  </div>
                </div>
                <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
                  <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                    <div className="h-5 w-20 rounded bg-gray-200" />
                  </div>
                </div>
                <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
                  <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                    <div className="h-5 w-28 rounded bg-gray-200" />
                  </div>
                </div>
                <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
                  <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                    <div className="h-5 w-16 rounded bg-gray-200" />
                  </div>
                </div>
                <div className="overflow-hidden border-r border-b border-[#EEEFF1]">
                  <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                    <div className="h-5 w-20 rounded bg-gray-200" />
                  </div>
                </div>
                <div className="overflow-hidden border-b border-[#EEEFF1]">
                  <div className="flex h-full w-full items-center gap-x-1.5 pr-[3.5px] pl-1">
                    <div className="h-5 w-18 rounded bg-gray-200" />
                  </div>
                </div>
              </Fragment>
            ))}
          </div>
        </div>
      </div>
    </div>
  </div>
);
```

### `/preview.tsx`

```tsx
"use client";

import { Dashboard } from "./dashboard";
import { Sidebar } from "./sidebar";

const Preview = () => (
  <div
    className="relative grid h-full w-full overflow-hidden"
    style={{ gridTemplateColumns: "var(--sidebar-width, 280px) 1fr" }}
  >
    <Sidebar />
    <Dashboard />
  </div>
);

export default Preview;
```

### `/sidebar.tsx`

```tsx
import type { ReactNode } from "react";
import {
  BarChart3,
  Bell,
  Building,
  Building2,
  CheckSquare,
  ChevronDown,
  DollarSign,
  FileText,
  GitBranch,
  Grid3X3,
  Link,
  Mail,
  Settings,
  Star,
  Users,
  Workflow,
} from "lucide-react";

import { useResizableSidebar } from "./use-resizable-sidebar";

interface NavItem {
  id: string;
  label: string;
  /** A lucide element, or a plain string to render an emoji glyph instead. */
  icon: ReactNode;
  isActive?: boolean;
  subItems?: NavItem[];
}

const navigationItems: NavItem[] = [
  { icon: <Bell size={14} />, id: "notifications", label: "Notifications" },
  { icon: <CheckSquare size={14} />, id: "tasks", label: "Tasks" },
  { icon: <Mail size={14} />, id: "emails", label: "Emails" },
  { icon: <BarChart3 size={14} />, id: "reports", label: "Reports" },
  {
    icon: <Settings size={14} />,
    id: "automations",
    label: "Automations",
    subItems: [
      { icon: <Workflow size={14} />, id: "workflows", label: "Workflows" },
      { icon: <GitBranch size={14} />, id: "sequences", label: "Sequences" },
    ],
  },
];

const favoritesItems: NavItem[] = [
  {
    icon: <Star size={14} />,
    id: "onboarding-pipeline",
    label: "Onboarding pipeline",
  },
  {
    icon: <Building2 size={14} />,
    id: "top-of-funnel",
    label: "Top of funnel",
  },
  {
    icon: <FileText size={14} />,
    id: "revops-workflows",
    label: "RevOps workflows",
  },
];

const recordsItems: NavItem[] = [
  {
    icon: <Building size={14} />,
    id: "companies",
    isActive: true,
    label: "Companies",
  },
  { icon: <Users size={14} />, id: "people", label: "People" },
  { icon: <DollarSign size={14} />, id: "deals", label: "Deals" },
  { icon: <Grid3X3 size={14} />, id: "workspaces", label: "Workspaces" },
  { icon: <Link size={14} />, id: "partnerships", label: "Partnerships" },
];

/** Emoji glyphs need the centring wrapper a component icon draws itself. */
const EmojiIcon = ({ children }: { children: string }) => (
  <span className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[16px] leading-4">
    {children}
  </span>
);

const listsItems: NavItem[] = [
  {
    icon: <EmojiIcon>🚀</EmojiIcon>,
    id: "strategic-accounts",
    label: "Strategic accounts",
  },
];

const NavigationItem = ({ item }: { item: NavItem }) => (
  <div className="flex w-full flex-col">
    <div
      className={`flex min-w-0 items-center gap-x-1.5 rounded-[9px] px-2 py-1 transition-colors duration-500 [transition-timing-function:cubic-bezier(0.65,0,0.35,1)] ${item.isActive ? "bg-[#F4F5F6]" : ""}`}
    >
      <div className="relative w-[14px]">{item.icon}</div>
      <span className="min-w-0 flex-1 truncate text-[14px] leading-5 font-medium tracking-[-0.28px]">
        {item.label}
      </span>
      {item.subItems && <ChevronDown size={14} />}
    </div>
    {item.subItems && (
      <div className="flex flex-col gap-y-px py-px">
        {item.subItems.map((subItem) => (
          <div key={subItem.id} className="relative flex pl-5">
            <div className="absolute left-3.5 h-[32px] w-px -translate-y-[2px] bg-[#D1D3D6] opacity-40" />
            <NavigationItem item={subItem} />
          </div>
        ))}
      </div>
    )}
  </div>
);

const NavigationSection = ({ title, items }: { title: string; items: NavItem[] }) => (
  <div className="flex flex-col">
    <div className="flex min-w-0 items-center gap-x-1.5 px-2 py-1.5">
      <ChevronDown size={14} />
      <span className="min-w-0 flex-1 truncate text-[12px] leading-4 font-medium tracking-normal text-[#75777C]">
        {title}
      </span>
    </div>
    <ul className="flex flex-col gap-px">
      {items.map((item) => (
        <NavigationItem key={item.id} item={item} />
      ))}
    </ul>
  </div>
);

export const Sidebar = () => {
  const { handleMouseDown } = useResizableSidebar();

  return (
    <div className="relative border-r border-[#EEEFF1] bg-[#FBFBFB]">
      <div className="flex items-center justify-between gap-x-6 border-r border-b border-[#EEEFF1] bg-[#FBFBFB] pt-3 pr-[15px] pb-[11px] pl-3">
        <div className="flex min-w-0 flex-1 items-center">
          <img
            alt="Dataembed"
            loading="lazy"
            width="96"
            height="96"
            decoding="async"
            className="size-6 shrink-0 rounded"
            src="https://www.dataembed.com/favicon/web-app-manifest-512x512.png"
          />
          <div className="text-secondary-foreground ml-2 min-w-0 truncate text-[16px] leading-5 font-semibold tracking-[-0.32px]">
            Dataembed
          </div>
          <svg className="ml-[5px] shrink-0" width="18" height="18" fill="none">
            <path
              d="M5.25 7.125 9 10.875l3.75-3.75"
              stroke="#5C5E63"
              strokeWidth="1.2"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
          </svg>
        </div>
        <svg width="18" height="18" fill="none" xmlns="http://www.w3.org/2000/svg">
          <g stroke="#75777C" strokeWidth="1.2">
            <rect x="1.5" y="2.5" width="15" height="13" rx="3" />
            <path d="M7.8 2.725v12.5" />
            <path
              d="M3.975 5.425h1.35M3.975 7.674h1.35"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
          </g>
        </svg>
      </div>
      <div className="relative row-span-2 block border-r border-[#EEEFF1] bg-[#FBFBFB] pt-[10px] pr-[7px] pl-2">
        <div className="flex min-w-0 flex-1 items-center justify-between rounded-sm bg-white py-1 pr-1 pl-1.5 shadow-[0px_0px_2px_0px_#E0E0E0,0px_2px_4px_-2px_rgba(24,39,75,0.02),0px_4px_4px_-2px_rgba(24,39,75,0.06)]">
          <div className="flex min-w-0 flex-1 items-center gap-x-1">
            <svg width="14" height="14" fill="none" className="shrink-0">
              <rect
                x="2"
                y="1"
                width="10"
                height="12"
                rx="2.5"
                stroke="#232529"
                strokeWidth="1.1"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
              <rect
                x="2"
                y="1"
                width="10"
                height="9"
                rx="2.5"
                stroke="#232529"
                strokeWidth="1.1"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
              <path
                d="M5.333 3.166v4.667m0-1.667 1.053-1m0 0 2.105-2m-2.105 2 2.28 2.667"
                stroke="#232529"
                strokeWidth="1.1"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
            <span className="min-w-0 flex-1 truncate text-[14px] leading-5 font-medium tracking-[-0.28px]">
              Quick actions
            </span>
          </div>
          <div className="flex min-w-[20px] shrink-0 justify-center rounded-md border border-[#E6E7EA] p-[3px]">
            <span className="text-center text-[11px] leading-3 font-normal tracking-[0.22px] text-[#75777C]">
              ⌘K
            </span>
          </div>
        </div>
        <div className="mt-2.5 flex flex-col gap-[9px]">
          <div className="flex flex-col">
            <ul className="flex flex-col gap-px">
              {navigationItems.map((item) => (
                <NavigationItem key={item.id} item={item} />
              ))}
            </ul>
          </div>

          <NavigationSection title="Favorites" items={favoritesItems} />
          <NavigationSection title="Records" items={recordsItems} />
          <NavigationSection title="Lists" items={listsItems} />
        </div>
      </div>

      {/* Resize Handle */}
      {/* oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- mouse-only resize affordance; the sidebar is fully usable at its default width, so there is no keyboard equivalent to provide */}
      <div
        className="absolute top-0 -right-0.5 h-full w-1 cursor-col-resize bg-transparent transition-colors duration-200 hover:bg-[#266DF0] active:bg-[#266DF0]"
        onMouseDown={handleMouseDown}
      />
    </div>
  );
};
```

### `/use-resizable-sidebar.ts`

```ts
import { useEffect, useRef } from "react";
import type { MouseEvent as ReactMouseEvent } from "react";

interface UseResizableSidebarOptions {
  defaultWidth?: number;
  minWidth?: number;
  maxWidth?: number;
}

export const useResizableSidebar = ({
  defaultWidth = 240,
  minWidth = 100,
  maxWidth = 300,
}: UseResizableSidebarOptions = {}) => {
  const startXRef = useRef<number>(0);
  const startWidthRef = useRef<number>(0);
  const isResizingRef = useRef<boolean>(false);

  const handleMouseDown = (e: ReactMouseEvent) => {
    e.preventDefault();
    isResizingRef.current = true;
    startXRef.current = e.clientX;
    // oxlint-disable-next-line unicorn/prefer-number-coercion -- parseInt tolerates a trailing unit the replace() misses; Number() would NaN on it
    startWidthRef.current = Number.parseInt(
      getComputedStyle(document.documentElement)
        .getPropertyValue("--sidebar-width")
        .replace("px", "") || defaultWidth.toString(),
      10,
    );
    document.body.style.cursor = "col-resize";
    document.body.style.userSelect = "none";
  };

  useEffect(() => {
    document.documentElement.style.setProperty("--sidebar-width", `${defaultWidth}px`);

    // The width lives in a CSS variable rather than state so dragging never re-renders.
    const handleMouseMove = (e: MouseEvent) => {
      if (!isResizingRef.current) {
        return;
      }

      const newWidth = startWidthRef.current + (e.clientX - startXRef.current);
      const clampedWidth = Math.min(Math.max(newWidth, minWidth), maxWidth);
      document.documentElement.style.setProperty("--sidebar-width", `${clampedWidth}px`);
    };

    const handleMouseUp = () => {
      if (!isResizingRef.current) {
        return;
      }
      isResizingRef.current = false;
      document.body.style.cursor = "";
      document.body.style.userSelect = "";
    };

    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);

    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
      document.body.style.cursor = "";
      document.body.style.userSelect = "";
    };
  }, [defaultWidth, minWidth, maxWidth]);

  return {
    handleMouseDown,
  };
};
```
