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.
glass/layout/sidebar-channelsSource
"use client";
import { useEffect, useId, useState, type ReactNode } from "react";
import { ChevronDown, Hash, Lock, Menu, Plus, Video, X } from "lucide-react";
import { cn } from "@/lib/utils";
export interface SidebarChannelsWorkspace {
name: string;
initials: string;
tone: string;
}
export interface SidebarChannelsSection {
title: string;
items: { label: string; href: string; private?: boolean; unread?: number; live?: boolean }[];
}
export interface SidebarChannelsProps {
workspaces?: SidebarChannelsWorkspace[];
activeWorkspace?: number;
sections?: SidebarChannelsSection[];
activeHref?: string;
children?: ReactNode;
className?: string;
}
const GLASS = "da-stroke bg-da-surface backdrop-blur-da backdrop-saturate-150";
const DEFAULT_SECTIONS: SidebarChannelsSection[] = [
{
title: "Recap channels",
items: [
{ label: "product", href: "#product", unread: 3 },
{ label: "design-crit", href: "#design" },
{ label: "sales-calls", href: "#sales", unread: 12 },
{ label: "leadership", href: "#leadership", private: true },
],
},
{
title: "Live now",
items: [
{ label: "Customer call — Parcel", href: "#live-parcel", live: true },
{ label: "Standup · Mobile", href: "#live-standup", live: true },
],
},
];
/** Two-column chat-app sidebar: a workspace switcher column of gradient squircles plus a glass channel list with sections, unread counts and live meetings; drawer on mobile. */
export function SidebarChannels({
workspaces = [
{ name: "Lumen", initials: "Lu", tone: "from-da-primary to-da-accent" },
{ name: "Parcel", initials: "Pa", tone: "from-da-danger to-da-primary" },
{ name: "Side project", initials: "Sp", tone: "from-da-accent to-da-success" },
],
activeWorkspace = 0,
sections = DEFAULT_SECTIONS,
activeHref = "#product",
children,
className,
}: SidebarChannelsProps) {
const [open, setOpen] = useState(false);
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
const id = 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 ws = workspaces[activeWorkspace] ?? workspaces[0];
return (
<div className={cn("flex min-h-dvh text-da-fg", className)}>
{open && <div aria-hidden className="fixed inset-0 z-40 bg-da-overlay lg:hidden" onClick={() => setOpen(false)} />}
<aside
id={id}
className={cn(
"fixed inset-y-0 left-0 z-50 flex p-3 transition-transform duration-(--da-duration-slow) ease-da lg:sticky lg:top-0 lg:h-dvh lg:translate-x-0 motion-reduce:transition-none",
open ? "translate-x-0" : "-translate-x-full",
)}
>
<nav aria-label="Workspaces" className="flex flex-col items-center gap-2 pr-3">
{workspaces.map((w, i) => (
<a
key={w.name}
href={`#ws-${i}`}
aria-label={w.name}
aria-current={i === activeWorkspace ? "true" : undefined}
className={cn(
"da-focus da-transition grid size-11 place-items-center rounded-da-md bg-gradient-to-br font-da-display text-sm font-semibold text-da-primary-fg shadow-da-sm hover:rounded-da-sm",
w.tone,
i === activeWorkspace && "ring-2 ring-da-fg ring-offset-2 ring-offset-da-bg",
)}
>
{w.initials}
</a>
))}
<button type="button" aria-label="Add workspace" className={cn("da-focus grid size-11 place-items-center rounded-da-md text-da-muted-fg", GLASS)}>
<Plus aria-hidden className="size-5" />
</button>
</nav>
<div className={cn("flex w-60 flex-col rounded-da-lg p-3 shadow-da-lg", GLASS)}>
<div className="flex items-center justify-between px-2 py-1.5">
<p className="font-da-display font-semibold">{ws?.name}</p>
<button
type="button"
onClick={() => setOpen(false)}
aria-label="Close navigation"
className="da-focus grid size-7 place-items-center rounded-full lg:hidden"
>
<X aria-hidden className="size-4" />
</button>
</div>
<nav aria-label="Channels" className="mt-3 flex-1 space-y-4 overflow-y-auto">
{sections.map((s) => {
const isCollapsed = collapsed[s.title];
return (
<div key={s.title}>
<button
type="button"
aria-expanded={!isCollapsed}
onClick={() => setCollapsed((c) => ({ ...c, [s.title]: !c[s.title] }))}
className="da-focus flex w-full items-center gap-1 rounded-da-sm px-2 py-1 text-xs font-semibold text-da-muted-fg hover:text-da-fg"
>
<ChevronDown aria-hidden className={cn("da-transition size-3.5", isCollapsed && "-rotate-90")} />
{s.title}
</button>
<ul hidden={isCollapsed} className="mt-1 space-y-0.5">
{s.items.map((it) => {
const active = it.href === activeHref;
const Icon = it.live ? Video : it.private ? Lock : Hash;
return (
<li key={it.href}>
<a
href={it.href}
aria-current={active ? "page" : undefined}
className={cn(
"da-focus da-transition flex items-center gap-2 rounded-da-sm px-2 py-1.5 text-sm",
active ? "bg-gradient-to-r from-da-primary to-da-accent font-semibold text-da-primary-fg" : "hover:bg-da-surface",
!active && it.unread && "font-semibold",
)}
>
<Icon aria-hidden className={cn("size-4 shrink-0", !active && (it.live ? "text-da-danger" : "text-da-muted-fg"))} />
<span className="truncate">{it.label}</span>
{it.live && <span className="ml-auto rounded-da-pill bg-da-danger px-1.5 text-[10px] font-semibold text-da-danger-fg">LIVE</span>}
{it.unread !== undefined && (
<span
className={cn(
"ml-auto rounded-da-pill px-1.5 text-[11px] font-semibold",
active ? "bg-da-primary-fg/25" : "bg-da-primary text-da-primary-fg",
)}
>
{it.unread}
<span className="sr-only"> unread</span>
</span>
)}
</a>
</li>
);
})}
</ul>
</div>
);
})}
</nav>
</div>
</aside>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 px-4 pt-4 lg:hidden">
<button
type="button"
onClick={() => setOpen(true)}
aria-expanded={open}
aria-controls={id}
aria-label="Open navigation"
className={cn("da-focus grid size-10 place-items-center rounded-full", GLASS)}
>
<Menu aria-hidden className="size-5" />
</button>
</div>
<main>{children}</main>
</div>
</div>
);
}
export default SidebarChannels;
modules/glass/layout/sidebar-channels/index.tsx
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| workspaces | { name, initials, tone }[] | — | Switcher (tone = gradient classes). |
| activeWorkspace | number | 0 | Index. |
| sections | { title, items: { label, href, private?, unread?, live? }[] }[] | — | Channel groups. |
| activeHref | string | — | Current channel. |
| children | ReactNode | — | Content. |
| className | string | — | Classes. |
Other sidebar variants in Glass
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.