supabase on GitHub

supabase/supabase

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

30 files reviewed·August 1, 2026

View on GitHub

Elevated

Design risk in this codebase.

7issues
Critical, Serious & Moderate · top 6 shown below

Top fix

Add reduced-motion guard to the infinite progress bar animation

See the fix

Verdict

Decorative motion outruns accessibility here, animations and marquees keep running for the users least able to escape them. The biggest risk is a critical reduced-motion gap sitting right next to otherwise sound, simple layouts.

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

95/100

Motion

1 critical1 serious
MotionCritical

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

Progress bar animation loops forever with no reduced-motion guard

The animated foreground bar in ProgressBar sets animation: terminalLine 10s steps(8, end) ... infinite directly in inline style, with no prefers-reduced-motion check anywhere in the component or its callers.

Why it matters

Users with vestibular disorders who set prefers-reduced-motion: reduce still get an infinite stepped animation running indefinitely on every render of this bar, which is exactly the class of motion that guideline exists to stop.

Fix

Gate any infinite or auto-playing animation behind a prefers-reduced-motion media query that disables or freezes it.

style={{
          maskImage: 'url("/images/state-of-startups/pattern-checker.svg")',
          maskSize: '4px',
          maskRepeat: 'repeat',
          maskPosition: 'top left',
          clipPath: 'inset(0 100% 0 0)',
          animation: `terminalLine 10s steps(8, end) ${animationDelay} infinite ${reverse ? 'reverse' : ''}`,
          animationFillMode: 'both',
        }}
style={{
          maskImage: 'url("/images/state-of-startups/pattern-checker.svg")',
          maskSize: '4px',
          maskRepeat: 'repeat',
          maskPosition: 'top left',
          clipPath: 'inset(0 100% 0 0)',
          animation: `terminalLine 10s steps(8, end) ${animationDelay} infinite ${reverse ? 'reverse' : ''}`,
          animationFillMode: 'both',
        }}
        className="motion-reduce:animate-none motion-reduce:[clip-path:inset(0)]"
MotionSerious

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

Marquee only pauses on mouse hover, so keyboard focus keeps sliding away

surveyResults.css defines a hover pause on .marquee-row, but the row and its Link children in ParticipantsCarousel have no focus-within equivalent, so the CSS only stops the scroll on :hover.

Why it matters

A keyboard user who tabs onto a company link inside the marquee gets a focus ring that immediately scrolls out of view as the track keeps animating, making it impossible to read or reliably activate the focused link.

Fix

Pause continuously animating content on keyboard focus the same way it pauses on hover.

className="marquee-row group relative w-full overflow-hidden"
className="marquee-row group relative w-full overflow-hidden focus-within:[&_.marquee-track]:[animation-play-state:paused]"
96/100

UX

2 serious
UXSerious

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

Logout button gives no feedback while signOut is in flight

The "Logout" button in protected/index.vue calls the async handleLogout, which awaits supabase.auth.signOut() before redirecting, but the button never disables or shows a pending state during that await.

Why it matters

A user who taps "Logout" twice before the network call resolves fires signOut() twice and can hit a race between the second call and the router.replace redirect, producing an inconsistent auth state or a confusing flash before landing on /auth/login.

Fix

Disable the trigger and show pending state for any button that starts an async action with a visible side effect.

const handleLogout = async () => {
  await supabase.auth.signOut()
  router.replace("/auth/login")
}
const loggingOut = ref(false)
const handleLogout = async () => {
  if (loggingOut.value) return
  loggingOut.value = true
  await supabase.auth.signOut()
  router.replace("/auth/login")
}
UXSerious

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

Root wrapper uses h-screen, breaking centering under mobile browser chrome

The outer div wrapping the "Checking authentication..." and logged-in states uses class="flex h-screen w-full items-center justify-center". h-screen resolves to the static 100vh, which does not account for mobile Safari's collapsing address bar.

Why it matters

On iOS Safari the content can render taller than the visible viewport when the browser chrome is expanded, pushing the centered "Hello {{ email }}" block and Logout button partly under the address bar or nav controls.

Fix

Use min-h-screen or the dvh unit for full-bleed vertical centering on mobile viewports.

<div class="flex h-screen w-full items-center justify-center">
<div class="flex min-h-dvh w-full items-center justify-center">

Get this score on every PR.

Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.

Install Rams free
98/100

Accessibility

1 serious
AccessibilitySerious

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

Cohort toggle pills fall well under the 44px touch target minimum

The cohort toggle buttons (rendered from CohortToggleOption.label, e.g. any option text) use className with px-3 py-1 and text-xs, which renders roughly 24-26px tall.

Why it matters

Touch targets under ~26px height are hard to hit precisely on a phone, increasing mis-taps between adjacent pills and slowing down cohort selection on mobile.

Fix

Give tappable pill controls a minimum height of 44px through padding or a min-h utility.

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'
              }`}
className={`min-h-[44px] px-3 py-2.5 text-xs font-mono uppercase tracking-wider border rounded-full transition-colors flex items-center ${
                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

Components

1 serious
ComponentsSerious

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

Toggle button variant logic is a raw ternary template literal, not a shared helper

The active/inactive className on the cohort toggle button is built with a hand-rolled template literal ternary instead of a cn()/clsx style class-merge helper.

Why it matters

As more variants get added (disabled, hover-only, loading), string concatenation like this becomes error-prone to edit and hard to diff, raising the maintenance cost of every future style change to this component.

Fix

Compose conditional Tailwind classes through a cn() helper instead of manual template literal ternaries.

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'
              }`}
className={cn(
                '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'
              )}

Typography

No issues found

Color

No issues found

Spacing

No issues found

Craft

No issues found

Working well

  • The cohort toggle's active state (border-brand-500/40 bg-brand-300/40 text-brand-link) versus the inactive outline (border-overlay text-foreground-light) gives a real tinted-fill versus plain-outline distinction, and the label text itself still identifies the selected filter, so the signal isn't color-only.
  • The participants shuffle uses shuffleSeeded(participants, 1) with a comment explaining it exists to keep SSR and client renders deterministic, which is a clean, deliberate fix for a real hydration-mismatch risk rather than an arbitrary Math.random call.
  • Redirecting with router.replace("/auth/login") instead of router.push in both the auth check and handleLogout correctly prevents the back button from returning a signed-out user to the protected page.

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: 59/100.

This page is an automated design review of supabase/supabase’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.

More design scores

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.

Or get a design review on every pull requestInstall Rams