resend/react-email
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Component-level discipline is solid, typed primitives, real alt text, but global layout defaults quietly undercut the whole app: dark mode never reaches browser chrome, line height is stretched everywhere, and uncompressed fonts load on every page. The biggest risk is that these root-layout mistakes compound silently across every screen, while boilerplate metadata and a glowing logo signal nobody did a final pass.
Files Rams reviewed
apps/web/src/app/components/[slug]/page.tsx
apps/web/src/app/components/page.tsx
packages/react-email/src/components/tailwind/e2e/nextjs/src/app/layout.tsx
packages/react-email/src/components/tailwind/e2e/nextjs/src/app/page.tsx
apps/web/src/app/components/get-imported-components-for.tsx
apps/web/src/app/editor/page.tsx
apps/web/src/app/layout.tsx
apps/web/src/app/templates/page.tsx
packages/ui/src/app/layout.tsx
packages/ui/src/app/page.tsx
packages/ui/src/app/preview/[...slug]/page.tsx
apps/web/src/app/editor/basic-editor/page.tsx
apps/web/src/app/editor/bubble-menu/page.tsx
apps/web/src/app/editor/buttons/page.tsx
apps/web/src/app/editor/column-layouts/page.tsx
apps/web/src/app/editor/custom-bubble-menu/page.tsx
apps/web/src/app/editor/custom-extensions/page.tsx
apps/web/src/app/editor/custom-theme/page.tsx
apps/web/src/app/editor/email-export/page.tsx
apps/web/src/app/editor/email-theming/page.tsx
apps/web/src/app/editor/full-email-builder/page.tsx
apps/web/src/app/editor/image-upload/page.tsx
apps/web/src/app/editor/inspector-composed/page.tsx
apps/web/src/app/editor/inspector-custom/page.tsx
apps/web/src/app/editor/inspector-defaults/page.tsx
apps/web/src/app/editor/layout.tsx
apps/web/src/app/editor/link-editing/page.tsx
apps/web/src/app/editor/slash-commands/page.tsx
apps/web/src/app/editor/standalone-editor-full/page.tsx
apps/web/src/app/editor/standalone-editor-inspector/page.tsx
Craft
Oversized glow behind the logo reads as decoration, not elevation
The logo's shadow is `0 .625rem 12.5rem 1.25rem` (10px offset, 200px blur, 20px spread) at 50% opacity, a 20x blur-to-offset ratio.
Why it matters
At that ratio the shadow smears into a soft haze around the mark instead of reading as a lifted surface, which is the classic 'glow' substitute for real hierarchy rather than an intentional elevation cue.
Fix
Keep shadows tight and directional: small offset, modest blur, layered sharp-to-soft, instead of one huge low-opacity blur.
boxShadow: '0 .625rem 12.5rem 1.25rem #2B7CA080',boxShadow: '0 0.25rem 0.5rem rgba(43,124,160,0.15), 0 0.5rem 1.5rem rgba(43,124,160,0.15)',Default boilerplate metadata ships as real page title and description
The `metadata` export sets `title: 'Create Next App'` and `description: 'Generated by create next app'`, the unmodified create-next-app defaults.
Why it matters
Unedited placeholder copy in the document title and meta description ships to browser tabs, search results, and social previews, signaling the page was never finished.
Fix
Replace placeholder metadata with copy that names the actual app or test target.
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};export const metadata: Metadata = {
title: 'Tailwind Email E2E',
description: 'End-to-end test harness for the Tailwind email component',
};Accessibility
Rendered email HTML has no landmark or heading context for this test page
The page renders `<div dangerouslySetInnerHTML={{ __html: emailHtml }} />` as its only content, with no `<main>` wrapper or page heading around the injected email markup.
Why it matters
This is an e2e test harness page, so the immediate user impact is limited, but screen reader users and automated a11y checks landing on this route get no page-level structure to orient with before hitting the email content.
Fix
Wrap injected third-party markup in a landmark element so the page has a navigable structure.
return <div dangerouslySetInnerHTML={{ __html: emailHtml }} />;return <main dangerouslySetInnerHTML={{ __html: emailHtml }} />;Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeTypography
Global leading-loose stretches line height across every screen in the app
`<body>` sets `leading-loose` (line-height 2) as the base for all text in the product, with no per-element override visible in this file.
Why it matters
A line-height of 2 is built for airy marketing copy, not dense UI text like lists, labels, or table rows; every component built on top of this default inherits oversized vertical rhythm unless it explicitly fights the inheritance, which compounds into inconsistent spacing across the app.
Fix
Set body leading to a UI-appropriate default (leading-normal or leading-relaxed) and apply leading-loose locally only where long-form copy needs it.
<body className="relative h-screen bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12"><body className="relative h-screen bg-black text-slate-11 leading-normal selection:bg-cyan-5 selection:text-cyan-12">Color
Dark theme never reaches the browser chrome or form controls
The `<html>` element sets `color-scheme="dark"` as a JSX/HTML attribute rather than the CSS property. Browsers only read `color-scheme` as a CSS declaration (or a `<meta name="color-scheme">` tag), so this attribute is inert markup.
Why it matters
Scrollbars, form control defaults, and native UI stay in light mode against the `bg-black` body, producing a visible light-on-dark mismatch for every user on this all-dark site.
Fix
Set color-scheme through CSS or a meta tag, not an HTML attribute.
<html
className={`${inter.variable} ${commitMono.variable} antialiased`}
lang="en"
color-scheme="dark"
><html
className={`${inter.variable} ${commitMono.variable} antialiased`}
lang="en"
style={{ colorScheme: 'dark' }}
>UX
Invalid category slug renders an unstyled, dead-end paragraph
When `foundCategory` isn't found, the page returns `<p>Component category not found.</p>` with no `PageWrapper`, no heading, and no link back to `/components`.
Why it matters
The user lands outside the entire site shell (no nav, no styling) with zero path forward, so a bad or outdated link becomes a total dead end instead of a recoverable moment.
Fix
Render error states inside the same layout shell with a heading and a link back to the working list.
if (!foundCategory) {
return <p>Component category not found.</p>;
}if (!foundCategory) {
return (
<PageWrapper>
<div className="flex flex-col items-center gap-4 px-6 py-24 text-center">
<Heading size="6" weight="medium" className="text-slate-12">
Component category not found
</Heading>
<Link className="text-slate-11 underline hover:text-slate-12" href="/components">
Back to all components
</Link>
</div>
</PageWrapper>
);
}Spacing
Components
Motion
Working well
- The `Button asChild` wrapping a `Link` to "Check the docs" keeps the component semantically an anchor while inheriting the button's visual and focus treatment, which is the correct pattern: users get real link behavior (right-click, open in new tab, crawlable href) without losing button styling.
- Using `<Heading size="6" weight="medium">` for the category title instead of a raw `text-2xl font-medium` span means the type scale lives in one component's API. If the scale ever changes, every page that imports `Heading` updates together instead of needing a find-and-replace across the app.
- The 'Back' link's states are built entirely from one token family: `text-slate-11` at rest, `hover:text-slate-12` on hover, `focus:bg-slate-6` plus `focus:ring-slate-3` on focus. Staying inside one gray scale for every interaction state is exactly how a system avoids drift between pages.
- The `<html lang="en">` attribute is set correctly on the root layout, which is exactly where it belongs per A11Y-014. Screen readers and browsers use this to pick pronunciation and hyphenation rules, and it's easy to forget on scaffold files like this one.
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.