formbricks/formbricks
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
Low
Design risk in this codebase.
More findings
Verdict
Formbricks builds a quiet, low-friction interface but goes silent exactly when things break. Six serious issues cluster around swallowed errors and unguarded actions, meaning the calm UI hides failures instead of handling them.
Files Rams reviewed
apps/web/app/(app)/billing-confirmation/components/ConfirmationPage.tsx
apps/web/app/(app)/workspaces/[workspaceId]/components/WorkspaceLayout.tsx
apps/web/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx
apps/web/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx
apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/create-first-workspace-button.tsx
apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.tsx
apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/ai/components/create-survey-with-ai-onboarding.tsx
apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/survey/components/create-first-survey.tsx
apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/templates/components/xm-template-list.tsx
apps/web/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer.tsx
apps/web/app/(app)/account/authorize/components/OAuthConsentActions.tsx
apps/web/app/(app)/account/settings/authorized-apps/components/RevokeOAuthConsentButton.tsx
apps/web/app/(app)/workspaces/[workspaceId]/components/NavigationLink.tsx
apps/web/app/(app)/workspaces/[workspaceId]/components/SettingsSidebarContent.tsx
apps/web/app/(app)/workspaces/[workspaceId]/components/TopControlBar.tsx
apps/web/app/(app)/workspaces/[workspaceId]/components/WidgetStatusIndicator.tsx
apps/web/app/(app)/workspaces/[workspaceId]/components/organization-breadcrumb.tsx
apps/web/app/(app)/workspaces/[workspaceId]/components/workspace-and-org-switch.tsx
apps/web/app/(app)/workspaces/[workspaceId]/components/workspace-breadcrumb.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/account/components/AccountSettingsNavbar.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/account/notifications/components/EditAlerts.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/account/notifications/components/NotificationSwitch.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/account/profile/components/AccountSecurity.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/account/profile/components/DeleteAccount.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/account/profile/components/EditProfileDetailsForm.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/account/profile/components/password-confirmation-modal.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/components/SettingsCard.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/organization/domain/components/pretty-urls-table.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/organization/enterprise/components/EnterpriseLicenseFeaturesTable.tsx
apps/web/app/(app)/workspaces/[workspaceId]/settings/organization/enterprise/components/EnterpriseLicenseStatus.tsx
UX
apps/web/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx:91
Refetch errors get a vanishing toast with no way to retry
In `refetchResponses`, a `serverError` triggers `toast.error(getFormattedErrorMessage(...))` and nothing else, no inline error state or retry control on the response list itself.
Why it matters
Once the toast auto-dismisses, the user has no record that the refresh failed and no button to try again, they just see the response list frozen at whatever it was before.
Fix
Surface a persistent inline error state with a retry button alongside the toast so failures leave a recoverable trace.
if (getResponsesActionResponse?.serverError) {
toast.error(getFormattedErrorMessage(getResponsesActionResponse) ?? t("common.something_went_wrong"));
}if (getResponsesActionResponse?.serverError) {
toast.error(getFormattedErrorMessage(getResponsesActionResponse) ?? t("common.something_went_wrong"));
setFetchError(true);
}apps/web/app/(app)/billing-confirmation/components/ConfirmationPage.tsx:82
'Back to billing overview' link has no guard against repeat clicks
The `<a href={billingHref}>{t("billing_confirmation.back_to_billing_overview")}</a>` that replaces the loading button once sync finishes has no disabled or pressed state, so it stays fully clickable through the full-document navigation.
Why it matters
A user who clicks twice queues a second navigation on top of the first, though the impact is limited since this is a full page load rather than a client transition.
Fix
Track a local navigating flag and disable the link's pointer events after the first click.
<a href={billingHref}>{t("billing_confirmation.back_to_billing_overview")}</a><a href={billingHref} aria-disabled={isNavigating} onClick={() => setIsNavigating(true)}>
{t("billing_confirmation.back_to_billing_overview")}
</a>apps/web/app/(app)/workspaces/[workspaceId]/components/WorkspaceLayout.tsx:117
Root layout clips content under mobile browser chrome
The workspace layout wraps banners, modals, and page content in `<div className="flex h-screen min-h-screen flex-col overflow-hidden">`.
Why it matters
On mobile Safari and Chrome, `h-screen` doesn't shrink when the address bar is visible, so `overflow-hidden` clips content near the bottom edge instead of letting it scroll into view.
Fix
Use the dynamic viewport unit (h-dvh) instead of h-screen so the layout height tracks the real visible viewport on mobile.
<div className="flex h-screen min-h-screen flex-col overflow-hidden"><div className="flex h-dvh min-h-dvh flex-col overflow-hidden">apps/web/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx:128
Filtered response fetch swallows errors, showing an empty list instead
`fetchFilteredResponses` awaits `getResponsesAction` and reads `.data` directly with no check of `serverError`, unlike `refetchResponses` a few lines above which handles this case.
Why it matters
A failed filtered query renders as "no responses match these filters" instead of an error, so users think their search yielded zero results and give up rather than retry.
Fix
Check getResponsesActionResponse?.serverError in fetchFilteredResponses and surface the same toast used in refetchResponses.
const getResponsesActionResponse = await getResponsesAction({
surveyId,
limit: responsesPerPage,
offset: 0,
filterCriteria: filters,
});
responses = getResponsesActionResponse?.data || [];const getResponsesActionResponse = await getResponsesAction({
surveyId,
limit: responsesPerPage,
offset: 0,
filterCriteria: filters,
});
if (getResponsesActionResponse?.serverError) {
toast.error(getFormattedErrorMessage(getResponsesActionResponse) ?? t("common.something_went_wrong"));
}
responses = getResponsesActionResponse?.data || [];apps/web/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx:59
'Load more' pagination has zero error handling on failed fetches
`fetchNextPage` calls `getResponsesAction` and takes `.data || []` with no check of `serverError` and no catch block, so a failed load-more request just quietly does nothing.
Why it matters
The user clicks to load more responses, the request fails, and the list simply doesn't grow with no error and no indication whether more responses actually exist.
Fix
Check serverError after the fetch and surface a toast, matching the error handling already present in refetchResponses.
const getResponsesActionResponse = await getResponsesAction({
surveyId,
limit: responsesPerPage,
offset: (newPage - 1) * responsesPerPage,
filterCriteria: filters,
});
newResponses = getResponsesActionResponse?.data || [];const getResponsesActionResponse = await getResponsesAction({
surveyId,
limit: responsesPerPage,
offset: (newPage - 1) * responsesPerPage,
filterCriteria: filters,
});
if (getResponsesActionResponse?.serverError) {
toast.error(getFormattedErrorMessage(getResponsesActionResponse) ?? t("common.something_went_wrong"));
return;
}
newResponses = getResponsesActionResponse?.data || [];Motion
apps/web/app/(app)/billing-confirmation/components/ConfirmationPage.tsx:69
Confetti fires unconditionally with no reduced-motion guard
`{showConfetti && <Confetti />}` mounts the confetti animation on every successful upgrade with no check for `prefers-reduced-motion`.
Why it matters
Vestibular-sensitive users land on a celebratory confirmation screen and get forced full-screen motion they have no way to opt out of.
Fix
Check window.matchMedia('(prefers-reduced-motion: reduce)') before mounting Confetti, or skip it when the media query matches.
{showConfetti && <Confetti />}{showConfetti && !prefersReducedMotion && <Confetti />}Accessibility
Typography
Color
Spacing
Components
Craft
Working well
- The lastFetchedFiltersKeyRef guard against refetch loops is documented inline with a clear causal explanation (session-cookie refresh re-firing the effect), which is exactly the kind of comment that justifies a non-obvious pattern instead of just describing the code.
- Distinguishing 'no active filters, use initial data' from 'filters changed, refetch' avoids an unnecessary network request on first render, which keeps the initial summary view fast.
- The switcher trigger uses focus-visible:ring-2 with ring-inset, giving a clear keyboard focus indicator that respects the rounded corner instead of clipping outside it.
- Blocking the back link until Stripe sync completes (isSyncing state) is a thoughtful fix for stale billing data, with the rationale documented directly in the comment.
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: 88/100.
This page is an automated design review of formbricks/formbricks’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.