Rams MCP · The full engine, now in your coding agent
keephq on GitHub

keephq/keep

Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.

30 files reviewed·July 9, 2026

Elevated

Design risk in this codebase.

6issues
Critical & Serious

Top fix

Add role, tabIndex, and keyboard handling to category filter chips

See the fix

Verdict

Interactive controls repeatedly fake button semantics instead of using real ones, locking out keyboard and screen-reader users across filters and widgets. The bigger risk is what happens off the happy path: bad URLs and slow loads collapse to silent blank screens instead of feedback.

Files Rams reviewed

keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx

keep-ui/app/(health)/layout.tsx

keep-ui/app/(keep)/dashboard/GridLayout.tsx

keep-ui/app/(keep)/extraction/[rule_id]/executions/[execution_id]/page.tsx

keep-ui/app/(keep)/extraction/[rule_id]/executions/page.tsx

keep-ui/app/(keep)/incidents/[id]/topology/page.tsx

keep-ui/app/(keep)/layout.tsx

keep-ui/app/(keep)/mapping/[rule_id]/executions/[execution_id]/page.tsx

keep-ui/app/(keep)/mapping/[rule_id]/executions/page.tsx

keep-ui/app/(keep)/providers/layout.tsx

keep-ui/app/(keep)/providers/oauth2/[providerType]/page.tsx

keep-ui/app/(keep)/topology/page.tsx

keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-page.tsx

keep-ui/app/(keep)/workflows/preview/[workflowId]/page.tsx

keep-ui/app/(keep)/workflows/preview/page.tsx

keep-ui/app/(signin)/layout.tsx

keep-ui/app/(signin)/mobile/page.tsx

keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx

keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx

keep-ui/app/(health)/health/page.tsx

keep-ui/app/(keep)/[...not-found]/page.tsx

keep-ui/app/(keep)/ai/page.tsx

keep-ui/app/(keep)/alerts/[id]/page.tsx

keep-ui/app/(keep)/alerts/fingerprint/[fp]/page.tsx

keep-ui/app/(keep)/alerts/fingerprint/[fp]/ui/alert-fingerprint-page.tsx

keep-ui/app/(keep)/dashboard/[id]/page.tsx

keep-ui/app/(keep)/deduplication/page.tsx

keep-ui/app/(keep)/extraction/layout.tsx

keep-ui/app/(keep)/extraction/page.tsx

keep-ui/app/(keep)/incidents/[id]/activity/page.tsx

93/100

Accessibility

1 critical2 serious
Accessibility·keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx:39Critical

Category filter chips use onClick on a Badge with no button semantics

All 15 category chips ("AI", "Monitoring", "Incident Management", etc.) are Tremor `Badge` components with an `onClick` and `cursor-pointer` class. `Badge` renders a `span`, not a button: there's no `tabIndex`, no keyboard handler, no `role`, and no `aria-pressed` for the toggle state.

Why it matters

Keyboard-only and screen-reader users cannot tab to or activate any category filter, and screen readers never learn a chip is a toggle or whether it's currently selected. This locks a whole input method out of filtering providers.

Fix

Give the interactive chip a button role, keyboard handler, and aria-pressed state, or swap it for a native <button>.

<Badge
  color={
    providersSelectedCategories.includes(category) ? "orange" : "slate"
  }
  className={`rounded-full ${
    providersSelectedCategories.includes(category)
      ? "shadow-inner"
      : "hover:shadow-inner"
  } cursor-pointer`}
  key={category}
  onClick={() => toggleCategory(category)}
>
  {category}
</Badge>
<Badge
  color={
    providersSelectedCategories.includes(category) ? "orange" : "slate"
  }
  className={`rounded-full ${
    providersSelectedCategories.includes(category)
      ? "shadow-inner"
      : "hover:shadow-inner"
  } cursor-pointer focus-visible:outline focus-visible:outline-2 focus-visible:outline-orange-500`}
  key={category}
  role="button"
  tabIndex={0}
  aria-pressed={providersSelectedCategories.includes(category)}
  onClick={() => toggleCategory(category)}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      toggleCategory(category);
    }
  }}
>
  {category}
</Badge>
Accessibility·keep-ui/app/(health)/layout.tsx:45Serious

Build-info footer text is nearly invisible against the page background

The build/version footer renders `text-slate-400` at `text-xs` inside a div with `opacity-80`, sitting directly on the `bg-gray-50` body background.

Why it matters

Blending slate-400 at 80% opacity over gray-50 lands around 2.5:1 contrast, far below the 4.5:1 WCAG AA floor for small text. Anyone checking the build hash or version, including engineers debugging a deploy, strains to read it.

Fix

Raise the footer text to a darker gray token and drop the opacity reduction so the contrast clears 4.5:1.

<div className="pointer-events-none opacity-80 w-full p-2 text-slate-400 text-xs">
<div className="pointer-events-none w-full p-2 text-gray-500 text-xs">
Accessibility·keep-ui/app/(keep)/dashboard/GridLayout.tsx:63Serious

Widget reordering only works with a mouse drag, no keyboard path

`draggableHandle=".grid-item__widget"` wires drag-to-reposition through `react-grid-layout`, but the file defines no keyboard handler or focusable control that lets a user move or resize a widget without dragging.

Why it matters

Keyboard-only and motor-impaired users can view the dashboard but can never rearrange it, which locks them out of a core feature everyone else has.

Fix

Pair the drag handle with a keyboard-operable alternative (e.g. a move/resize menu or arrow-key repositioning) for the same action.

draggableHandle=".grid-item__widget"
draggableHandle=".grid-item__widget"
// TODO: expose a keyboard-accessible move/resize control in GridItemContainer
// as an alternative to pointer-only drag interaction
94/100

UX

3 serious
UX·keep-ui/app/(keep)/extraction/[rule_id]/executions/[execution_id]/page.tsx:26Serious

Loading state renders a blank page with no feedback

The component returns `null` while `isLoading` is true or `execution` hasn't loaded yet. Nothing renders: no spinner, no skeleton, no text.

Why it matters

Users landing on this execution detail page from a link see a blank white screen with zero feedback, and can't tell if the page is loading or broken.

Fix

Render a skeleton or loading indicator matching the page layout instead of returning null.

if (isLoading || !execution) {
  return null;
}
if (isLoading || !execution) {
  return (
    <div className="p-4">
      <Card className="h-40 animate-pulse" />
    </div>
  );
}
UX·keep-ui/app/(keep)/extraction/[rule_id]/executions/page.tsx:39Serious

Invalid rule_id leaves users on a permanent blank page

`rule` comes from `extractions?.find(...)`. If `params.rule_id` matches nothing, `rule` stays `undefined` forever and the guard on line 39 returns `null` indefinitely, with no message or link back.

Why it matters

A mistyped or stale rule_id in the URL traps the user on an unexplained blank screen with no path forward, instead of telling them the rule doesn't exist and offering a way back to the rule list.

Fix

Render a not-found state with a message and a link back to the extraction rules list when the rule can't be resolved.

if (isLoading || !rule) {
  return null;
}
if (isLoading) {
  return null;
}

if (!rule) {
  return (
    <div className="p-4">
      <Subtitle>Rule not found. <Link href="/extraction">Back to all rules</Link></Subtitle>
    </div>
  );
}
UX·keep-ui/app/(keep)/incidents/[id]/topology/page.tsx:28Serious

Topology page has no visible heading naming the incident

Both render branches drop straight into `<main><TopologySearchProvider><TopologyMap .../></TopologySearchProvider></main>` with no heading. `generateMetadata` sets the incident name in the browser tab title only, never in the page body.

Why it matters

Users navigating tabs (Timeline, Topology, Activity) inside an incident have no on-screen confirmation of which incident's topology they're viewing, which slows orientation especially with several incident tabs open.

Fix

Show the incident name as a visible heading above the topology map, not just in the document title.

<main className="h-[calc(100vh-28rem)]">
  <TopologySearchProvider>
    <TopologyMap
      selectedApplicationIds={[relevantApplication?.id || ""]}
      topologyApplications={applications}
    />
  </TopologySearchProvider>
</main>
<main className="h-[calc(100vh-28rem)]">
  <h2 className="sr-only">{getIncidentName(incident)} topology</h2>
  <TopologySearchProvider>
    <TopologyMap
      selectedApplicationIds={[relevantApplication?.id || ""]}
      topologyApplications={applications}
    />
  </TopologySearchProvider>
</main>

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • Splitting the render logic on `incident.incident_type === "topology"` versus the fallback branch keeps the data-fetching intent explicit: topology-native incidents get a filtered map scoped to the relevant application, while others get the raw incident.services list. That's the right way to branch server-rendered views on data shape rather than trying to overload one component prop set.
  • The two-column grid (`lg:col-span-2` for Logs, a narrower sidebar for Extracted Fields and Alert ID) puts the largest content area on the primary artifact, the logs, while metadata stays secondary. That's the right call: the log output is what someone debugging an execution actually reads first.
  • Placing `<ThemeScript />` as the very first child of `<body>`, with a comment explaining it must run before anything else, is the correct fix for theme-flash-on-load: the script needs to set the class/attribute before React hydrates any themed element, not after.
  • Centralizing the toggle logic in a single `toggleCategory` function instead of inline per-chip handlers keeps the selection behavior in one place, so a future change (like enforcing single-select) is a one-line edit rather than 15 repeated fixes.

Score your own repo with Rams.

Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.