Design Library

Security console - counted filter bar

console-counted-filters.tsx

componentskill: emil-design-engdashboardsecurityfilterstabledense

Variants of security-console-2026-q3: console-counted-rail, 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
a filter bar that tells you what a click will show before you click it. the three metric cards this replaced restated the page title, the nav count and one severity slice, and cost a full band of vertical space above the data
Guardrails
always a count on every filter, press feedback at 140ms on a strong ease-out, one selected treatment shared by chips, tabs and rows
never animation on a keyboard-repeated action, a metric card that repeats a number already on screen, a second accent for the selected state

Install

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

Source

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

import { Search } from 'lucide-react'
import { useMemo, useState } from 'react'

type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'
type Filter = Severity | 'all'

const CHIP: Record<Severity, string> = {
  critical: 'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-400',
  high: 'border-orange-500/30 bg-orange-500/10 text-orange-700 dark:text-orange-400',
  medium: 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400',
  low: 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-400',
  info: 'border-zinc-500/30 bg-zinc-500/10 text-zinc-600 dark:text-zinc-400',
}

const FILTERS: Filter[] = ['all', 'critical', 'high', 'medium', 'low', 'info']

type Row = { name: string; template: string; host: string; sev: Severity; seen: string }

const ROWS: Row[] = [
  {
    name: 'unsafe random function in boundary generation',
    template: 'CVE-2025-7783',
    host: 'preview.komplyor.fr',
    sev: 'critical',
    seen: '07-10 11:27',
  },
  {
    name: 'SSRF in IOFactory::load when filename is user controlled',
    template: 'CVE-2026-34084',
    host: 'media.changevivienne.com',
    sev: 'critical',
    seen: '07-10 11:04',
  },
  {
    name: 'sandbox bypass in twig 2.16.x and 3.9.0 through 3.25.x',
    template: 'CVE-2026-24425',
    host: 'old.pma.changevivienne.com',
    sev: 'high',
    seen: '07-06 06:22',
  },
  {
    name: 'mod_proxy request smuggling',
    template: 'apache-httpd-smuggling',
    host: 'www.changevivienne.com',
    sev: 'high',
    seen: '07-06 06:21',
  },
  {
    name: 'TLS certificate expires in under 14 days',
    template: 'ssl-expiry-window',
    host: 'astraea-voyance.com',
    sev: 'medium',
    seen: '07-02 07:57',
  },
  {
    name: 'directory listing enabled on static asset path',
    template: 'dir-listing',
    host: 'bijouxdantan.fr',
    sev: 'low',
    seen: '06-29 02:38',
  },
  {
    name: 'server banner discloses exact patch version',
    template: 'banner-disclosure',
    host: 'godotcap.com',
    sev: 'info',
    seen: '06-29 02:31',
  },
]

/** Strong ease-out. The built-in CSS curves are too weak to read as intended,
 *  and ease-in on a control feels broken because it withholds the first frame,
 *  which is the frame the user is watching. */
const EASE = 'ease-[cubic-bezier(0.23,1,0.32,1)]'

function Chip({
  active,
  count,
  onClick,
  children,
}: {
  active: boolean
  count: number
  onClick: () => void
  children: React.ReactNode
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-pressed={active}
      className={[
        'inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-2.5 py-1 text-sm capitalize',
        'transition-[color,background-color,border-color,transform] duration-[140ms]',
        EASE,
        'active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100',
        active
          ? 'border-teal-500/40 bg-teal-500/10 text-teal-700 dark:text-teal-300'
          : 'border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground',
      ].join(' ')}
    >
      {children}
      <span className="text-[11px] tabular-nums">{count.toLocaleString('en-US')}</span>
    </button>
  )
}

export default function ConsoleCountedFilters() {
  const [filter, setFilter] = useState<Filter>('all')
  const [query, setQuery] = useState('')

  const counts = useMemo(() => {
    const base: Record<Filter, number> = {
      all: ROWS.length,
      critical: 0,
      high: 0,
      medium: 0,
      low: 0,
      info: 0,
    }
    for (const row of ROWS) base[row.sev] += 1
    return base
  }, [])

  const rows = useMemo(() => {
    const q = query.trim().toLowerCase()
    return ROWS.filter((r) => filter === 'all' || r.sev === filter).filter(
      (r) => !q || r.name.toLowerCase().includes(q) || r.host.toLowerCase().includes(q),
    )
  }, [filter, query])

  return (
    <div className="min-h-[100dvh] bg-background p-4 text-foreground sm:p-8">
      <div className="mx-auto max-w-5xl">
        <header className="mb-7 flex flex-wrap items-start justify-between gap-x-6 gap-y-3">
          <div className="min-w-0">
            <h1 className="text-xl font-semibold tracking-tight">Vulnerabilities</h1>
            <p className="mt-1.5 max-w-[68ch] text-sm text-muted-foreground">
              Findings from the nuclei engine.
            </p>
          </div>
          <span className="shrink-0 text-sm text-muted-foreground">
            <span className="font-medium tabular-nums text-red-700 dark:text-red-400">47</span>{' '}
            affected services
          </span>
        </header>

        <div className="mb-4 flex flex-wrap items-center gap-2">
          {FILTERS.map((f) => (
            <Chip
              key={f}
              active={filter === f}
              count={counts[f]}
              onClick={() => setFilter(f)}
            >
              {f}
            </Chip>
          ))}
          <div className="relative ml-auto">
            <Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
            <input
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="Search findings"
              aria-label="Search findings"
              className={`h-9 w-56 rounded-md border border-input bg-transparent pl-8 pr-3 text-sm transition-[border-color] duration-[140ms] ${EASE} placeholder:text-muted-foreground focus-visible:border-teal-500/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500 motion-reduce:transition-none`}
            />
          </div>
        </div>

        <div className="overflow-hidden rounded-xl border border-border/70 bg-card">
          <div className="overflow-x-auto">
            <table className="w-full caption-bottom text-sm">
              <thead>
                <tr className="border-b border-border">
                  <th className="h-9 px-3 text-left text-xs font-medium text-muted-foreground">
                    Severity
                  </th>
                  <th className="h-9 px-3 text-left text-xs font-medium text-muted-foreground">
                    Finding
                  </th>
                  <th className="h-9 px-3 text-left text-xs font-medium text-muted-foreground">
                    Host
                  </th>
                  <th className="h-9 px-3 text-left text-xs font-medium text-muted-foreground">
                    Template
                  </th>
                  <th className="h-9 px-3 text-right text-xs font-medium text-muted-foreground">
                    Last seen
                  </th>
                </tr>
              </thead>
              <tbody>
                {rows.map((r) => (
                  <tr
                    key={r.template + r.host}
                    className={`border-b border-border/60 transition-colors duration-[120ms] ${EASE} last:border-0 hover:bg-muted/40 motion-reduce:transition-none`}
                  >
                    <td className="px-3 py-2.5 align-middle">
                      <span
                        className={`inline-flex items-center rounded-md border px-1.5 py-0.5 text-[11px] font-medium uppercase tracking-wide ${CHIP[r.sev]}`}
                      >
                        {r.sev}
                      </span>
                    </td>
                    <td className="max-w-[22rem] truncate px-3 py-2.5 align-middle">{r.name}</td>
                    <td className="px-3 py-2.5 align-middle font-mono text-xs text-muted-foreground">
                      {r.host}
                    </td>
                    <td className="px-3 py-2.5 align-middle">
                      <span className="rounded border border-border/70 bg-muted/40 px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">
                        {r.template}
                      </span>
                    </td>
                    <td className="whitespace-nowrap px-3 py-2.5 text-right align-middle text-xs tabular-nums text-muted-foreground">
                      {r.seen}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>

          {rows.length === 0 && (
            <div className="flex flex-col items-center gap-2 px-6 py-10 text-center">
              <p className="text-sm font-medium">No findings match</p>
              <p className="text-xs text-muted-foreground">
                Clear the search, or widen the severity filter.
              </p>
            </div>
          )}
        </div>
      </div>
    </div>
  )
}