onlook-dev/onlook
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Solid architecture and choreography mask a pattern of one-line polish getting skipped: contrast, motion guards, live regions all left undone. The biggest risk is silent failure states, a network hiccup and a real no-subscription case look identical, quietly misrouting paying users.
Files Rams reviewed
apps/web/client/src/app/about/layout.tsx
apps/web/client/src/app/auth/redirect/page.tsx
apps/web/client/src/app/callback/github/install/page.tsx
apps/web/client/src/app/faq/layout.tsx
apps/web/client/src/app/features/ai-for-frontend/layout.tsx
apps/web/client/src/app/features/ai/layout.tsx
apps/web/client/src/app/features/ai/page.tsx
apps/web/client/src/app/features/builder/layout.tsx
apps/web/client/src/app/features/builder/page.tsx
apps/web/client/src/app/features/layout.tsx
apps/web/client/src/app/features/page.tsx
apps/web/client/src/app/features/prototype/layout.tsx
apps/web/client/src/app/layout.tsx
apps/web/client/src/app/login/page.tsx
apps/web/client/src/app/page.tsx
apps/web/client/src/app/pricing/page.tsx
apps/web/client/src/app/project/[id]/layout.tsx
apps/web/client/src/app/project/[id]/page.tsx
apps/web/client/src/app/projects/_components/select/masonry-layout.tsx
apps/web/client/src/app/projects/import/github/page.tsx
apps/web/client/src/app/projects/import/local/page.tsx
apps/web/client/src/app/projects/import/page.tsx
apps/web/client/src/app/projects/layout.tsx
apps/web/client/src/app/see-a-demo/page.tsx
apps/web/client/src/app/site-map/layout.tsx
apps/web/client/src/app/site-map/page.tsx
apps/web/client/src/app/workflows/claude-code/layout.tsx
apps/web/client/src/app/workflows/layout.tsx
apps/web/client/src/app/workflows/page.tsx
apps/web/client/src/app/workflows/vibe-coding/layout.tsx
Color
Forced dark theme leaves native browser chrome light
The app forces dark theme via `forcedTheme="dark"` on `ThemeProvider`, but the `<html>` element sets no `color-scheme` value and there is no `<meta name="color-scheme">` tag.
Why it matters
Scrollbars, form control defaults, and the browser's own UI chrome render in light mode against the app's dark surfaces, producing a visible mismatch at every native control edge.
Fix
Set color-scheme to dark on the root html element to match the forced theme.
<html lang={locale} className={inter.variable} suppressHydrationWarning><html lang={locale} className={inter.variable} style={{ colorScheme: 'dark' }} suppressHydrationWarning>White success icon on bg-green-500 fails non-text contrast
The success state's `CheckCircled` icon renders `text-white` inside a `bg-green-500` circle. White on green-500 (#22c55e) computes to 2.28:1.
Why it matters
WCAG 1.4.11 requires 3:1 contrast for meaningful graphical objects; at 2.28:1 the checkmark is hard to distinguish from its own background for low-vision users, undermining the one visual cue that confirms success.
Fix
Darken the status background so the fixed white icon clears the 3:1 non-text contrast minimum.
indicatorColor="bg-green-500"indicatorColor="bg-green-600"UX
Hero locked to h-screen crops content behind mobile browser chrome
The same hero wrapper sets `h-screen` (100vh) with `flex items-center justify-center`. On mobile Safari and Chrome, 100vh is calculated against the maximum viewport, not the visible area once the address bar is showing, so content can render taller than what's actually visible.
Why it matters
On first load, mobile visitors can see the hero's CTA or heading pushed below the fold or clipped by browser chrome, which is the worst place for it to happen given this is the page's entry point.
Fix
Use the dynamic viewport unit (h-dvh) so hero height tracks the actual visible viewport on mobile.
<div className="w-screen h-screen flex items-center justify-center" id="hero"><div className="w-full h-dvh flex items-center justify-center" id="hero">Success screen has no manual way out if window.close() is blocked
The success `StateContainer` passes no `actions`, relying solely on `setTimeout(() => window.close(), 3000)`. Browsers frequently block scripted `window.close()` on tabs not opened purely by script (e.g. after a redirect flow).
Why it matters
If the auto-close is blocked, the user is left staring at "All set!" with no button, no link, and no indication anything else needs to happen, turning a successful flow into a dead end.
Fix
Give every reachable state a designed action, including a manual fallback for auto-dismiss behavior that browsers can silently block.
{state === 'success' && (
<StateContainer
indicatorColor="bg-green-500"
indicatorIcon={Icons.CheckCircled}
iconAnimated={true}
title="All set!"
description="Your GitHub account is now connected"
/>
)}{state === 'success' && (
<StateContainer
indicatorColor="bg-green-500"
indicatorIcon={Icons.CheckCircled}
iconAnimated={true}
title="All set!"
description="Your GitHub account is now connected"
actions={
<Button variant="outline" onClick={() => window.close()} className="w-full">
Close this tab
</Button>
}
/>
)}Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeAccessibility
"Redirecting..." status has no live region for screen reader users
The heading "Redirecting..." and paragraph "Please wait while we redirect you back." render as plain `<h1>`/`<p>` with no `role="status"` or `aria-live`, while two queries and a localforage read run in the background before navigation happens.
Why it matters
If the redirect takes a couple seconds (two network queries plus storage access), screen reader users get no indication anything is happening and no confirmation when it resolves, since nothing announces the wait state.
Fix
Wrap the transitional status text in a container with role='status' so assistive tech announces the wait state as it renders.
<div className="text-center">
<h1 className="text-2xl font-semibold mb-4">Redirecting...</h1>
<p className="text-foreground-secondary">Please wait while we redirect you back.</p>
</div><div className="text-center" role="status">
<h1 className="text-2xl font-semibold mb-4">Redirecting...</h1>
<p className="text-foreground-secondary">Please wait while we redirect you back.</p>
</div>Components
Hardcoded grays on Card override the component's own theming
`Card` is styled with `className="bg-gray-900 border-gray-800 shadow-2xl"` while the rest of the page uses semantic tokens (`text-foreground-primary`, `text-foreground-secondary`). The outer wrapper also uses `bg-gray-800` boxes and `border-gray-800`.
Why it matters
Mixing raw Tailwind gray classes with semantic foreground tokens means the Card no longer follows the design system's theme source; a future dark/light theme change updates the tokens everywhere except this page, and maintenance cost grows every time this pattern is copied.
Fix
Style Card and its surrounding chrome through the same semantic tokens already used for text, not raw Tailwind gray values.
<Card className="bg-gray-900 border-gray-800 shadow-2xl"><Card className="bg-background-primary border-border shadow-2xl">Typography
Spacing
Motion
Craft
Working well
- The section order (Hero, ResponsiveMockupSection, WhatCanOnlookDoSection, ContributorSection, TestimonialsSection, FAQSection, CTASection) sequences the page correctly: product demonstration and features come before social proof, and the CTA lands only after proof has been established. That's the right shape for a conversion narrative, claim then evidence then ask.
- The header logo bridge (Onlook logo, dots, GitHub logo) sits outside the `AnimatePresence` boundary while only `key={state}` on the inner motion.div re-animates. This is the correct pattern: persistent chrome never re-triggers its entrance animation when the card content below it swaps between loading, success, and error.
- Keeping AuthModal, NonProjectSettingsModal, and SubscriptionModal as sibling components mounted once at the page root, rather than inlined inside individual sections, keeps page.tsx readable as pure composition and avoids duplicating modal instances across the tree.
- Using `disableTransitionOnChange` on `ThemeProvider` is the right call: it suppresses the color transition that would otherwise animate across every element the instant the theme resolves on load, which is exactly the flash MOTION-017 warns about.
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.