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

vercel/ai-chatbot

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

Remove maximumScale cap so users can pinch-zoom the app

See the fix

Verdict

Clean composition and consistent tokens sit on top of an accessibility foundation that's quietly broken: zoom is capped, focus rings are stripped, and loading states default to blank divs. The biggest risk is that these aren't isolated bugs but repeated patterns, every auth and async flow leaves users guessing whether the app is working.

Files Rams reviewed

app/(auth)/layout.tsx

app/(auth)/login/page.tsx

app/(auth)/register/page.tsx

app/(chat)/layout.tsx

app/layout.tsx

app/(chat)/chat/[id]/page.tsx

app/(chat)/page.tsx

app/globals.css

components/ai-elements/code-block.tsx

components/ai-elements/conversation.tsx

components/ai-elements/message.tsx

components/ai-elements/model-selector.tsx

components/ai-elements/reasoning.tsx

components/ai-elements/shimmer.tsx

components/ai-elements/suggestion.tsx

components/ai-elements/tool.tsx

components/chat/app-sidebar.tsx

components/chat/artifact-actions.tsx

components/chat/artifact-messages.tsx

components/chat/artifact.tsx

components/chat/auth-form.tsx

components/chat/chat-header.tsx

components/chat/code-editor.tsx

components/chat/console.tsx

components/chat/create-artifact.tsx

components/chat/data-stream-handler.tsx

components/chat/data-stream-provider.tsx

components/chat/diffview.tsx

components/chat/document-preview.tsx

components/chat/document-skeleton.tsx

91/100

Accessibility

1 critical3 serious
Accessibility·app/layout.tsx:15Critical

Viewport export caps zoom, blocking low-vision users from pinch-zooming

The `viewport` export sets `maximumScale: 1`, which suppresses pinch-to-zoom across the whole app.

Why it matters

iOS ignores this cap for pinch zoom, but other mobile browsers honor it, so low-vision users on Android or non-Safari browsers lose the ability to zoom the entire app, not just a single input.

Fix

Remove the zoom cap and fix any input-zoom issue at the source with a 16px input font instead.

export const viewport = {
  maximumScale: 1,
};
export const viewport = {
  width: "device-width",
  initialScale: 1,
};
Accessibility·app/(auth)/login/page.tsx:19Serious

Login page renders no loading state while the form action is pending

`handleSubmit` calls `formAction(formData)` but the page never derives a pending flag from `useActionState` (the hook exposes `isPending` as a third value) and never passes it to `SubmitButton` beyond `isSuccessful`.

Why it matters

Between click and the server responding, the button gives no feedback that anything happened, so users on a slow connection click "Sign in" again and risk a duplicate submission.

Fix

Destructure the pending state from useActionState and disable the submit button while the action is in flight.

const [state, formAction] = useActionState<LoginActionState, FormData>(
  login,
  { status: "idle" }
);
const [state, formAction, isPending] = useActionState<LoginActionState, FormData>(
  login,
  { status: "idle" }
);
Accessibility·app/(auth)/layout.tsx:18Serious

Decorative icons next to labels announce as unnamed noise to screen readers

`ArrowLeftIcon`, `SparklesIcon`, and `VercelIcon` sit beside visible text ("Back", the form heading, "AI Gateway") with no `aria-hidden` set on any of them.

Why it matters

Without aria-hidden, screen readers announce these as an unlabeled image or raw SVG title on top of the adjacent text, adding noise to every read of the auth flow's back link and footer.

Fix

Mark decorative icons aria-hidden when they sit next to their own text label.

<ArrowLeftIcon className="size-3.5" />
Back
<ArrowLeftIcon className="size-3.5" aria-hidden="true" />
Back
Accessibility·app/layout.tsx:78Serious

Focus outline removed globally with no replacement ring

globals.css sets `button:focus-visible, select:focus-visible, [role="button"]:focus-visible, input:focus-visible, textarea:focus-visible { outline: none; }` with no accompanying box-shadow or ring replacement anywhere in the visible stylesheet.

Why it matters

Keyboard users tabbing through the app (nav links, the theme toggle, the chat composer) lose all visible indication of where focus is, which breaks WCAG 2.4.7 and makes keyboard navigation unusable.

Fix

Never remove focus-visible outline without substituting a visible ring or box-shadow.

<ThemeProvider
  attribute="class"
  defaultTheme="system"
  disableTransitionOnChange
  enableSystem
>
<ThemeProvider
  attribute="class"
  defaultTheme="system"
  disableTransitionOnChange
  enableSystem
>
  {/* Ensure globals.css focus-visible rule includes a visible outline/ring replacement */}
96/100

UX

2 serious
UX·app/(chat)/layout.tsx:20Serious

Blank colored div gives zero feedback while session and cookies load

The outer `Suspense` wrapping `SidebarShell` falls back to `<div className="flex h-dvh bg-sidebar" />` while `auth()` and `cookies()` resolve. There is no spinner, skeleton, or sidebar-shaped placeholder, just an empty colored rectangle.

Why it matters

Every chat page load blocks on this boundary. Users see a frozen tinted screen instead of any indication the app is working, which reads as a hang rather than a load.

Fix

Replace blank loading divs with a skeleton that mirrors the resolved layout's structure.

<Suspense fallback={<div className="flex h-dvh bg-sidebar" />}>
  <SidebarShell>{children}</SidebarShell>
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
  <SidebarShell>{children}</SidebarShell>
</Suspense>
UX·app/(auth)/register/page.tsx:24Serious

Validation errors only surface as toasts, never next to the field that failed

The status effect maps `user_exists`, `failed`, and `invalid_data` all to a `toast({ ... })` call with no inline message rendered near an email or password field.

Why it matters

A toast disappears and gives no indication of which field caused "Failed validating your submission!": users have to guess whether the problem was their email format or password, which adds friction to fixing the form.

Fix

Show validation errors inline next to the relevant input, using the toast only as a secondary signal.

} else if (state.status === "invalid_data") {
  toast({
    description: "Failed validating your submission!",
    type: "error",
  });
} else if (state.status === "invalid_data") {
  setFieldError("Check your email and password and try again.");
  toast({
    description: "Failed validating your submission!",
    type: "error",
  });

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

  • Composing `AppSidebar`, `SidebarProvider`, and `SidebarInset` from the project's own `@/components/ui/sidebar` primitive instead of hand-rolling a collapsible panel is the right call. It keeps sidebar state, keyboard behavior, and collapse persistence in one owned place instead of duplicated per layout.
  • The Toaster, sidebar, and skeleton fallbacks all reference tokens (bg-sidebar, bg-card, text-foreground, border-border) rather than hardcoded hex. This is exactly how a theme layer should be consumed, and it means dark mode or a rebrand touches the token file, not this layout.
  • Splitting the sign-in flow into `AuthForm` and `SubmitButton` instead of inlining the form here keeps this page focused on state orchestration and makes the same primitives reusable on the register page. This is the right extension point rather than duplicating markup.
  • The "Sign up" link uses `text-foreground underline-offset-4 hover:underline` instead of button styling, so it reads as navigation rather than an action. That distinction (link looks like a link, button looks like a button) is exactly what keeps affordances honest.

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.