Design Library

Security console - counted nav rail

console-counted-rail.tsx

componentskill: emil-design-engnavigationsidebarsecuritydashboarddense

Variants of security-console-2026-q3: console-counted-filters, console-severity-ledger

Brief

The prompt that produced this. Re-run it to generate more in the same taste.

Aesthetic
Dark-first security console - cool slate, one teal accent, tinted-not-solid chips, tabular numerals, hairline separation, mono for machine strings, 8px controls and 14px containers
Reference
https://linear.app
Intent
twelve destinations that each answer "how much is in there" before you navigate. the rail is hit dozens of times a day, so it is the one surface that must not move under the pointer
Guardrails
always a live count where the destination has one, colour-only transitions on the nav items, the accent on the active item, never on the hover, one primary action, pinned to the foot of the rail
never transform or motion on a high-frequency target, a coloured left border thicker than a hairline, a section eyebrow over every group

Install

pnpm dlx shadcn@latest add http://localhost:3040/r/console-counted-rail.json

Source

Meta block and library type import stripped - this is what pastes cleanly elsewhere.

import {
  BarChart3,
  Boxes,
  Database,
  Globe,
  KeyRound,
  LayoutGrid,
  Network,
  Play,
  Plug,
  ScanLine,
  Settings,
  ShieldAlert,
  Users,
} from 'lucide-react'
import { useState } from 'react'

type Item = { label: string; icon: typeof LayoutGrid; count?: number }

/**
 * Grouped by hairline, not by eyebrow. A tracked uppercase label over every
 * group is grammar nobody chose; the gap already says these belong together.
 */
const GROUPS: Item[][] = [
  [{ label: 'Dashboard', icon: LayoutGrid }],
  [
    { label: 'Vulnerabilities', icon: ShieldAlert, count: 89764 },
    { label: 'Scans', icon: ScanLine, count: 63 },
    { label: 'Inventory', icon: Database, count: 142 },
    { label: 'Asset groups', icon: Boxes, count: 15 },
    { label: 'Application map', icon: Network },
  ],
  [
    { label: 'Reports', icon: BarChart3 },
    { label: 'People', icon: Users, count: 1 },
    { label: 'Domain monitoring', icon: Globe },
    { label: 'Credential monitoring', icon: KeyRound },
    { label: 'Integrations', icon: Plug },
  ],
]

const EASE = 'ease-[cubic-bezier(0.23,1,0.32,1)]'

export default function ConsoleCountedRail() {
  const [active, setActive] = useState('Vulnerabilities')

  return (
    <div className="min-h-[100dvh] bg-background text-foreground">
      {/* A 240px rail plus content does not fit a phone. Rather than
          hiding the design behind a drawer, it stacks: the rail keeps its
          counts and sits above the surface it navigates. */}
      <div className="mx-auto flex max-w-5xl flex-col gap-6 p-4 sm:flex-row sm:p-8">
        <aside className="flex w-full shrink-0 flex-col gap-4 rounded-xl border border-border/70 bg-card p-3 sm:w-60">
          <div className="flex items-baseline gap-1.5 px-2 pt-1">
            <span className="text-sm font-semibold tracking-tight">asm</span>
            <span className="text-[10px] leading-none text-muted-foreground">
              attack surface monitor
            </span>
          </div>

          <nav className="flex flex-1 flex-col gap-4">
            {GROUPS.map((group, gi) => (
              <div key={group[0].label} className="flex flex-col gap-0.5">
                {gi > 0 && <div className="mx-2 mb-2 border-t border-border/60" />}
                {group.map(({ label, icon: Icon, count }) => {
                  const on = active === label
                  return (
                    <button
                      key={label}
                      type="button"
                      onClick={() => setActive(label)}
                      aria-current={on || undefined}
                      // Colour only, no transform. These are hit dozens of
                      // times a day and a target that shifts under the pointer
                      // reads as lag, not as polish.
                      className={[
                        'group flex cursor-pointer items-center gap-2.5 rounded-md px-3 py-2 text-left text-sm',
                        'transition-colors duration-[140ms]',
                        EASE,
                        'motion-reduce:transition-none',
                        on
                          ? 'bg-teal-500/10 text-foreground'
                          : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground',
                      ].join(' ')}
                    >
                      <Icon
                        className={`size-4 shrink-0 transition-colors duration-[140ms] ${EASE} motion-reduce:transition-none ${
                          on ? 'text-teal-600 dark:text-teal-400' : ''
                        }`}
                      />
                      <span className="flex-1 truncate">{label}</span>
                      {count !== undefined && (
                        <span className="min-w-5 rounded-full bg-muted/70 px-1.5 text-center text-[11px] tabular-nums text-muted-foreground">
                          {count.toLocaleString('en-US')}
                        </span>
                      )}
                    </button>
                  )
                })}
              </div>
            ))}
          </nav>

          <div className="flex flex-col gap-3">
            <button
              type="button"
              className={[
                'flex cursor-pointer items-center gap-2.5 rounded-md px-3 py-2 text-left text-sm text-muted-foreground',
                'transition-colors duration-[140ms]',
                EASE,
                'hover:bg-muted/50 hover:text-foreground motion-reduce:transition-none',
              ].join(' ')}
            >
              <Settings className="size-4 shrink-0" />
              <span className="flex-1">Settings</span>
            </button>

            {/* The one primary action. It is pressed rarely enough to earn a
                press state that the nav items above deliberately refuse. */}
            <button
              type="button"
              className={[
                'inline-flex h-9 cursor-pointer items-center justify-center gap-2 rounded-md',
                'bg-teal-600 text-sm font-medium text-white dark:bg-teal-500 dark:text-teal-950',
                'transition-[background-color,transform] duration-[140ms]',
                EASE,
                'hover:bg-teal-500 active:scale-[0.97] dark:hover:bg-teal-400',
                'motion-reduce:transition-none motion-reduce:active:scale-100',
              ].join(' ')}
            >
              <Play className="size-4" />
              Run scan
            </button>

            <p className="px-3 text-[11px] text-muted-foreground">self-hosted ยท nuclei engine</p>
          </div>
        </aside>

        <section className="min-w-0 flex-1">
          <h1 className="text-xl font-semibold tracking-tight">{active}</h1>
          <p className="mt-1.5 max-w-[68ch] text-sm text-muted-foreground">
            The rail carries the counts so the page does not have to open with a row of metric
            cards restating them.
          </p>
          <div className="mt-6 rounded-xl border border-dashed border-border/70 px-6 py-16 text-center text-sm text-muted-foreground">
            {active}
          </div>
        </section>
      </div>
    </div>
  )
}