shadcn-ui/taxonomy
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Auth and layout pages nail restraint with clean hierarchy and disciplined token use, but polish stops at the surface. The real risk is unfinished edges: empty panels, missing alt text, and no error handling for a billing check that can take down the whole page.
Files Rams reviewed
app/(auth)/login/page.tsx
app/(auth)/register/page.tsx
app/(dashboard)/dashboard/billing/page.tsx
app/(dashboard)/dashboard/layout.tsx
app/(dashboard)/dashboard/page.tsx
app/(docs)/docs/[[...slug]]/page.tsx
app/(docs)/guides/[...slug]/page.tsx
app/(docs)/guides/page.tsx
app/(docs)/layout.tsx
app/(marketing)/[...slug]/page.tsx
app/(marketing)/blog/[...slug]/page.tsx
app/(marketing)/blog/page.tsx
app/(marketing)/layout.tsx
app/(marketing)/page.tsx
app/(marketing)/pricing/page.tsx
app/layout.tsx
app/(auth)/layout.tsx
app/(dashboard)/dashboard/settings/page.tsx
app/(docs)/docs/layout.tsx
app/(docs)/guides/layout.tsx
app/(editor)/editor/[postId]/page.tsx
app/(editor)/editor/layout.tsx
app/api/og/route.tsx
components/billing-form.tsx
components/editor.tsx
components/empty-placeholder.tsx
components/icons.tsx
components/main-nav.tsx
components/mdx-components.tsx
components/mobile-nav.tsx
Spacing
Arbitrary width value breaks the app's spacing scale
The form wrapper uses `sm:w-[350px]`, a bracketed arbitrary value instead of a scale token like `sm:w-96` (384px) or `sm:max-w-sm`.
Why it matters
Arbitrary pixel widths can't be reasoned about alongside the rest of the layout scale, and future edits to this page (or a design-token width scale) will silently miss this one bespoke value.
Fix
Replace bracketed arbitrary widths with the nearest Tailwind scale utility.
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"><div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:max-w-sm">Fixed h-screen container has no scroll fallback for short viewports
The outer container is `h-screen w-screen` with content vertically centered via `justify-center` and no `overflow-y-auto`. The stacked content (logo, heading, subtext, `UserAuthForm`, and the two-line legal paragraph) has no scroll path if it exceeds the available height.
Why it matters
On short viewports (mobile landscape, or when a form validation error adds height), content taller than the screen gets clipped with no way to scroll to the cut-off legal text or the bottom of the form.
Fix
Allow the content column to scroll independently when it exceeds viewport height instead of relying on a fixed h-screen with no overflow handling.
<div className="container grid h-screen w-screen flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0"><div className="container grid min-h-screen w-screen flex-col items-center justify-center overflow-y-auto lg:max-w-none lg:grid-cols-2 lg:px-0">UX
Absolute-positioned Login link can overlap centered content on short screens
The "Login" link is `absolute right-4 top-4 md:right-8 md:top-8`, floating outside the centered flex column that holds the logo, heading, and form.
Why it matters
Because the main content is vertically centered with `justify-center` inside an `h-screen` container, a short viewport (landscape phones, embedded webviews) can push the centered card up into the absolutely positioned "Login" link, overlapping the logo or heading.
Fix
Reserve top padding on the content column equal to the Login link's height so the two can never overlap regardless of viewport height.
<Link
href="/login"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute right-4 top-4 md:right-8 md:top-8"
)}
>
Login
</Link><Link
href="/login"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute right-4 top-4 z-10 md:right-8 md:top-8"
)}
>
Login
</Link>External Stripe docs link has no visual cue that it leaves the app
The "Stripe docs" link opens `target="_blank"` with only an underline style, no icon or text indicating it navigates to an external site.
Why it matters
Users clicking "Stripe docs" get a new tab with no warning, which is disorienting on mobile where tab switching isn't obvious, and screen reader users get no announcement that the link leaves the current context.
Fix
Add a visible external-link indicator (icon or accessible text) to links that open in a new tab.
<a
href="https://stripe.com/docs/testing#cards"
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-8"
>
Stripe docs
</a><a
href="https://stripe.com/docs/testing#cards"
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-8"
>
Stripe docs (opens in new tab)
</a>Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeAccessibility
Twitter card image ships with no alt text for the shared preview
`twitter.images` is a bare string array: `[`${siteConfig.url}/og.jpg`]`. The Twitter card schema supports an object form with an `alt` field, which isn't used here.
Why it matters
Screen reader users who encounter the shared card get no description of the preview image, the same gap A11Y-001 flags for on-page <img> tags.
Fix
Use the object form of the Twitter image with an alt field describing the image.
images: [`${siteConfig.url}/og.jpg`],images: [{ url: `${siteConfig.url}/og.jpg`, alt: siteConfig.name }],Craft
Blank muted panel wastes half the desktop screen with no content
The right-hand panel `<div className="hidden h-full bg-muted lg:block" />` renders as a solid empty block on any viewport ≥1024px, sitting in a `lg:grid-cols-2` layout next to the actual registration form.
Why it matters
On desktop, half the viewport carries zero information: no brand statement, no testimonial, no product context. The eye has nothing to land on there, and the form column gets squeezed into a narrow `sm:w-[350px]` well while an equal-width void sits beside it.
Fix
Give the empty panel a purpose (brand image, quote, or product shot) or collapse the grid to a single centered column until content exists.
<div className="hidden h-full bg-muted lg:block" /><div className="hidden h-full bg-muted lg:flex lg:items-end lg:p-10">
{/* brand image, quote, or testimonial content goes here */}
</div>Typography
Color
Components
Motion
Working well
- Composing the page from `DashboardShell`, `DashboardHeader`, and `BillingForm` instead of inlining markup keeps this page thin and delegates the actual plan-management complexity to a dedicated component, which is the right split for a page that will only grow more billing states over time.
- Every color and spacing value in this file comes from tokens and the Tailwind scale (`text-muted-foreground`, `space-y-6`, `sm:w-[350px]`) with zero hardcoded hex or arbitrary pixel values, which keeps the auth flow themeable and consistent with the rest of the app.
- Pairing `suppressHydrationWarning` on `<html>` with `ThemeProvider attribute="class"` is the correct next-themes pattern: it silences the expected hydration mismatch from client-side theme class injection without masking real hydration bugs elsewhere.
- The "Login" link uses `buttonVariants({ variant: "ghost" })` instead of a filled button, correctly signaling it's a secondary navigation path away from the primary task (creating an account) rather than competing with the form's submit action.
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.