dubinc/dub
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Admin utilities repeat the same gap across every file: solid async logic, loading states, and confirm dialogs, but no visible submit button or input labels anywhere. The biggest risk is these tools only work for whoever already knows the hidden Enter-key shortcut.
Files Rams reviewed
apps/web/app/(ee)/admin.dub.co/(dashboard)/components/ban-link.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/components/delete-partner-account.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/components/impersonate-user.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/components/impersonate-workspace.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/components/reset-login-attempts.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/components/slack-support-invite.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/components/user-info.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/components/refresh-domain.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/components/register-premium-domain.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/components/renew-domain.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/page.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/page.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/fraud/page.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/page.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/paypal/page.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/stablecoin/page.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/programs/sales/page.tsx
apps/web/app/(ee)/admin.dub.co/(dashboard)/revenue/page.tsx
apps/web/app/(ee)/app.dub.co/embed/referrals/page.tsx
apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/page.tsx
apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/success/page.tsx
apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/layout.tsx
apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/page.tsx
apps/web/app/(ee)/partners.dub.co/(auth-login-register)/(generic)/layout.tsx
apps/web/app/(ee)/partners.dub.co/(auth-login-register)/(generic)/login/page.tsx
apps/web/app/(ee)/partners.dub.co/(auth-login-register)/(generic)/register/page.tsx
apps/web/app/(ee)/partners.dub.co/(auth-other)/invite/page.tsx
apps/web/app/(ee)/partners.dub.co/(auth-other)/layout.tsx
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/layout.tsx
Accessibility
Email input has no label, only a placeholder that vanishes on input
The `email` input in `Form` has `id="email"` and `placeholder="panic@thedis.co"` but no `<label htmlFor="email">` anywhere in the tree.
Why it matters
Screen reader users get no announced name for the field once they focus it, and the placeholder disappears the moment a value is typed, so sighted users lose the field's purpose too.
Fix
Add a visible label element tied to the input via htmlFor/id.
<div className="relative flex w-full rounded-md shadow-sm">
<input
name="email"
id="email"
type="email"<div className="relative flex w-full rounded-md shadow-sm">
<label htmlFor="email" className="sr-only">
User email
</label>
<input
name="email"
id="email"
type="email"Submit button loses all focus indication for keyboard users
The "Send invite" button sets `focus:outline-none` but never adds a `ring` width utility, only `hover:bg-neutral-700`. Tabbing to the primary action leaves no visible focus state at all.
Why it matters
Keyboard-only users tabbing through the form can't tell where focus is when they reach the submit button, so they can't confirm the action they're about to trigger before pressing Enter.
Fix
Never remove the outline without a visible focus-visible replacement.
className={cn(
"rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-700 focus:outline-none",
pending && "opacity-50",
)}className={cn(
"rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-500 focus-visible:ring-offset-2",
pending && "opacity-50",
)}aria-invalid is hardcoded to true regardless of the input's real state
The input carries `aria-invalid="true"` as a static prop, not tied to any validation state check.
Why it matters
Screen readers announce every valid entry as an error, so assistive tech users get false failure signals on every interaction and stop trusting the field's feedback.
Fix
Bind aria-invalid to actual validation state or remove it.
placeholder="IG47WZs"
aria-invalid="true"placeholder="IG47WZs"Slug input has no associated label for screen reader users
The `<input id="slug" name="slug" />` sits next to a visual `app.dub.co` prefix span, but nothing announces what the field is for. There is no `<label htmlFor="slug">` and no `aria-label`.
Why it matters
A screen reader user tabs into this field and hears only 'edit text, required' with no indication it expects a workspace slug, so they have to guess or abandon the lookup.
Fix
Associate every form input with a label, using sr-only text when a visual label isn't wanted.
<div className="relative flex w-full rounded-md shadow-sm">
<span className="inline-flex items-center rounded-l-md border border-r-0 border-neutral-300 bg-neutral-50 px-5 text-neutral-500 sm:text-sm">
app.dub.co
</span><div className="relative flex w-full rounded-md shadow-sm">
<label htmlFor="slug" className="sr-only">
Workspace slug
</label>
<span className="inline-flex items-center rounded-l-md border border-r-0 border-neutral-300 bg-neutral-50 px-5 text-neutral-500 sm:text-sm">
app.dub.co
</span>UX
Form has no visible submit button, so mouse-only users can't reset attempts
The `<form>` in `ResetLoginAttempts` wraps a single email `<input>` and a `Form` component with no `<button type="submit">` anywhere. The only way to trigger the reset is pressing Enter inside the input.
Why it matters
Anyone relying on a mouse, touch, or an assistive device that doesn't submit forms via Enter has no way to trigger the action at all: this is a hard lockout for that group, not just an awkward affordance.
Fix
Add a visible, labeled submit button so the primary action is discoverable and clickable, not implicit.
{pending && (
<LoadingSpinner className="absolute inset-y-0 right-2 my-auto h-full w-5 text-neutral-400" />
)}
</div>
);
}; {pending && (
<LoadingSpinner className="absolute inset-y-0 right-2 my-auto h-full w-5 text-neutral-400" />
)}
<button
type="submit"
disabled={pending}
className="ml-2 rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
>
Reset
</button>
</div>
);
};Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeComponents
Destructive checkbox has a 16px hit area with no extended tap zone
The `<input type="checkbox" className="h-4 w-4 ..." />` for "Delete partner account as well" is 16x16px with no padding or extended hit area, sitting in a `flex items-center space-x-2` row.
Why it matters
This checkbox controls whether a partner's entire account is deleted versus just their Stripe connection, one of the highest-consequence toggles on the page, yet its tap target falls well under the 24px WCAG 2.2 AA minimum for touch.
Fix
Wrap the checkbox and label in a single clickable label element with padding to enlarge the effective hit area.
<div className="flex items-center space-x-2">
<input
name="deletePartnerAccount"
id="deletePartnerAccount"
type="checkbox"
disabled={pending}
className="h-4 w-4 rounded border-neutral-300 text-neutral-600 focus:ring-neutral-500"
/>
<label
htmlFor="deletePartnerAccount"
className="text-sm text-neutral-700"
>
Delete partner account as well
</label>
</div><label
htmlFor="deletePartnerAccount"
className="flex cursor-pointer items-center gap-2 py-2"
>
<input
name="deletePartnerAccount"
id="deletePartnerAccount"
type="checkbox"
disabled={pending}
className="h-4 w-4 rounded border-neutral-300 text-neutral-600 focus:ring-neutral-500"
/>
<span className="text-sm text-neutral-700">
Delete partner account as well
</span>
</label>Typography
Color
Spacing
Motion
Craft
Working well
- The confirm() message correctly branches on the checkbox state, telling the admin explicitly whether they're about to delete the full partner account plus Stripe, or just the Stripe express account. Naming the exact consequence before the destructive action fires is exactly what UX-022 asks for, even though it's done through a native dialog rather than a custom one.
- The `Form` component disables the input and swaps in a `LoadingSpinner` while `pending` is true, and `BanButton` passes `loading={pending}` to its own button. This is the right pattern: it prevents double-submits and gives the user visible feedback that the async request is in flight, satisfying both the loading-state and double-submit rules in one move.
- Wrapping the ban action in a native `confirm()` before the fetch fires means a destructive action (banning a user and deleting their workspaces) can't happen from a single accidental click. Pairing that confirmation with a `variant="danger"` button keeps the destructive intent visually distinct from the neutral impersonate action above it.
- The submit button swaps its own label between "Send invite" and "Sending…" while disabling itself and dropping to opacity-50 during the request. This is the correct pattern: state, label, and interactivity all change together so there's no window where a user can double-submit or wonder if the click registered.
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.