Rams MCP · The full engine, now in your coding agent
formbricks on GitHub

formbricks/formbricks

Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.

30 files reviewed·August 1, 2026

View on GitHub

Low

Design risk in this codebase.

7issues
Serious & Moderate · top 6 shown below

Top fix

Give failed fetches a retry action instead of a vanishing toast

See the fix

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

90/100

UX

5 serious
UXSerious

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);
}
UXSerious

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>
UXSerious

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">
UXSerious

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 || [];
UXSerious

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 || [];
98/100

Motion

1 serious
MotionSerious

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

No issues found

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

Craft

No issues found

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.

Or get a design review on every pull requestInstall Rams