Skip to content

Sidebar Dual

Two-level navigation: an ink icon rail switches sections, and a paper second panel lists that section's links with counts and a yellow active link. Second panel hides on very small screens.

brutalist/layout/sidebar-dual
Open ↗

Source

"use client";

import { useState, type ReactNode } from "react";
import { BarChart3, FileText, Plug, Settings, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";

export interface SidebarDualSection {
  id: string;
  label: string;
  icon: LucideIcon;
  links: { label: string; href: string; count?: number }[];
}

export interface SidebarDualProps {
  sections?: SidebarDualSection[];
  defaultSection?: string;
  activeHref?: string;
  children?: ReactNode;
  className?: string;
}

const DEFAULT_SECTIONS: SidebarDualSection[] = [
  {
    id: "releases",
    label: "Releases",
    icon: FileText,
    links: [
      { label: "All releases", href: "#all", count: 142 },
      { label: "Drafts", href: "#drafts", count: 3 },
      { label: "Scheduled", href: "#scheduled", count: 1 },
      { label: "Templates", href: "#templates" },
    ],
  },
  {
    id: "insights",
    label: "Insights",
    icon: BarChart3,
    links: [
      { label: "Overview", href: "#overview" },
      { label: "Widget", href: "#widget-stats" },
      { label: "Digests", href: "#digest-stats" },
    ],
  },
  {
    id: "integrations",
    label: "Integrations",
    icon: Plug,
    links: [
      { label: "GitHub", href: "#github" },
      { label: "Slack", href: "#slack" },
      { label: "Linear", href: "#linear" },
    ],
  },
  {
    id: "settings",
    label: "Settings",
    icon: Settings,
    links: [
      { label: "Workspace", href: "#workspace" },
      { label: "Members", href: "#members" },
      { label: "Billing", href: "#billing" },
    ],
  },
];

export function SidebarDual({ sections = DEFAULT_SECTIONS, defaultSection = "releases", activeHref = "#all", children, className }: SidebarDualProps) {
  const [current, setCurrent] = useState(defaultSection);
  const section = sections.find((s) => s.id === current) ?? sections[0];

  return (
    <div className={cn("flex min-h-full bg-da-bg text-da-fg", className)}>
      <div className="sticky top-0 flex h-dvh shrink-0">
        <nav aria-label="Sections" className="da-stroke-r flex w-[72px] flex-col items-center gap-2 bg-da-fg py-4 text-da-bg">
          {sections.map((s) => {
            const Icon = s.icon;
            const on = s.id === section?.id;
            return (
              <button
                key={s.id}
                type="button"
                aria-pressed={on}
                aria-label={s.label}
                title={s.label}
                onClick={() => setCurrent(s.id)}
                className={cn("da-focus grid size-12 place-items-center border-[length:var(--da-border-width)]", on ? "border-da-bg bg-da-primary text-da-primary-fg" : "border-transparent hover:border-da-bg")}
              >
                <Icon aria-hidden className="size-5" />
              </button>
            );
          })}
        </nav>
        {section && (
          <nav aria-label={section.label} className="da-stroke-r hidden w-56 flex-col bg-da-surface-2 sm:flex">
            <p className="da-stroke-b flex h-16 items-center px-4 font-da-display text-lg tracking-da-display uppercase">{section.label}</p>
            <ul className="flex flex-col gap-1 p-3">
              {section.links.map((l) => {
                const active = l.href === activeHref;
                return (
                  <li key={l.href}>
                    <a
                      href={l.href}
                      aria-current={active ? "page" : undefined}
                      className={cn("da-focus flex items-center justify-between px-3 py-2 font-bold", active ? "da-stroke bg-da-primary text-da-primary-fg" : "hover:bg-da-surface")}
                    >
                      {l.label}
                      {l.count !== undefined && <span className="font-da-mono text-xs">{l.count}</span>}
                    </a>
                  </li>
                );
              })}
            </ul>
          </nav>
        )}
      </div>
      <main className="min-w-0 flex-1">{children}</main>
    </div>
  );
}

export default SidebarDual;

modules/brutalist/layout/sidebar-dual/index.tsx

Props

PropTypeDefaultDescription
childrenReactNode—Page content.
sectionsSidebarDualSection[]—{ id, label, icon, links: { label, href, count? }[] }[].
defaultSectionstring"releases"Initially open section id.
activeHrefstring—Current href (aria-current).
classNamestring—Classes on the root.

Other sidebar variants in Brutalist

Sidebar in other art directions

App Sidebar

Floating frosted sidebar inset from the viewport edges: gradient-orb brand, pill nav items with an ink active state and gradient badges, workspace group, gradient promo card and user chip. Becomes a floating pill top bar + glass drawer below lg.

GlassSidebar

Sidebar Adaptive

Adaptive navigation: a full glass sidebar with brand, gradient action button, labelled links and a usage meter on desktop; a floating bottom tab bar with a raised gradient “+” on mobile.

GlassSidebar

Sidebar Channels

Chat-app style double sidebar: a column of gradient workspace squircles plus a glass channel panel with collapsible sections, private/live icons, unread counts and LIVE meeting badges; drawer on mobile.

GlassSidebar

Sidebar Floating

Floating glass icon rail detached from the viewport edges: gradient logo orb, gradient active item, unread badges, glass tooltips on hover/focus, settings and avatar at the bottom; a floating bottom bar on mobile.

GlassSidebar

App Sidebar

Linear-style app shell: 240px translucent sidebar with workspace switcher, search, "New" button with shortcut, grouped 32px nav rows with counters, team list with color squares and settings at the bottom. Slide-in drawer below lg.

MinimalSidebar

Sidebar Inset

Inset app shell: the sidebar sits on the tinted page background while content lives in a raised rounded panel. Workspace header, collapsible nested sections with guide line, support/settings and user card; drawer on mobile.

MinimalSidebar

Sidebar Rail

Sidebar that collapses between a 224px labelled list and a 56px icon rail (⌘B or bottom button) with tooltips on hover/focus and corner badges; icon-only on mobile. Controlled or uncontrolled.

MinimalSidebar

Sidebar Tree

Docs / settings sidebar with a filter box and a nested collapsible tree auto-expanded to the active page (highlighted in indigo); stacks above content on mobile.

MinimalSidebar

App Sidebar

Typographic app sidebar: hairline right border, logo + site switcher, mono group labels, plain text links with right-aligned mono counts; the active link is ink with a signal dot; drawer on mobile.

Mono CleanSidebar

Sidebar Icons

Narrow icon rail (56px) of square hairline cells; the active cell is filled ink; labels appear as mono tooltips to the right on hover/focus; turns into a bottom bar on mobile.

Mono CleanSidebar

Sidebar Ink

Ink sidebar (stays near-black in dark mode): giant numbered text links “01 Overview”, the active one full white with an arrow, a storage meter as a hairline bar at the bottom; drawer on mobile.

Mono CleanSidebar

Sidebar Tree

CMS page tree: nested pages drawn with hairline guide lines and +/− toggles (aria-expanded), mono “Pages” header with an add button; active page ink with signal dot; drawer on mobile.

Mono CleanSidebar

App Sidebar

Classic enterprise app shell: white bordered sidebar with brand, entity switcher, grouped nav (uppercase group labels, count badges, blue active state), settings and user footer; off-canvas drawer on mobile.

Neo CorporateSidebar

Sidebar Collapsible

Sidebar that collapses from 240px to a 64px icon rail with a toggle button (aria-expanded); collapsed items keep tooltips (title) and sr-only labels. Always a rail on small screens.

Neo CorporateSidebar

Sidebar Dual

Two-level navigation: a narrow icon rail of top-level sections (tooltips, aria-pressed) and a secondary panel listing the selected section’s pages; the panel hides below md.

Neo CorporateSidebar

Sidebar Navy

Dark navy sidebar (fg-colored in light mode, surface in dark mode — dark in both): brand, search field, nav with white-tint active state and badges, and a cash-balance card at the bottom; drawer on mobile.

Neo CorporateSidebar

App Sidebar

Calm app shell: cream sidebar with leaf logo, pill nav items (sage pill for the active page), a small “this year” footprint card with a sprout, and settings at the bottom; drawer on mobile.

Organic SoftSidebar

Sidebar Floating

Floating sidebar: a rounded cream panel inset from the page edges with a soft shadow, italic serif group labels, a sage “New entry” pill and sage-tinted active items; drawer on mobile.

Organic SoftSidebar

Sidebar Garden

Deep sage sidebar (dark sage tint in dark mode) with cream text, a translucent active pill, count bubbles, rolling hills illustrated along the bottom and the user’s name on the hill; drawer on mobile.

Organic SoftSidebar

Sidebar Rail

Slim round icon rail: circular icon links on a sand column, the active one a sage circle with a leaf-shaped marker; labels appear as soft tooltips on hover/focus; becomes a bottom tab bar on mobile.

Organic SoftSidebar

App Sidebar

Friendly app shell: warm sidebar with emoji workspace chip, big periwinkle “New task” pill, search field, nav with peach count bubbles and a colored-dot board list; slide-in drawer on mobile.

Soft FlatSidebar

Sidebar Boards

Board navigator: starred favorites, then collapsible emoji spaces holding emoji boards with count bubbles; active board in a white pill; dashed “New space” button.

Soft FlatSidebar

Sidebar Card

Floating white sidebar card with generous radius on a tinted page: two-dot logo, nav with lavender active pill, butter “tip of the day” card and user row; horizontal nav on mobile.

Soft FlatSidebar

Sidebar Rail

Slim rounded icon rail: round icons whose active item fills with its own pastel color, dark pill tooltips, emoji avatar at the bottom; floating bottom bar on mobile.

Soft FlatSidebar