khoj-ai/khoj
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Semantic HTML gets abandoned exactly where it matters most: the sidebar's nav links are clean, but the two primary actions are unfocusable divs. Credit for solid structural instincts elsewhere, but keyboard users can't sign up or start a chat.
Files Rams reviewed
src/interface/web/app/components/allConversations/sidePanel.module.css
src/interface/web/app/components/appSidebar/appSidebar.tsx
src/interface/web/app/components/chatHistory/chatHistory.module.css
src/interface/web/app/components/chatMessage/FileContentSnippet.tsx
src/interface/web/app/components/chatMessage/chatMessage.module.css
src/interface/web/app/components/deprecationBanner.tsx
src/interface/web/app/components/excalidraw/excalidrawWrapper.tsx
src/interface/web/app/components/loading/loading.tsx
src/interface/web/app/components/loginPrompt/loginPopup.tsx
src/interface/web/app/components/loginPrompt/loginPrompt.module.css
src/interface/web/app/components/logo/fileLogo.tsx
src/interface/web/app/components/mermaid/mermaid.tsx
src/interface/web/app/components/navMenu/navMenu.module.css
src/interface/web/app/components/navMenu/navMenu.tsx
src/interface/web/app/components/profileCard/profileCard.tsx
src/interface/web/app/components/shareLink/shareLink.tsx
src/interface/web/app/components/suggestions/suggestionCard.tsx
src/interface/web/app/components/userMemory/userMemory.tsx
src/interface/web/app/agents/layout.tsx
src/interface/web/app/automations/layout.tsx
src/interface/web/app/chat/layout.tsx
src/interface/web/app/layout.tsx
src/interface/web/app/settings/layout.tsx
src/interface/web/app/share/chat/layout.tsx
src/interface/web/app/share/chat/page.tsx
src/interface/web/app/components/agentCard/agentCard.module.css
src/interface/web/app/components/chatInputArea/chatInputArea.module.css
src/interface/web/app/components/chatWoot/ChatwootWidget.tsx
src/interface/web/app/components/excalidraw/excalidraw.tsx
src/interface/web/app/components/loginPrompt/GoogleSignIn.tsx
Accessibility
"Sign up to get started" and "New" render as unfocusable divs, not buttons
Both `SidebarMenuButton asChild` wrappers pass onClick to an inner `<div>` (the Sign up prompt and the New chat action). Because `asChild` swaps the rendered element for that div, the click handler lands on a non-interactive element with no keyboard focus, no tabIndex, and no button role. - `<div><UserPlusIcon /><span>Sign up to get started</span></div>` - `<div><Plus /><span>New</span></div>`
Why it matters
Keyboard-only and screen reader users cannot tab to or activate the two most important actions in the sidebar: starting a chat and signing up. This locks a class of users out of the primary entry point of the app.
Fix
Use a semantic <button> element as the asChild target for any onClick handler.
<div>
<UserPlusIcon />
<span>Sign up to get started</span>
</div><button type="button" className="flex items-center gap-2 w-full">
<UserPlusIcon />
<span>Sign up to get started</span>
</button>Banner text nearly unreadable against its own orange fill
The banner text (`text-orange-50`) sits on `bg-orange-600/90`. Blended against a white page background, the fill resolves to roughly #EC6924, and orange-50 text against it computes to ~3:1 contrast.
Why it matters
This falls below the 4.5:1 WCAG AA minimum for body text, so the deprecation notice, the most time-sensitive message on the page, is the hardest thing on the page to read.
Fix
Raise contrast to at least 4.5:1 by darkening the background or using a lighter, fully-opaque text color.
<div className="w-full px-4 py-2.5 bg-orange-600/90 shadow-sm flex items-center justify-center gap-2 text-sm text-orange-50 z-0 relative"><div className="w-full px-4 py-2.5 bg-orange-700 shadow-sm flex items-center justify-center gap-2 text-sm text-white z-0 relative">Color
Line-number and truncation text have no dark-mode variant while the highlight does
The target-line highlight correctly ships `bg-green-100 dark:bg-green-900`, but the adjacent line-number span (`text-gray-400`) and the truncation footer (`text-gray-500 italic`) carry no `dark:` variant anywhere in the file.
Why it matters
In dark mode these grays sit on a dark chat bubble with too little contrast, or clash with the theme, breaking readability exactly where the file already proves dark mode is supported.
Fix
Pair every gray text color with an explicit dark-mode counterpart, matching the pattern already used on the highlight background.
<div className="text-gray-500 italic">
... and {lines.length - maxLines} more lines
</div><div className="text-gray-500 dark:text-gray-400 italic">
... and {lines.length - maxLines} more lines
</div>Dark class toggling has no color-scheme declaration for browser chrome
The inline script adds `document.documentElement.classList.add('dark')` to flip the app into dark mode before hydration, but the `<html>` element and `<head>` never set `color-scheme`.
Why it matters
Without color-scheme, native browser UI, form controls, scrollbars, and the default page background flash light even when the script has already applied dark mode, so the page looks half-themed for a frame and native inputs stay light inside an otherwise dark UI.
Fix
Set color-scheme to light dark (or toggle it alongside the dark class) so native controls track the active theme.
<html
lang="en"
className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}
suppressHydrationWarning
><html
lang="en"
className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}
style={{ colorScheme: "light dark" }}
suppressHydrationWarning
>Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeComponents
Hand-rolled expanded overlay has no dialog semantics or focus trap
When `expanded` is true, the wrapper renders a `fixed inset-0` overlay div with a dark backdrop, but it carries no `role="dialog"`, no `aria-modal`, and never moves focus into it. Escape is handled by directly reassigning the global `onkeydown` property instead of a scoped listener, so focus never returns to the trigger button on close.
Why it matters
Keyboard and screen reader users get no signal they've entered a modal-like view, can tab out into the page behind the backdrop, and lose their place when it closes.
Fix
Give the expanded container dialog semantics and move focus into it when opened, or replace it with an accessible dialog primitive.
<div
className={`${expanded ? "fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm z-50 flex items-center justify-center" : ""}`}
><div
role={expanded ? "dialog" : undefined}
aria-modal={expanded ? true : undefined}
aria-label="Expanded diagram view"
className={`${expanded ? "fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm z-50 flex items-center justify-center" : ""}`}
>UX
InlineLoading button has no explicit type, defaults to form submit
`InlineLoading` renders `<button className={...}>` with no `type` attribute. Buttons without an explicit type default to `type="submit"` in HTML.
Why it matters
If this component is ever mounted inside a `<form>`, clicking it (or it receiving a stray click/enter) submits the form unintentionally.
Fix
Set type='button' on any button that isn't intentionally a form submit control.
<button className={`${props.className}`}><button type="button" className={`${props.className}`}>Typography
Spacing
Motion
Craft
Working well
- Splitting `KhojLogoType` versus the plain `<h2>Ask Anything</h2>` behind `isMobileWidth` is a sensible way to keep the header compact on narrow viewports without hiding the wayfinding label entirely, since mobile users get the brand mark while desktop users get the literal page context.
- The primary nav items ("Home", "Agents", "Automations", "Search", "Settings") are built from real <a> tags with href, which means keyboard focus, browser history, and screen readers all work for free. This is the pattern the two custom action buttons above should have followed.
- Wrapping the theme-detection logic in try/catch guards against localStorage being unavailable (private browsing, disabled storage) without breaking the rest of the script, which is a reasonable defensive choice for code that runs before any error boundary exists.
- Padding line numbers with `padStart(3, " ")` inside a monospace `<pre>` context keeps the gutter column visually aligned without needing `tabular-nums`: monospace already guarantees equal digit widths, so this is the simpler correct fix for the same problem.
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.