documenso/documenso
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
Elevated
Design risk in this codebase.
More findings
Verdict
A signing flow with good bones and unfinished edges: creation and auth screens fall apart the moment something fails or takes time. The infinite spinner with no reduced-motion exit is the sharpest risk, since it strands sensitive users mid-signature with no way out.
Files Rams reviewed
apps/remix/app/components/general/direct-template/direct-template-page.tsx
apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx
apps/remix/app/components/general/document-signing/csc-recipient-blocked-page.tsx
apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx
apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx
apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx
apps/remix/app/components/general/generic-error-layout.tsx
apps/docs/src/app/(home)/page.tsx
apps/docs/src/app/docs/[[...slug]]/page.tsx
apps/docs/src/app/docs/layout.tsx
apps/docs/src/app/layout.tsx
apps/remix/app/components/general/direct-template/direct-template-invalid-page.tsx
apps/remix/app/components/dialogs/account-delete-dialog.tsx
apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx
apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
apps/remix/app/components/dialogs/admin-organisation-sync-subscription-dialog.tsx
apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
apps/remix/app/components/dialogs/admin-user-delete-dialog.tsx
apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx
apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
apps/remix/app/components/dialogs/admin-user-reset-two-factor-dialog.tsx
apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
UX
apps/remix/app/components/general/direct-template/direct-template-page.tsx:121
Failed document creation shows an error toast with no way forward
The catch block for `createDocumentFromDirectTemplate` shows a destructive toast with `title`/`description` from `getDirectTemplateErrorMessage`, then rethrows. There is no retry button or recovery action attached to the toast itself.
Why it matters
A signer who just finished filling out every field sees an error and has no explicit next step; they have to guess that re-clicking the original submit button is safe rather than being offered a clear retry.
Fix
Pair destructive error toasts with an explicit action the user can take, such as a retry button, rather than a message alone.
toast({
title: _(errorMessage.title),
description: _(errorMessage.description),
variant: 'destructive',
});toast({
title: _(errorMessage.title),
description: _(errorMessage.description),
variant: 'destructive',
action: (
<ToastAction altText="Retry" onClick={() => onSignDirectTemplateSubmit(fields, nextSigner)}>
<Trans>Retry</Trans>
</ToastAction>
),
});apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx:45
"Login" button typed submit with no form to submit into
The "Login" `Button` on the direct template auth page has `type="submit"` and its click behavior is entirely driven by an `onClick` handler calling `handleChangeAccount()`. There's no `<form>` element anywhere in this component.
Why it matters
A submit-typed button outside a form does nothing extra today, but if this component is ever nested inside a form (a common refactor), it will trigger an unrelated form submission the author never intended.
Fix
Type buttons as type="button" when their action is handled entirely by onClick and there is no form to submit.
<Button
className="mt-4 w-full"
type="submit"
onClick={async () => handleChangeAccount()}
loading={isSigningOut}
>
<Trans>Login</Trans>
</Button><Button
className="mt-4 w-full"
type="button"
onClick={async () => handleChangeAccount()}
loading={isSigningOut}
>
<Trans>Login</Trans>
</Button>apps/remix/app/components/general/direct-template/direct-template-page.tsx:86
No submitting state on document creation lets signers double-submit
`onSignDirectTemplateSubmit` calls `createDocumentFromDirectTemplate` directly with no loading flag tracked around the mutation. Whatever button triggers this handler has no way to disable itself or show a busy state while the request is in flight.
Why it matters
A signer who clicks again during the round trip can trigger a duplicate document creation, which is exactly the kind of state corruption a signing flow can't afford.
Fix
Track an isSubmitting flag around async mutations and disable the triggering control until it resolves.
const onSignDirectTemplateSubmit = async (
fields: DirectTemplateLocalField[],
nextSigner?: { name: string; email: string },
) => {
try {
let directTemplateExternalId = searchParams?.get('externalId') || undefined;const [isSigningSubmitting, setIsSigningSubmitting] = useState(false);
const onSignDirectTemplateSubmit = async (
fields: DirectTemplateLocalField[],
nextSigner?: { name: string; email: string },
) => {
setIsSigningSubmitting(true);
try {
let directTemplateExternalId = searchParams?.get('externalId') || undefined;Spacing
apps/remix/app/components/general/direct-template/direct-template-page.tsx:149
Arbitrary calc height on the signing form container has no token backing
The `DocumentFlowFormContainer` on the direct template signing page uses `className="lg:h-[calc(100vh-6rem)]"`, a one-off bracket value with a magic 6rem offset that isn't tied to any header/nav measurement in the file.
Why it matters
If the surrounding chrome (header, banner) ever changes height, this hardcoded offset silently drifts out of sync and either clips the form or leaves a dead gap, and nothing in the codebase enforces the relationship.
Fix
Derive layout offsets from a shared token or CSS variable rather than a hand-picked rem value.
<DocumentFlowFormContainer className="lg:h-[calc(100vh-6rem)]" onSubmit={(e) => e.preventDefault()}><DocumentFlowFormContainer className="lg:h-[calc(100vh-var(--header-height,6rem))]" onSubmit={(e) => e.preventDefault()}>apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx:31
Auth-required screen locks to an arbitrary 70vh instead of content height
The root container on the direct template auth page is `h-[70vh]`, a one-off viewport-relative height with no relationship to its content (a heading, a line of text, and one button).
Why it matters
On short viewports (landscape mobile, small laptop windows with browser chrome) 70vh can crop content or force scrolling for a three-line message; on tall viewports it leaves an oversized empty page for no reason.
Fix
Size containers to their content with min-height rather than a fixed viewport-percentage height.
<div className="mx-auto flex h-[70vh] w-full max-w-md flex-col items-center justify-center"><div className="mx-auto flex min-h-[50vh] w-full max-w-md flex-col items-center justify-center">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 freeMotion
apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx:87
Infinite signing spinner has no reduced-motion escape for sensitive users
The `Loader2Icon` shown while "Applying your signature" is in progress uses `animate-spin` with no `motion-reduce` guard. It spins continuously for the entire signing wait, however long that takes, with no way to opt out.
Why it matters
Vestibular-sensitive users get no way to disable this motion, and unlike a one-shot transition this one runs indefinitely on a page they're stuck on until the signature finalizes.
Fix
Respect prefers-reduced-motion on any infinite animation by pairing animate-spin with motion-reduce:animate-none.
<Loader2Icon className="h-12 w-12 animate-spin text-primary" /><Loader2Icon className="h-12 w-12 animate-spin text-primary motion-reduce:animate-none" />Accessibility
Typography
Color
Components
Craft
Working well
- Wrapping the mutation in try/catch with AppError.parseError and a mapped user-facing toast message is a solid recoverable-error pattern rather than a raw console error.
- The fire-once guard uses a ref with a documented comment explaining exactly why useState would double-fire under StrictMode: that's a deliberate, well-reasoned choice.
- Branching the heading and body copy per AppErrorCode instead of one generic message gives recipients specific, actionable context for each failure mode.
- The empty state for a missing document uses an icon plus two lines of explanatory text instead of a blank void, giving users a clear next step.
Scored August 1, 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 documenso/documenso’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.