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 railnever 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>
)
}