antiwork/gumroad
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 23, 2026
Low
Design risk in this codebase.
More findings
Verdict
Polished components sit on top of silent failure paths. Buttons are labelled and states look right, but wrong emails, dead mailto links, and duplicate CTAs leave users stranded with no signal.
Files Rams reviewed
app/javascript/pages/HelpCenter/Layout.tsx
app/javascript/pages/UrlRedirects/ConfirmPage.tsx
app/javascript/pages/UrlRedirects/DownloadPage.tsx
app/javascript/pages/UrlRedirects/UnavailablePageLayout.tsx
app/javascript/components/AffiliatedPage.tsx
app/javascript/components/Analytics/AnalyticsLayout.tsx
app/javascript/components/ApiDocumentation/Endpoints/PublicProductPage.tsx
app/javascript/components/Authentication/Layout.tsx
app/javascript/components/BundleEdit/Layout.tsx
app/javascript/components/CheckoutDashboard/Layout.tsx
app/javascript/components/Collaborators/Layout.tsx
app/javascript/components/CollabsPage.tsx
app/javascript/components/Developer/Layout.tsx
app/javascript/components/Discover/Layout.tsx
app/javascript/components/Download/PageListLayout.tsx
app/javascript/components/EmailAction/Layout.tsx
app/javascript/components/EmailsPage/Layout.tsx
app/javascript/components/Library/Layout.tsx
app/javascript/components/Product/Layout.tsx
app/javascript/components/ProductsLayout.tsx
app/javascript/components/ProductsPage.tsx
app/javascript/components/Profile/EditPage.tsx
app/javascript/components/Profile/Layout.tsx
app/javascript/components/Public/LookupLayout.tsx
app/javascript/components/Settings/Layout.tsx
app/javascript/components/WorkflowsPage/Layout.tsx
app/javascript/components/server-components/ProductEditPage.tsx
app/javascript/pages/Admin/ActionCallDashboard/Index.tsx
app/javascript/pages/Admin/ApiTokens/Index.tsx
app/javascript/pages/Admin/Base/Index.tsx
UX
app/javascript/pages/UrlRedirects/ConfirmPage.tsx:58
No error path exists when the entered email doesn't match a purchase
`useForm` destructures `data, setData, post, processing` but never `errors`, and `EmailConfirmation` renders no error message anywhere. If the server rejects the submitted email, the UI shows no feedback.
Why it matters
A user who mistypes or forgets which email they used gets no indication the submission failed. They're left resubmitting the same form or assuming the page is broken, with no way to correct course.
Fix
Surface server-side validation errors inline near the field that caused them.
const { data, setData, post, processing } = useForm({
id: confirmation_info.id,
destination: confirmation_info.destination ?? "",
display: confirmation_info.display ?? "",
email: confirmation_info.email ?? "",
authenticity_token,
});const { data, setData, post, processing, errors } = useForm({
id: confirmation_info.id,
destination: confirmation_info.destination ?? "",
display: confirmation_info.display ?? "",
email: confirmation_info.email ?? "",
authenticity_token,
});app/javascript/pages/HelpCenter/Layout.tsx:27
"Email support" mailto link fails silently with no visible address to fall back on
Line 27 renders `<a href={mailto:${SUPPORT_EMAIL}}>Email support</a>`. The link text is generic and never shows the actual address.
Why it matters
On a device or browser with no mail client configured, clicking does nothing and gives no feedback: since the address itself is never shown, the user has no way to email support manually.
Fix
Show the real email address as the link text so it remains usable even when the mailto handler fails to open.
<a href={`mailto:${SUPPORT_EMAIL}`}>Email support</a><a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a>app/javascript/pages/UrlRedirects/UnavailablePageLayout.tsx:15
Malformed props crash the unavailable-page layout itself
`useUnavailablePageProps` calls `typia.assert<UnavailablePageProps>(usePage().props)` directly, with no try/catch or fallback rendering around it.
Why it matters
If Inertia ever sends props that don't match `UnavailablePageProps`, `typia.assert` throws and the page hard-crashes with an unhandled exception, on the exact component whose job is to show a graceful 'unavailable' message to the user.
Fix
Validate props defensively and render a fallback state instead of letting an assertion throw uncaught on a user-facing error page.
export const useUnavailablePageProps = () => typia.assert<UnavailablePageProps>(usePage().props);export const useUnavailablePageProps = () => {
const result = typia.validate<UnavailablePageProps>(usePage().props);
if (!result.success) throw new Error("Invalid UnavailablePageProps");
return result.data;
};app/javascript/pages/UrlRedirects/ConfirmPage.tsx:96
Email input renders no inline error even though errors are now tracked
The `Input` for `confirm-email` has no error slot below it. Rendering `errors.email` next to the field (once destructured from `useForm`) is the missing piece that turns the tracked error into visible feedback.
Why it matters
Validation state that exists in code but never reaches the screen is functionally the same as having no validation at all for the person using the form.
Fix
Render field-level errors directly beneath the input they belong to, with `role="alert"` so assistive tech announces it.
<Input
id="confirm-email"
type="email"
name="email"
autoComplete="email"
placeholder="Email address"
onChange={(e) => setData("email", e.target.value)}
defaultValue={confirmation_info.email ?? ""}
/>
<Button type="submit" color="accent" disabled={processing}><Input
id="confirm-email"
type="email"
name="email"
autoComplete="email"
placeholder="Email address"
onChange={(e) => setData("email", e.target.value)}
defaultValue={confirmation_info.email ?? ""}
/>
{errors.email ? (
<p role="alert" className="text-red">
{errors.email}
</p>
) : null}
<Button type="submit" color="accent" disabled={processing}>app/javascript/pages/HelpCenter/Layout.tsx:20
Two solid support buttons give users no reason to pick one over the other
The header actions render two buttons: - "Email support" (`<Button asChild>` wrapping a mailto anchor, no explicit color) - "Contact support" (`<Button color="accent">`, opens a modal) Both sit in the same row with no copy explaining what separates a direct email from the modal flow.
Why it matters
When two CTAs address the same need with no stated difference, users spend extra time deciding instead of acting, and some will pick the wrong one and have to backtrack.
Fix
Give each support path a line of copy or a visually secondary treatment so one action reads as the default and the other as an alternative.
Accessibility
app/javascript/pages/UrlRedirects/UnavailablePageLayout.tsx:26
Placeholder image ships with no alt text at the call site
`<PlaceholderImage src={placeholderImage} />` is rendered with no `alt` prop passed in. Nothing at this call site tells assistive tech whether the comic-stars image is decorative or informative.
Why it matters
If the underlying component defaults to an empty or missing alt, screen reader users on this unavailable-page state get either silence or a raw filename read aloud, right when they most need the page's message to be clear.
Fix
Pass an explicit alt value at the call site: empty string if purely decorative, descriptive text if it conveys meaning.
<PlaceholderImage src={placeholderImage} /><PlaceholderImage src={placeholderImage} alt="" />Typography
Color
Spacing
Components
Motion
Craft
Working well
- The polling lifecycle is cleanly scoped: audioDurationsPoll only starts while hasUnprocessedAudio is true and stops otherwise, and the same pattern applies to mediaLocationsPoll. This keeps background network activity tied to actual need rather than running indefinitely, which is the right default for a download page a user may leave open.
- The submit button switches its own label to "Confirming..." and sets `disabled={processing}` in the same breath. This is the correct pattern for async actions: it gives visible feedback and blocks a second click from firing a duplicate request, solving two problems with one state variable.
- The form ships a real `action` and `method="post"` plus a hidden `authenticity_token`, so it still works as a standard HTML form submission if JavaScript fails, while `onSubmit` hijacks it for Inertia in the normal case. That's a resilient way to layer a JS framework over a plain form.
- The email input has a `label` with `htmlFor="confirm-email"` matching the input's `id`, even though it's visually hidden with `sr-only`. Screen reader users still get a proper accessible name, which is the part that actually matters when a design calls for no visible label.
Scored July 23, 2026 with Rams Engine v0.0.2 · Engine changelog
First scored July 23, 2026: 59/100. This rescore on v0.0.2: 86/100.
This page is an automated design review of antiwork/gumroad’s UI code: 30 files read against 291 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.