Sections

Command Bar Shell

App shell chrome: workspace switcher dropdown, centered pseudo command input with ⌘K hint, and an icon cluster with a notification badge.

Install

npx shadcn@latest add @paragon/command-bar-shell

command-bar-shell.tsx

"use client";

import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { motion, useReducedMotion } from "motion/react";
import {
  Bell,
  Check,
  ChevronsUpDown,
  CircleHelp,
  Plus,
  Search,
} from "lucide-react";
import { cn } from "@/lib/utils";

/* ------------------------------------------------------------------ */

interface Workspace {
  id: string;
  name: string;
  plan: string;
  hue: string;
}

const WORKSPACES: Workspace[] = [
  {
    id: "w1",
    name: "meridian-labs",
    plan: "Scale",
    hue: "bg-violet-600 text-white",
  },
  {
    id: "w2",
    name: "meridian-staging",
    plan: "Pro",
    hue: "bg-sky-600 text-white",
  },
  {
    id: "w3",
    name: "priya-sandbox",
    plan: "Free",
    hue: "bg-emerald-600 text-white",
  },
];

function IconButton({
  label,
  className,
  children,
  ...props
}: { label: string } & React.ComponentProps<"button">) {
  return (
    <button
      type="button"
      aria-label={label}
      className={cn(
        "pressable relative inline-flex size-8 items-center justify-center rounded-lg text-muted-foreground",
        "transition-[color,background-color] duration-150 ease-out hover:bg-muted hover:text-foreground",
        "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
        "after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2",
        className,
      )}
      {...props}
    >
      {children}
    </button>
  );
}

export interface CommandBarShellProps extends React.ComponentProps<"div"> {
  /** Unread notification count on the bell. */
  notificationCount?: number;
}

/**
 * App shell chrome: a top bar with workspace switcher (origin-aware dropdown),
 * a centered pseudo command input advertising ⌘K, and a right-side icon
 * cluster with a notification badge that pops in once. Below sits a faint
 * placeholder canvas standing in for the content area.
 */
export function CommandBarShell({
  notificationCount = 3,
  className,
  ...props
}: CommandBarShellProps) {
  const reducedMotion = useReducedMotion();
  const uid = React.useId().replace(/[^a-zA-Z0-9-]/g, "");
  const [workspace, setWorkspace] = React.useState<Workspace>(WORKSPACES[0]);
  const menuClass = `menu-content-${uid}`;

  return (
    <div
      className={cn(
        "w-full overflow-hidden rounded-xl bg-background shadow-border",
        className,
      )}
      {...props}
    >
      <style href={`paragon-command-bar-shell-${uid}`} precedence="paragon">{`
        @keyframes menu-in-${uid} {
          from { opacity: 0; transform: scale(0.97); }
        }
        .${menuClass} {
          transform-origin: var(--radix-dropdown-menu-content-transform-origin);
          animation: menu-in-${uid} 150ms var(--ease-out);
        }
        @media (prefers-reduced-motion: reduce) {
          .${menuClass} { animation: none; }
        }
      `}</style>

      {/* Top bar */}
      <header className="flex h-13 items-center gap-2 border-b px-3 sm:gap-3 sm:px-4">
        {/* Product mark + workspace switcher */}
        <div className="flex min-w-0 items-center gap-1.5">
          <div
            aria-hidden
            className="flex size-6 shrink-0 items-center justify-center rounded-md bg-primary text-[11px] font-bold text-primary-foreground"
          >
            R
          </div>
          <DropdownMenuPrimitive.Root modal={false}>
            <DropdownMenuPrimitive.Trigger asChild>
              <button
                type="button"
                className={cn(
                  "flex h-8 min-w-0 items-center gap-1.5 rounded-lg px-2 text-sm font-medium",
                  "transition-[background-color] duration-150 ease-out hover:bg-muted",
                  "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
                )}
              >
                <span className="truncate">{workspace.name}</span>
                <span className="hidden rounded-full bg-muted px-1.5 py-px text-[11px] font-medium text-muted-foreground sm:inline">
                  {workspace.plan}
                </span>
                <ChevronsUpDown
                  aria-hidden
                  className="size-3.5 shrink-0 text-muted-foreground"
                />
              </button>
            </DropdownMenuPrimitive.Trigger>
            <DropdownMenuPrimitive.Portal>
              <DropdownMenuPrimitive.Content
                align="start"
                sideOffset={6}
                className={cn(
                  "z-50 w-60 rounded-lg bg-popover p-1 text-popover-foreground shadow-overlay",
                  menuClass,
                )}
              >
                <DropdownMenuPrimitive.Label className="px-2.5 pt-1.5 pb-1 text-xs font-medium text-muted-foreground">
                  Workspaces
                </DropdownMenuPrimitive.Label>
                {WORKSPACES.map((ws) => (
                  <DropdownMenuPrimitive.Item
                    key={ws.id}
                    onSelect={() => setWorkspace(ws)}
                    className={cn(
                      "flex cursor-default items-center gap-2.5 rounded-md px-2.5 py-2 text-sm outline-none select-none",
                      "transition-[background-color] duration-150 ease-out data-highlighted:bg-muted",
                    )}
                  >
                    <span
                      aria-hidden
                      className={cn(
                        "flex size-5 shrink-0 items-center justify-center rounded text-[10px] font-bold",
                        ws.hue,
                      )}
                    >
                      {ws.name.charAt(0).toUpperCase()}
                    </span>
                    <span className="min-w-0 flex-1 truncate font-medium">
                      {ws.name}
                    </span>
                    {ws.id === workspace.id && (
                      <Check aria-hidden className="size-3.5 shrink-0" />
                    )}
                  </DropdownMenuPrimitive.Item>
                ))}
                <DropdownMenuPrimitive.Separator className="mx-1 my-1 h-px bg-border" />
                <DropdownMenuPrimitive.Item
                  className={cn(
                    "flex cursor-default items-center gap-2.5 rounded-md px-2.5 py-2 text-sm text-muted-foreground outline-none select-none",
                    "transition-[background-color,color] duration-150 ease-out data-highlighted:bg-muted data-highlighted:text-foreground",
                  )}
                >
                  <Plus aria-hidden className="size-4 shrink-0" />
                  Create workspace
                </DropdownMenuPrimitive.Item>
              </DropdownMenuPrimitive.Content>
            </DropdownMenuPrimitive.Portal>
          </DropdownMenuPrimitive.Root>
        </div>

        {/* Centered pseudo command input */}
        <div className="flex min-w-0 flex-1 justify-center">
          <button
            type="button"
            aria-label="Open command menu"
            aria-keyshortcuts="Meta+K"
            className={cn(
              "hidden h-8 w-full max-w-xs items-center gap-2 rounded-lg border border-input px-2.5 text-sm text-muted-foreground sm:flex",
              "transition-[border-color,background-color] duration-150 ease-out hover:bg-muted/50",
              "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
            )}
          >
            <Search aria-hidden className="size-3.5 shrink-0" />
            <span className="flex-1 truncate text-left">
              Search or jump to…
            </span>
            <kbd className="rounded border bg-muted px-1 font-sans text-[11px] text-muted-foreground">
              ⌘K
            </kbd>
          </button>
          <IconButton label="Search" className="sm:hidden">
            <Search aria-hidden className="size-4" />
          </IconButton>
        </div>

        {/* Right cluster */}
        <div className="flex shrink-0 items-center gap-0.5 sm:gap-1">
          <IconButton label="Help and documentation">
            <CircleHelp aria-hidden className="size-4" />
          </IconButton>
          <IconButton
            label={`Notifications, ${notificationCount} unread`}
            className="relative"
          >
            <Bell aria-hidden className="size-4" />
            {notificationCount > 0 && (
              <motion.span
                aria-hidden
                initial={
                  reducedMotion ? { opacity: 0 } : { opacity: 0, scale: 0.5 }
                }
                animate={{ opacity: 1, scale: 1 }}
                transition={{
                  delay: 0.35,
                  type: "spring",
                  duration: 0.35,
                  bounce: 0.2,
                }}
                className="absolute top-0.5 right-0.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-destructive px-0.5 text-[9px] font-semibold text-destructive-foreground tabular-nums"
              >
                {notificationCount}
              </motion.span>
            )}
          </IconButton>
          <button
            type="button"
            aria-label="Account menu for Priya Raman"
            className={cn(
              "pressable relative ml-1 flex size-7 items-center justify-center rounded-full bg-violet-600/10 text-[11px] font-semibold text-violet-700 dark:bg-violet-400/10 dark:text-violet-300",
              "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
              "after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2",
            )}
          >
            PR
          </button>
        </div>
      </header>

      {/* Placeholder canvas */}
      <div aria-hidden className="p-4 sm:p-6">
        <div className="flex h-56 flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/20">
          <div className="grid w-40 gap-2 opacity-60">
            <div className="h-2 w-3/4 rounded-full bg-muted-foreground/20" />
            <div className="h-2 w-full rounded-full bg-muted-foreground/15" />
            <div className="h-2 w-1/2 rounded-full bg-muted-foreground/10" />
          </div>
          <p className="text-xs text-muted-foreground/70">
            Content area
          </p>
        </div>
      </div>
    </div>
  );
}