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

vercel/next-forge

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

Give mobile menu toggle an accessible name and expanded state

See the fix

Verdict

Visual polish is real, staggered marquees, clean grids, disciplined CTA hierarchy, but accessibility is an afterthought everywhere it counts. The biggest risk is that entire sections and controls are invisible or unusable for keyboard and screen reader users.

Files Rams reviewed

apps/web/app/[locale]/components/header/index.tsx

docs/app/[lang]/(home)/components/apps/index.tsx

docs/app/[lang]/(home)/components/features/index.tsx

apps/app/app/(authenticated)/components/avatar-stack.tsx

apps/app/app/(authenticated)/components/collaboration-provider.tsx

apps/app/app/(authenticated)/components/cursors.tsx

apps/app/app/(authenticated)/components/header.tsx

apps/app/app/(authenticated)/components/sidebar.tsx

apps/web/app/[locale]/(home)/components/cases.tsx

apps/web/app/[locale]/(home)/components/cta.tsx

apps/web/app/[locale]/(home)/components/faq.tsx

apps/web/app/[locale]/(home)/components/features.tsx

apps/web/app/[locale]/(home)/components/hero.tsx

apps/web/app/[locale]/(home)/components/stats.tsx

apps/web/app/[locale]/(home)/components/testimonials.tsx

apps/web/app/[locale]/components/footer.tsx

apps/web/app/[locale]/components/header/language-switcher.tsx

apps/web/app/[locale]/contact/components/contact-form.tsx

docs/app/[lang]/(home)/components/hero.tsx

docs/app/[lang]/(home)/components/installer.tsx

apps/app/app/(authenticated)/layout.tsx

apps/app/app/(authenticated)/page.tsx

apps/app/app/(authenticated)/search/page.tsx

apps/app/app/(unauthenticated)/layout.tsx

apps/app/app/layout.tsx

apps/web/app/[locale]/(home)/page.tsx

apps/web/app/[locale]/blog/[slug]/page.tsx

apps/web/app/[locale]/blog/page.tsx

apps/web/app/[locale]/layout.tsx

apps/web/app/[locale]/legal/[slug]/page.tsx

90/100

Accessibility

2 critical2 serious
Accessibility·apps/web/app/[locale]/components/header/index.tsx:150Critical

Mobile menu toggle has no accessible name or expanded state

The mobile nav trigger `<Button onClick={() => setOpen(!isOpen)} variant="ghost">` renders only a `Menu`/`X` icon with no `aria-label`, no `aria-expanded`, and no `aria-controls` pointing at the panel it opens.

Why it matters

Screen reader users hear an unlabeled button with no indication it opens a menu or whether it's currently open, so the entire mobile navigation is effectively locked out for non-visual users.

Fix

Give icon-only toggle buttons an aria-label and expose expanded state with aria-expanded/aria-controls.

<Button onClick={() => setOpen(!isOpen)} variant="ghost">
  {isOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</Button>
<Button
  onClick={() => setOpen(!isOpen)}
  variant="ghost"
  aria-label={isOpen ? "Close menu" : "Open menu"}
  aria-expanded={isOpen}
  aria-controls="mobile-nav-panel"
>
  {isOpen ? <X className="h-5 w-5" aria-hidden="true" /> : <Menu className="h-5 w-5" aria-hidden="true" />}
</Button>
Accessibility·docs/app/[lang]/(home)/components/features/index.tsx:187Critical

aria-hidden on the entire section hides all 24 tool logos and the headline

The outer content wrapper carries `aria-hidden="true"` on the div that contains everything: the paragraph reading "Built with the best tools for modern developers" and all 24 `<span>` labels (BetterStack, Clerk, Stripe, React, etc.) inside the marquee rows.

Why it matters

Screen reader users get nothing from the `#features` section: no headline, no list of integrations. This is real content, not decoration, so hiding it removes a meaningful part of the page for assistive tech users while sighted users still see it.

Fix

Reserve aria-hidden for purely decorative wrappers; expose real content by removing it from the content container and only hiding truly non-informative background elements.

<div
  aria-hidden="true"
  className="relative h-full overflow-hidden bg-background py-24 ring-inset sm:py-32"
>
<div
  className="relative h-full overflow-hidden bg-background py-24 ring-inset sm:py-32"
>
Accessibility·docs/app/[lang]/(home)/components/apps/index.tsx:89Serious

Product screenshots ship empty alt text, hiding them from screen readers

Each `App` card renders `<Image alt="" src={app.image} />` for actual product screenshots (app.png, api.png, docs.png, etc.), not decorative graphics.

Why it matters

Screen reader users lose the visual proof for every app card ("app", "api", "email", "web", "docs", "studio", "storybook") and hear nothing where sighted users see the product in action.

Fix

Give every content image descriptive alt text naming what it shows.

<Image
  alt=""
  className="h-auto w-full overflow-hidden rounded-md border object-cover object-left shadow-sm"
  src={app.image}
/>
<Image
  alt={`Screenshot of the ${app.name} app`}
  className="h-auto w-full overflow-hidden rounded-md border object-cover object-left shadow-sm"
  src={app.image}
/>
Accessibility·apps/web/app/[locale]/components/header/index.tsx:154Serious

Mobile nav panel opens with no dialog semantics or escape route

The panel `<div className="container absolute top-20 right-0 ... ">` rendered when `isOpen` is true has no `role`, no focus moved into it on open, and no Escape-key handler to close it. It's driven purely by conditional render, not a dialog primitive.

Why it matters

Keyboard and screen reader users get no signal that a new navigation surface appeared, can't reliably navigate it, and have no keyboard path to dismiss it other than re-finding the toggle button.

Fix

Use an accessible disclosure/dialog pattern that names the region, moves focus in on open, and closes on Escape.

{isOpen && (
  <div className="container absolute top-20 right-0 flex w-full flex-col gap-8 border-t bg-background py-4 shadow-lg">
{isOpen && (
  <div
    id="mobile-nav-panel"
    role="dialog"
    aria-label="Mobile navigation"
    className="container absolute top-20 right-0 flex w-full flex-col gap-8 border-t bg-background py-4 shadow-lg"
  >
96/100

UX

2 serious
UX·apps/app/app/(authenticated)/components/avatar-stack.tsx:51Serious

Self avatar is visually and semantically identical to collaborators

The current user's avatar renders through the same `PresenceAvatar` with the same tooltip pattern as everyone else: `<p>{info?.name ?? "Unknown"}</p>`. Nothing in the markup marks it as "you" versus another collaborator, and it sits at the end of the stack with identical size, ring, and fallback styling.

Why it matters

In a presence stack, telling your own avatar apart from teammates matters for orientation, especially once the list grows past 2-3 people. Right now a user has to hover every avatar and mentally match names to know which one is theirs.

Fix

Append a self-identifying suffix to the current user's tooltip label so self-identification doesn't require memorizing your own display name.

{self && <PresenceAvatar info={self.info} />}
{self && (
  <PresenceAvatar
    info={
      self.info ? { ...self.info, name: `${self.info.name} (You)` } : self.info
    }
  />
)}
UX·apps/app/app/(authenticated)/components/collaboration-provider.tsx:18Serious

Mention and user-resolution failures throw generic errors with no recovery path

`resolveUsers` and `resolveMentionSuggestions` both throw `new Error("Problem resolving users")` / `"Problem resolving mention suggestions"` on failure, with no fallback value or user-facing message.

Why it matters

When the users or search API call fails, the thrown error has to be caught somewhere upstream or it crashes the collaboration editor entirely, leaving the person mid-comment with no indication of what broke or how to retry.

Fix

Return an empty result with a visible inline error state instead of throwing, or ensure the thrown error is caught by a boundary that shows a retry action.

if ("error" in response) {
  throw new Error("Problem resolving users");
}

return response.data;
if ("error" in response) {
  // Surface an empty list instead of throwing, so a lookup failure
  // degrades gracefully rather than crashing the editor.
  return [];
}

return response.data;

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

  • The desktop actions row separates "Contact" (ghost), "Sign in" (outline), and "Sign up" (filled default) into three distinct visual weights. This is correct: the least committed action reads lightest and the primary conversion action "Sign up" carries the most visual weight, so users scan the row and immediately know which button matters most.
  • External links in the mobile menu correctly set `rel="noopener noreferrer"` and `target="_blank"` conditionally based on `item.href.startsWith("http")`, which protects against reverse tabnabbing on external destinations like the docs link without penalizing internal links.
  • Each logo in a row gets its own `[animation-delay]` and `[animation-duration]` value (e.g. BetterStack at -26s/30s vs Clerk at -8s/30s), so the marquee never falls into lockstep. This is the detail that keeps a repeating pattern from reading as a robotic template loop.
  • The stacked avatars use `-space-x-1` with `ring-1 ring-background` on each `Avatar`: this is the correct way to overlap circular avatars: the ring punches a visible gap against the page background so overlapping edges stay legible instead of merging into a blob.

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.