Skip to content

Sidebar Conversations

AI chat app shell: near-black sidebar with brand and new-chat button, Agent Studio link, collapsible uppercase sections (spaces, direct agents with colored initials, conversations) with + actions, active pill, unread dot and user footer; slides in as a drawer on mobile.

nocturne/layout/sidebar-conversations
Open ↗

Source

"use client";

import { useEffect, useId, useState, type ReactNode } from "react";
import { ChevronDown, Menu, Palette, Plus, SquarePen, X } from "lucide-react";
import { cn } from "@/lib/utils";

export type SidebarTone = "primary" | "accent" | "success" | "warning";

export interface SidebarConversationsItem {
  label: string;
  href: string;
  /** Initials in a colored dot (agents, people). Omit for plain threads. */
  initials?: string;
  tone?: SidebarTone;
  unread?: boolean;
}

export interface SidebarConversationsSection {
  title: string;
  items: SidebarConversationsItem[];
  empty?: string;
  defaultOpen?: boolean;
}

export interface SidebarConversationsProps {
  brand?: string;
  studio?: { label: string; href: string };
  sections?: SidebarConversationsSection[];
  activeHref?: string;
  user?: { name: string; initials: string; plan: string };
  children?: ReactNode;
  className?: string;
}

const TONES: Record<SidebarTone, string> = {
  primary: "bg-da-primary text-da-primary-fg",
  accent: "bg-da-accent text-da-accent-fg",
  success: "bg-da-success text-da-success-fg",
  warning: "bg-da-warning text-da-warning-fg",
};

const DEFAULT_SECTIONS: SidebarConversationsSection[] = [
  { title: "Spaces", items: [], empty: "No spaces yet", defaultOpen: true },
  {
    title: "Direct",
    defaultOpen: true,
    items: [
      { label: "Director", href: "#director", initials: "D", tone: "accent" },
      { label: "Research", href: "#research", initials: "R", tone: "primary", unread: true },
      { label: "Copywriter", href: "#copy", initials: "C", tone: "success" },
    ],
  },
  {
    title: "Conversations",
    defaultOpen: false,
    items: [
      { label: "Onboarding redesign recap", href: "#c1" },
      { label: "Q4 pricing experiments", href: "#c2" },
      { label: "Release notes — v4.2", href: "#c3" },
    ],
  },
];

/** AI chat app shell: dark sidebar with brand + new-chat button, a studio link, collapsible sections (spaces, direct agents, conversations) with + actions, user footer; slides in as a drawer on mobile. */
export function SidebarConversations({
  brand = "Nova",
  studio = { label: "Agent Studio", href: "#studio" },
  sections = DEFAULT_SECTIONS,
  activeHref = "#director",
  user = { name: "Chloé Martin", initials: "CM", plan: "Pro plan" },
  children,
  className,
}: SidebarConversationsProps) {
  const [open, setOpen] = useState(false);
  const panelId = useId();

  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open]);

  const iconButton = "da-focus da-transition grid size-8 place-items-center rounded-da-sm text-da-muted-fg hover:bg-da-muted hover:text-da-fg";

  return (
    <div className={cn("relative flex min-h-dvh", className)}>
      <div
        aria-hidden
        onClick={() => setOpen(false)}
        className={cn("da-transition fixed inset-0 z-30 bg-da-overlay lg:hidden", open ? "opacity-100" : "pointer-events-none opacity-0")}
      />
      <aside
        id={panelId}
        aria-label="Conversations"
        className={cn(
          "da-transition-slow fixed inset-y-0 left-0 z-40 flex w-64 flex-col border-r border-da-border bg-da-bg lg:sticky lg:top-0 lg:h-dvh lg:translate-x-0",
          open ? "translate-x-0" : "-translate-x-full",
        )}
      >
        <div className="flex h-14 items-center gap-2.5 px-4">
          <span aria-hidden className="grid size-7 place-items-center rounded-full bg-gradient-to-br from-da-primary to-da-accent">
            <span className="size-2.5 rounded-full bg-da-bg" />
          </span>
          <span className="flex-1 font-da-display text-[15px] font-semibold">{brand}</span>
          <a href="#new" aria-label="New chat" className={iconButton}>
            <SquarePen aria-hidden className="size-4" />
          </a>
          <button type="button" aria-label="Close sidebar" onClick={() => setOpen(false)} className={cn(iconButton, "lg:hidden")}>
            <X aria-hidden className="size-4" />
          </button>
        </div>

        <nav aria-label="Workspace" className="flex-1 overflow-y-auto px-2 pb-4">
          <a href={studio.href} className="da-focus da-transition flex items-center gap-2.5 rounded-da-sm px-2.5 py-2 text-sm text-da-fg hover:bg-da-muted">
            <Palette aria-hidden className="size-4 text-da-muted-fg" />
            {studio.label}
          </a>

          {sections.map((s) => (
            <details key={s.title} open={s.defaultOpen ?? true} className="group mt-5">
              <summary className="da-focus flex cursor-pointer list-none items-center gap-1 rounded-da-sm px-2 py-1 font-da-mono text-[11px] tracking-da-label text-da-muted-fg uppercase hover:text-da-fg [&::-webkit-details-marker]:hidden">
                <ChevronDown aria-hidden className="da-transition size-3 -rotate-90 group-open:rotate-0" />
                <span className="flex-1">{s.title}</span>
                <span aria-hidden className="grid size-5 place-items-center rounded-da-sm hover:bg-da-muted">
                  <Plus className="size-3.5" />
                </span>
              </summary>
              {s.items.length === 0 ? (
                <p className="px-2.5 py-1.5 text-xs text-da-muted-fg">{s.empty ?? "Nothing here yet"}</p>
              ) : (
                <ul className="mt-1 space-y-0.5">
                  {s.items.map((it) => {
                    const active = it.href === activeHref;
                    return (
                      <li key={it.href}>
                        <a
                          href={it.href}
                          aria-current={active ? "page" : undefined}
                          onClick={() => setOpen(false)}
                          className={cn(
                            "da-focus da-transition flex items-center gap-2.5 rounded-da-pill px-2.5 py-1.5 text-sm",
                            active ? "bg-da-surface-2 text-da-fg" : "text-da-fg/80 hover:bg-da-muted hover:text-da-fg",
                          )}
                        >
                          {it.initials && (
                            <span aria-hidden className={cn("grid size-5 shrink-0 place-items-center rounded-full font-da-display text-[10px] font-semibold", TONES[it.tone ?? "primary"])}>
                              {it.initials}
                            </span>
                          )}
                          <span className="min-w-0 flex-1 truncate">{it.label}</span>
                          {it.unread && (
                            <span className="size-1.5 rounded-full bg-da-primary">
                              <span className="sr-only">unread</span>
                            </span>
                          )}
                        </a>
                      </li>
                    );
                  })}
                </ul>
              )}
            </details>
          ))}
        </nav>

        <div className="border-t border-da-border p-2">
          <a href="#account" className="da-focus da-transition flex items-center gap-2.5 rounded-da-md p-2 hover:bg-da-muted">
            <span aria-hidden className="grid size-8 place-items-center rounded-full bg-da-accent font-da-display text-xs font-semibold text-da-accent-fg">
              {user.initials}
            </span>
            <span className="min-w-0">
              <span className="block truncate text-sm font-medium">{user.name}</span>
              <span className="block text-xs text-da-muted-fg">{user.plan}</span>
            </span>
          </a>
        </div>
      </aside>

      <div className="flex min-w-0 flex-1 flex-col">
        <div className="flex h-14 items-center gap-2 border-b border-da-border px-3 lg:hidden">
          <button type="button" aria-label="Open sidebar" aria-expanded={open} aria-controls={panelId} onClick={() => setOpen(true)} className={iconButton}>
            <Menu aria-hidden className="size-5" />
          </button>
          <span className="font-da-display text-[15px] font-semibold">{brand}</span>
        </div>
        {children}
      </div>
    </div>
  );
}

export default SidebarConversations;

modules/nocturne/layout/sidebar-conversations/index.tsx

Props

PropTypeDefaultDescription
brandstring—Brand name.
studio{ label, href }—Top link.
sections{ title, items: { label, href, initials?, tone?, unread? }[], empty?, defaultOpen? }[]—Sections.
activeHrefstring—Current item.
user{ name, initials, plan }—Footer account.
childrenReactNode—Main content.

Sidebar in other art directions

App Sidebar

App shell with a sticky left sidebar: workspace header, grouped nav with yellow active state and counters, usage meter, user footer. Becomes a top bar + slide-over drawer below lg.

BrutalistSidebar

Icon Rail

Compact 80px icon rail: ink brand square, square icon links with yellow active state, red counters and ink tooltips on hover/focus, settings pinned at the bottom. Server-safe, CSS-only tooltips.

BrutalistSidebar

Sidebar Collapsible

App sidebar that collapses from 256px to a 76px icon column with a stepped width animation; ink active item, labels become sr-only when collapsed, collapse toggle pinned at the bottom.

BrutalistSidebar

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.

BrutalistSidebar

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