nextjs/saas-starter
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
24 files reviewed·July 25, 2026
Elevated
Design risk in this codebase.
More findings
Verdict
Buttons you can't read and a delete flow with no brakes define this build. The biggest risk is contrast and confirmation failures on the exact actions, saving and deleting, where trust matters most.
Files Rams reviewed
app/(dashboard)/dashboard/activity/page.tsx
app/(dashboard)/dashboard/general/page.tsx
app/(dashboard)/dashboard/layout.tsx
app/(dashboard)/dashboard/page.tsx
app/(dashboard)/dashboard/security/page.tsx
app/(dashboard)/layout.tsx
app/(dashboard)/page.tsx
app/(dashboard)/pricing/page.tsx
app/layout.tsx
app/(login)/sign-in/page.tsx
app/(login)/sign-up/page.tsx
app/(dashboard)/terminal.tsx
app/(login)/login.tsx
app/globals.css
app/not-found.tsx
components/ui/avatar.tsx
components/ui/button.tsx
components/ui/card.tsx
components/ui/dropdown-menu.tsx
components/ui/radio-group.tsx
app/(dashboard)/dashboard/activity/loading.tsx
app/(dashboard)/pricing/submit-button.tsx
components/ui/input.tsx
components/ui/label.tsx
Color
app/(dashboard)/dashboard/general/page.tsx:104
"Save Changes" button text fails contrast against its own fill
The submit button reading "Save Changes" uses white text on bg-orange-500 (~1.3:1) and bg-orange-600 on hover (~1.3:1). Both sit below the 4.5:1 minimum for normal-size text.
Why it matters
Users with low vision or in bright ambient light can't read the primary action label on the account settings form, and this is the only submit control on the page.
Fix
Darken the fill or switch to a token pairing that clears 4.5:1 for white text.
className="bg-orange-500 hover:bg-orange-600 text-white"className="bg-orange-700 hover:bg-orange-800 text-white"app/(dashboard)/dashboard/page.tsx:227
"Invite Member" button text is unreadable at normal size
The invite form's submit button uses the same white-on-orange-500 pairing (~2.8:1 contrast), the only path a team owner has to send an invitation.
Why it matters
A primary action button that's hard to read undermines confidence the invite actually went through, and fails WCAG AA for anyone with reduced contrast sensitivity.
Fix
Darken the fill or switch to a token pairing that clears 4.5:1 for white text.
className="bg-orange-500 hover:bg-orange-600 text-white"className="bg-orange-700 hover:bg-orange-800 text-white"Accessibility
app/layout.tsx:12
Viewport export blocks pinch-to-zoom for every visitor
The root layout's viewport export sets maximumScale: 1 with no accompanying input-zoom bug to justify it. On browsers that honor this meta value, users cannot pinch-zoom anywhere in the app, including dashboard forms and settings pages.
Why it matters
Low-vision users rely on pinch-zoom to read small text and hit tap targets accurately. Locking zoom removes a core accessibility mechanism sitewide, not just on one page.
Fix
Remove maximumScale from the viewport config so native pinch-zoom stays available.
export const viewport: Viewport = {
maximumScale: 1
};export const viewport: Viewport = {
width: 'device-width',
initialScale: 1
};app/(dashboard)/dashboard/layout.tsx:32
Mobile menu toggle never announces open or closed state
The "Toggle sidebar" button drives isSidebarOpen via onClick but exposes no aria-expanded attribute, so its state never reaches the accessibility tree.
Why it matters
Screen reader users get no announcement of whether the drawer is open or closed after activating the control, forcing them to re-explore the DOM to confirm the result of their own action.
Fix
Bind aria-expanded to the same boolean that controls the drawer's visibility.
<Button
className="-mr-3"
variant="ghost"
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
>
<Menu className="h-6 w-6" />
<span className="sr-only">Toggle sidebar</span>
</Button><Button
className="-mr-3"
variant="ghost"
aria-expanded={isSidebarOpen}
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
>
<Menu className="h-6 w-6" />
<span className="sr-only">Toggle sidebar</span>
</Button>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 freeUX
app/(dashboard)/dashboard/security/page.tsx:144
"Delete Account" submits instantly with no confirmation step
The security page's "Delete Account" button is type="submit" inside a form that fires on click or on Enter from the password field above it. There is no intermediate confirm dialog before the account is destroyed.
Why it matters
A misclick, an accidental Enter keypress while typing the password, or a double-tap permanently deletes the account with no recovery path, the highest-stakes action on the page treated like any other form submit.
Fix
Insert a confirmation dialog between the button click and the destructive form action.
<Button
type="submit"
variant="destructive"
className="bg-red-600 hover:bg-red-700"
disabled={isDeletePending}
><Button
type="button"
variant="destructive"
className="bg-red-600 hover:bg-red-700"
disabled={isDeletePending}
onClick={() => setShowConfirmDialog(true)}
>app/(dashboard)/dashboard/activity/page.tsx:71
Activity log has no fallback if the data fetch fails
ActivityPage awaits getActivityLogs() directly in the server component body with no try/catch and no loading or error branch, only a check on logs.length for the empty state.
Why it matters
A slow or failing database call surfaces as a blank page or an unstyled framework error screen instead of a recognizable error state, leaving users unsure whether their activity history is gone or just loading.
Fix
Wrap the fetch in error handling and render a distinct error state separate from the empty-list state.
export default async function ActivityPage() {
const logs = await getActivityLogs();export default async function ActivityPage() {
let logs;
try {
logs = await getActivityLogs();
} catch {
return <ActivityErrorState />;
}Typography
Spacing
Components
Motion
Craft
Working well
- InviteTeamMember correctly disables the form and shows a CardFooter explanation when !isOwner, giving non-owners a clear reason instead of a silently broken form.
- The password fields correctly use autoComplete="current-password" and "new-password", which lets password managers fill and suggest strong new passwords.
- Using SWRConfig fallback with server-fetched user/team data is a clean pattern for avoiding client-side loading flicker on first paint.
- The sidebar slide uses transform (translate-x) rather than animating left/width, which keeps the drawer transition GPU-accelerated.
Scored July 25, 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 nextjs/saas-starter’s UI code: 24 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.