danny-avila/librechat
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
More findings
Verdict
A team that clearly knows accessibility patterns keeps tripping on the last few inches of implementation. The biggest risk is the AgentCard keyboard path silently skipping onSelect, since divergent input paths erode trust in every other correct-looking affordance.
Files Rams reviewed
client/src/components/Auth/AuthLayout.tsx
client/src/components/Chat/Input/TokenUsage/index.tsx
client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx
client/src/components/Agents/AgentCard.tsx
client/src/components/Agents/AgentContact.tsx
client/src/components/Agents/AgentDetail.tsx
client/src/components/Agents/AgentDetailContent.tsx
client/src/components/Agents/AgentGrid.tsx
client/src/components/Agents/CategoryTabs.tsx
client/src/components/Agents/ErrorDisplay.tsx
client/src/components/Agents/MarketplaceAdminSettings.tsx
client/src/components/Agents/SearchBar.tsx
client/src/components/Agents/SmartLoader.tsx
client/src/components/Agents/VirtualizedAgentGrid.tsx
client/src/components/Artifacts/Artifact.tsx
client/src/components/Artifacts/ArtifactButton.tsx
client/src/components/Artifacts/ArtifactCodeEditor.tsx
client/src/components/Artifacts/ArtifactPreview.tsx
client/src/components/Artifacts/ArtifactTabs.tsx
client/src/components/Artifacts/ArtifactVersion.tsx
client/src/components/Artifacts/Artifacts.tsx
client/src/components/Artifacts/Code.tsx
client/src/components/Artifacts/DownloadArtifact.tsx
client/src/components/Audio/TTS.tsx
client/src/components/Audio/Voices.tsx
client/src/components/Auth/Footer.tsx
client/src/components/Auth/Login.tsx
client/src/components/Auth/LoginForm.tsx
client/src/components/Auth/Registration.tsx
client/src/components/Auth/RequestPasswordReset.tsx
Accessibility
client/src/components/Agents/AgentCard.tsx:64
Keyboard activation on AgentCard skips the onSelect callback mouse clicks fire
The card's `div role="button"` handles Enter/Space by calling `setIsOpen(true)` directly, while the mouse path goes through `OGDialogTrigger` into `handleOpenChange` (which presumably fires `onSelect` or other side effects on open). Keyboard users who tab to the card and press Enter bypass that handler entirely.
Why it matters
Keyboard-only users get a different code path than mouse users for the same action, so any tracking, validation, or state sync inside handleOpenChange silently never runs for them, producing inconsistent behavior that's invisible until someone files a bug that only reproduces via keyboard.
Fix
Route the keyboard Enter/Space handler through the same handleOpenChange function the dialog's onOpenChange calls, instead of calling setIsOpen directly.
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setIsOpen(true);
}
}}onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleOpenChange(true);
}
}}client/src/components/Auth/AuthLayout.tsx:42
'Click here' recovery link fails contrast on the invalid-token error
In AuthLayout.tsx, the recovery link wrapping localize('com_auth_click_here') uses `text-green-600` on the page's `bg-white` background. Tailwind's green-600 (#16a34a) against white measures ~3.3:1.
Why it matters
This is the only recovery path shown when a reset token is invalid, and it fails WCAG AA's 4.5:1 threshold for normal-size text, making the exact link a user needs to fix their account harder to read for low-vision users.
Fix
Darken the link color to a green shade that clears 4.5:1 against white, or underline it persistently instead of only on hover.
<a className="font-semibold text-green-600 hover:underline" href="/forgot-password"><a className="font-semibold text-green-700 underline" href="/forgot-password">client/src/components/Agents/AgentCard.tsx:103
AgentContact renders anchor links nested inside a role="button" container
AgentCard.tsx wraps the whole card in a `div role="button" tabIndex={0}` that opens a dialog on click, and `AgentContact` is rendered inside that same div at line 104, which renders `<a>` tags for contact info. This nests real interactive anchors inside a synthetic button.
Why it matters
Screen reader and keyboard users hit an ambiguous focus target: tabbing lands on the outer card first, but a nested link inside it either steals the click meant to open the dialog or becomes unreachable depending on event bubbling, and both the card and the links compete for the same click.
Fix
Move interactive contact links outside the clickable card region, or stop the anchor's click from bubbling to the card's open handler with stopPropagation.
<AgentContact
agent={agent}
className="mt-1 text-xs text-text-secondary [&_a]:font-normal [&_a]:text-text-secondary"
/><div onClick={(e) => e.stopPropagation()}>
<AgentContact
agent={agent}
className="mt-1 text-xs text-text-secondary [&_a]:font-normal [&_a]:text-text-secondary"
/>
</div>Motion
client/src/components/Chat/Input/TokenUsage/index.tsx:76
Token usage disclosure animates in with no reduced-motion guard
The `Ariakit.PopoverDisclosure` in TokenUsage's index.tsx carries `duration-300 animate-in fade-in zoom-in-95` unconditionally. There's no `motion-reduce:` variant or `prefers-reduced-motion` check anywhere in the className chain, so every user gets the zoom/fade regardless of OS setting.
Why it matters
Vestibular-sensitive users who've set 'reduce motion' at the OS level still get the fade-in/zoom-in on a control they'll hit repeatedly per session, since this is a persistent chat UI element, not a one-time onboarding animation.
Fix
Gate entrance animation classes behind a motion-reduce variant so the effect respects OS-level reduced-motion settings.
className={cn(
'flex size-9 items-center justify-center rounded-full p-1 transition-colors',
'hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'duration-300 animate-in fade-in zoom-in-95',
)}className={cn(
'flex size-9 items-center justify-center rounded-full p-1 transition-colors',
'hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'motion-safe:duration-300 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-95',
)}Typography
Color
Spacing
Components
UX
Craft
Working well
- AgentCard's card wraps a real aria-label built from the agent's name and description, plus aria-describedby pointing at the description paragraph's own id. That gives screen reader users context beyond the truncated visible text, which is the right pattern for a card that opens a detail dialog.
- The TokenUsage popover documents and wires finalFocus back to the disclosure button (finalFocus={disclosureRef}), and the adjacent comment explains why focus:outline-none is safe there. That's real attention to what happens after the dialog closes, not just how it opens.
- Falling back through support name, then email, then owner name (evident in the AgentContact usage) before landing on any 'no contact' string keeps the UI from ever rendering blank, which is a clean defensive pattern for optional profile data.
Scored August 1, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored July 9, 2026: 59/100. This rescore on v0.0.3: 92/100.
This page is an automated design review of danny-avila/librechat’s UI code: 30 files read against 309 versioned rules covering accessibility, color, typography, spacing, components, UX, motion, and craft. The score is out of 100; confirmed criticals cap it — one at 59, two at 49, three or more at 39.
Score your own repo.
Free on public repos, no account. The same engine that scored this page reads your UI code and mints a score page like this one.
Public repos only. The full engine reviews the UI code and mints a public score page — we email you the link too. Already-scored repos open instantly.