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

supabase/supabase

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

30 files reviewed·July 9, 2026

Top fix

Add aria-pressed and group labels to active state toggles

See the fix

Verdict

Solid token discipline collides with silent state changes: components use real design tokens but rely on color alone to signal selection, with no aria-pressed, labels, or live regions to back it up. The biggest risk is that screen reader users and keyboard navigators miss critical UI changes entirely, from cohort filters to auth transitions.

Files Rams reviewed

blocks/vue/registry/default/password-based-auth/nuxtjs/app/pages/protected/index.vue

apps/studio/components/interfaces/ConnectSheet/content/nextjs/app/supabasejs/content.tsx

apps/studio/components/interfaces/ConnectSheet/content/nextjs/pages/supabasejs/content.tsx

apps/www/app/state-of-startups/components/CohortToggle.tsx

apps/www/app/state-of-startups/components/DecorativeProgressBar.tsx

apps/www/app/state-of-startups/components/ParticipantsCarousel.tsx

apps/www/app/state-of-startups/components/SectionCallout.tsx

apps/www/app/state-of-startups/components/StateOfStartupsAuroraHeader.tsx

apps/www/app/state-of-startups/components/StateOfStartupsHeader.tsx

apps/www/app/state-of-startups/components/SurveyChannelMixChart.tsx

apps/www/app/state-of-startups/components/SurveyChapter.tsx

apps/www/app/state-of-startups/components/SurveyChapterSection.tsx

apps/www/app/state-of-startups/components/SurveyChart.tsx

apps/www/app/state-of-startups/components/SurveyChartShell.tsx

apps/www/app/state-of-startups/components/SurveyCompareStatCard.tsx

apps/www/app/state-of-startups/components/SurveyCrossTabChart.tsx

apps/www/app/state-of-startups/components/SurveyPullQuote.tsx

apps/www/app/state-of-startups/components/SurveyPullQuoteCarousel.tsx

apps/www/app/state-of-startups/components/SurveyPullQuoteGrid.tsx

apps/www/app/state-of-startups/components/SurveyRankedAnswersPair.tsx

apps/www/app/state-of-startups/components/SurveyStatCard.tsx

apps/www/app/state-of-startups/components/SurveySummarizedAnswer.tsx

apps/www/app/state-of-startups/components/SurveyWordCloud.tsx

apps/www/app/state-of-startups/components/TwoOptionToggle.tsx

apps/www/app/state-of-startups/components/YearToggle.tsx

apps/www/app/state-of-startups/components/surveyResults.css

blocks/vue/registry/default/dropzone/nuxtjs/app/components/dropzone-content.vue

blocks/vue/registry/default/dropzone/nuxtjs/app/components/dropzone-empty-state.vue

blocks/vue/registry/default/dropzone/nuxtjs/app/components/dropzone.vue

blocks/vue/registry/default/password-based-auth/nuxtjs/app/components/forgot-password-form.vue

94/100

Accessibility

3 serious
Accessibility·apps/www/app/state-of-startups/components/ParticipantsCarousel.tsx:59Serious

Duplicated marquee content doubles tab stops and screen reader announcements

Each row builds `const loop = [...row, ...row]` to make the CSS scroll seamless, then renders both copies as real `<li><Link>` elements with no `aria-hidden` or `tabIndex={-1}` on the second half. A screen reader or keyboard user tabbing through "Company A, Company B..." hits every company name twice in a row.

Why it matters

Keyboard users have to tab through twice as many links to get past the carousel, and screen reader users hear every startup name announced twice with no indication it's a repeat, which adds real navigation friction on a decorative element.

Fix

Hide the duplicated half of the loop from assistive technology and remove it from tab order.

{loop.map((participant, index) => (
  <li key={`${rowIndex}-${index}-${participant.company}`} className="shrink-0">
    <Link
      href={participant.url}
      target="_blank"
      rel="noopener noreferrer"
      className="text-xs md:text-sm font-mono tracking-widest uppercase text-foreground-lighter hover:text-brand-link transition-colors whitespace-nowrap"
    >
      {participant.company}
    </Link>
  </li>
))}
{loop.map((participant, index) => {
  const isDuplicate = index >= row.length
  return (
    <li
      key={`${rowIndex}-${index}-${participant.company}`}
      className="shrink-0"
      aria-hidden={isDuplicate || undefined}
    >
      <Link
        href={participant.url}
        target="_blank"
        rel="noopener noreferrer"
        tabIndex={isDuplicate ? -1 : 0}
        className="text-xs md:text-sm font-mono tracking-widest uppercase text-foreground-lighter hover:text-brand-link transition-colors whitespace-nowrap"
      >
        {participant.company}
      </Link>
    </li>
  )
})}
Design System·apps/studio/components/interfaces/ConnectSheet/content/nextjs/pages/supabasejs/content.tsx:60Serious

Rendered todo names have no truncation or wrap safeguard in the list

In `pages/index.tsx`'s generated snippet: `<li key={todo.id}>{todo.name}</li>` renders arbitrary user data with no `break-words` or truncation handling.

Why it matters

This is boilerplate that developers copy directly into real projects. A long, unbroken todo name (a pasted URL or long string) blows out the list container width in the resulting app, and that failure ships silently to every developer who copies this snippet as-is.

Fix

Add word-break handling to any list item rendering dynamic user text.

{todos.map((todo) => (
  <li key={todo.id}>{todo.name}</li>
))}
{todos.map((todo) => (
  <li key={todo.id} className="break-words">{todo.name}</li>
))}
Accessibility·apps/www/app/state-of-startups/components/CohortToggle.tsx:24Serious

Active cohort filter is marked only by color and border, invisible to screen readers

Each filter button toggles between an active state (`border-brand-500/40 bg-brand-300/40 text-brand-link`) and inactive state, but the `<button>` carries no `aria-pressed` or `aria-current`. The only signal of which cohort is selected is a border and background tint.

Why it matters

Screen reader users hear identical button announcements for every option and have no way to tell which cohort filter is currently applied, so they can't confirm what data they're viewing.

Fix

Expose the selected state programmatically with aria-pressed alongside the existing visual treatment.

<button
  key={option.label}
  type="button"
  onClick={() => onValueChange(option.label)}
  className={`px-3 py-1 text-xs font-mono uppercase tracking-wider border rounded-full transition-colors ${
    isActive
      ? 'border-brand-500/40 bg-brand-300/40 text-brand-link dark:text-brand'
      : 'border-overlay text-foreground-light hover:text-foreground hover:bg-surface-100'
  }`}
>
<button
  key={option.label}
  type="button"
  aria-pressed={isActive}
  onClick={() => onValueChange(option.label)}
  className={`px-3 py-1 text-xs font-mono uppercase tracking-wider border rounded-full transition-colors ${
    isActive
      ? 'border-brand-500/40 bg-brand-300/40 text-brand-link dark:text-brand'
      : 'border-overlay text-foreground-light hover:text-foreground hover:bg-surface-100'
  }`}
>
98/100

Spacing

1 serious
Spacing·apps/www/app/state-of-startups/components/DecorativeProgressBar.tsx:90Serious

Arbitrary calc() width bypasses the spacing/layout scale

`DecorativeProgressBar` sets `style={{ maxWidth: 'calc(50% + 60rem / 2)' }}` as an inline magic-number calculation instead of a Tailwind max-width utility or container token.

Why it matters

A one-off calc value with no name can't be reused or reasoned about elsewhere in the page, so any future layout tweak to this section has to be rediscovered by reading this one file.

Fix

Replace ad hoc calc() with an existing max-width scale value or a named CSS variable shared across the page's layout components.

style={{
  maxWidth: 'calc(50% + 60rem / 2)',
}}
style={{
  maxWidth: 'var(--decorative-bar-max-width, calc(50% + 60rem / 2))',
}}

Want this on your repo?

Rams reviews your next PR automatically and posts inline fix suggestions.

Review my public repo for free
98/100

UX

1 serious
UX·blocks/vue/registry/default/password-based-auth/nuxtjs/app/pages/protected/index.vue:35Serious

"Logout" button has no explicit type and defaults to submit

The `<button>` labeled "Logout" has no `type` attribute, so it defaults to `type="submit"`.

Why it matters

If this component is ever placed inside a form (a common refactor when adding a confirm step or wrapping layout), the button will submit that form unintentionally instead of only firing `handleLogout`.

Fix

Set type="button" on any button that isn't meant to submit a form.

<button
  class="rounded-md bg-primary text-primary-foreground px-3 py-2 text-sm font-medium hover:bg-primary/90"
  @click="handleLogout"
>
  Logout
</button>
<button
  type="button"
  class="rounded-md bg-primary text-primary-foreground px-3 py-2 text-sm font-medium hover:bg-primary/90"
  @click="handleLogout"
>
  Logout
</button>
98/100

Craft

1 serious
Craft·apps/studio/components/interfaces/ConnectSheet/content/nextjs/app/supabasejs/content.tsx:96Serious

Narrating comment restates the line directly below it

In the `middleware.ts` snippet, the comment `// Create an unmodified response` sits directly above `let supabaseResponse = NextResponse.next({...})`, which already says exactly that.

Why it matters

This code ships as the actual sample developers copy into their apps. A comment that just repeats the next line is filler every future reader has to read past and keep in sync, and it signals the snippet wasn't edited by a human before shipping.

Fix

Delete comments that restate the adjacent line; keep only comments that explain why.

// Create an unmodified response
let supabaseResponse = NextResponse.next({
  request: {
    headers: request.headers,
  },
});
let supabaseResponse = NextResponse.next({
  request: {
    headers: request.headers,
  },
});

Typography

No issues found

Color

No issues found

Components

No issues found

Motion

No issues found

Working well

  • The `.env.local` snippet conditionally emits `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` vs `NEXT_PUBLIC_SUPABASE_ANON_KEY` based on `projectKeys?.publishableKey`, and every downstream file (`server.ts`, `client.ts`, `middleware.ts`) reads the same conditional key name. Keeping that branch consistent across all five generated files is exactly what prevents a copy-pasted snippet from referencing an env var that doesn't exist.
  • The `.marquee-track` keyframes are wrapped in `@media (prefers-reduced-motion: reduce)` in surveyResults.css, and this component's `marquee-row group` class ties directly into that hover-pause rule. That's the correct order of operations: build the motion, then gate it, rather than bolting reduced-motion support on after the fact.
  • The `.env.local` block correctly branches between `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` and the legacy `NEXT_PUBLIC_SUPABASE_ANON_KEY` based on what `projectKeys` actually returns. This keeps the generated snippet accurate to the user's real project instead of showing a generic placeholder that would need manual correction.
  • Using `transition-colors` instead of a bare `transition` keeps the animation scoped to color, background, and border-color only. That's correct because it prevents the browser from watching unrelated properties like padding or shadow for changes, avoiding unintended animations on this toggle.

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.