supabase/supabase
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
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
Accessibility
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>
)
})}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>
))}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'
}`}
>Spacing
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 freeUX
"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>Craft
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
Color
Components
Motion
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.