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

usememos/memos

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

30 files reviewed·July 9, 2026

Top fix

Add real headings to auth and Explore pages

See the fix

Verdict

Logic is careful throughout, auth flows, pagination, CSRF checks all hold up, but the screens built on top of that logic go silent exactly when users need feedback most. Missing headings and unannounced state changes are the recurring failure, and screen reader users bear the brunt of it.

Files Rams reviewed

web/src/components/ConfirmDialog/index.tsx

web/src/components/MemoContent/index.tsx

web/src/components/MemoEditor/Editor/index.tsx

web/src/components/MemoEditor/index.tsx

web/src/components/Placeholder/index.tsx

web/src/pages/About.tsx

web/src/pages/AdminSignIn.tsx

web/src/pages/Archived.tsx

web/src/pages/Attachments.tsx

web/src/pages/AuthCallback.tsx

web/src/pages/Explore.tsx

web/src/pages/Home.tsx

web/src/pages/Inboxes.tsx

web/src/pages/MemoDetail.tsx

web/src/pages/Setting.tsx

web/src/pages/SignIn.tsx

web/src/pages/SignUp.tsx

web/src/pages/UserProfile.tsx

web/src/components/ActivityCalendar/CalendarCell.tsx

web/src/components/ActivityCalendar/MonthCalendar.tsx

web/src/components/ActivityCalendar/YearCalendar.tsx

web/src/components/AttachmentIcon.tsx

web/src/components/AttachmentLibrary/AttachmentFileRows.tsx

web/src/components/AttachmentLibrary/AttachmentLibraryEmptyState.tsx

web/src/components/AttachmentLibrary/AttachmentLibraryPrimitives.tsx

web/src/components/AttachmentLibrary/AttachmentLibraryStates.tsx

web/src/components/AttachmentLibrary/AttachmentLibraryToolbar.tsx

web/src/components/AttachmentLibrary/AttachmentMediaGrid.tsx

web/src/components/AuthFooter.tsx

web/src/components/ChangeMemberPasswordDialog.tsx

90/100

Accessibility

5 serious
Accessibility·web/src/pages/AuthCallback.tsx:148Serious

Error message appears with no live region, so screen readers announce nothing

`state.errorMessage` renders conditionally after the async OAuth exchange resolves (success/failure branches in the `useEffect`), but the container div has no `role="alert"` or `aria-live` attribute.

Why it matters

A screen reader user who lands on this page after an OAuth redirect hears silence when the error appears since the DOM update happens with no live-region announcement, leaving them unaware the sign-in failed.

Fix

Mark the error container with role='alert' so assistive tech announces it the moment it renders.

<div className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
<div role="alert" className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
Accessibility·web/src/pages/About.tsx:50Serious

External link icons add screen-reader noise with no new-tab warning

Each product link ("Website", "GitHub", "Docs") renders `<ExternalLinkIcon className="size-3.5" />` next to its label with no `aria-hidden`, and the anchors carry `target="_blank"` with no indication the link leaves the app.

Why it matters

Screen readers announce the icon as unlabeled noise after each link name, and users get no warning before being sent to an external site, which breaks the back-button expectation for assistive tech and keyboard users.

Fix

Mark decorative icons aria-hidden and add a visually-hidden 'opens in new tab' cue for links that leave the app.

<a href={link.href} target="_blank" rel="noreferrer">
  {link.label}
  <ExternalLinkIcon className="size-3.5" />
</a>
<a href={link.href} target="_blank" rel="noreferrer">
  {link.label}
  <ExternalLinkIcon className="size-3.5" aria-hidden="true" />
  <span className="sr-only"> (opens in new tab)</span>
</a>
Accessibility·web/src/pages/Attachments.tsx:188Serious

Background fetch status text updates silently for screen reader users

The `"fetching-data"` status shown at `!isLoading && isFetching && !isFetchingNextPage` renders as a plain `<div className="mt-4 text-center text-xs text-muted-foreground">` with no `role` or `aria-live` attribute.

Why it matters

Sighted users see the fetching label appear and disappear, but screen reader users get no announcement that a background refresh happened, so they can't tell whether the list just changed underneath them.

Fix

Mark transient async status text as a live region so assistive tech announces it.

<div className="mt-4 text-center text-xs text-muted-foreground">{t("resource.fetching-data")}</div>
<div role="status" aria-live="polite" className="mt-4 text-center text-xs text-muted-foreground">{t("resource.fetching-data")}</div>
Accessibility·web/src/pages/AdminSignIn.tsx:13Serious

Page has zero headings, so screen reader users lose their landmark

The brand title ("Memos" or custom title) and the instruction "Sign in with admin accounts" are both rendered as `<p>` tags. There is no `<h1>` anywhere on this page.

Why it matters

Screen reader users navigate by jumping between headings first. A sign-in page with no heading forces them to read the entire DOM linearly to find out where they are, which is exactly the moment they need the fastest orientation.

Fix

Give the page a single `<h1>` that carries the primary identity, and let the subtitle stay a `<p>`.

<p className="ml-2 text-5xl text-foreground opacity-80">{instanceGeneralSetting.customProfile?.title || "Memos"}</p>
<h1 className="ml-2 text-5xl text-foreground opacity-80">{instanceGeneralSetting.customProfile?.title || "Memos"}</h1>
Accessibility·web/src/pages/Explore.tsx:27Serious

Explore page ships no heading, leaving screen readers no page context

The component returns only `<PagedMemoList ... />` with no `<h1>` or any heading element wrapping it. There is no textual landmark announcing this is the "Explore" page.

Why it matters

Screen reader users navigate by heading list first; a page with zero headings gives them no way to confirm where they are or jump past the list, and it breaks the expected document outline (WCAG 1.3.1).

Fix

Add a page-level heading before the primary content so assistive tech and sighted users both get an entry point.

return (
  <PagedMemoList
return (
  <>
    <h1 className="sr-only">Explore</h1>
    <PagedMemoList
98/100

UX

1 serious
UX·web/src/pages/About.tsx:16Serious

Product chips restate words already used in the section description

The "About Memos" description reads "Markdown-native, lightweight, and fully yours," and the `PRODUCT_POINTS` chips directly below repeat two of the same words verbatim: - "Open. Write. Done." - "Markdown-native." - "Fully yours."

Why it matters

Reading the same claims twice in a row adds reading cost without adding information, so the Product section feels like padding rather than a distinct summary.

Fix

Every line should add new information; replace restated adjectives with distinct product facts.

const PRODUCT_POINTS = ["Open. Write. Done.", "Markdown-native.", "Fully yours."];
const PRODUCT_POINTS = ["Quick capture", "Plain Markdown files", "No vendor lock-in"];

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • Surfaces across the page (the info row, the product chips, the bird cards) all pull from the same neutral token set: `border-border`, `bg-background`, `bg-muted/40`, `bg-muted/20`. Reusing one token family across unrelated sections is what keeps a settings page feeling like one system instead of three.
  • Filtering and sorting concerns are pulled into `useMemoFilters` and `useMemoSorting` instead of being reimplemented inline. Keeping this page as a thin composition of shared hooks means changes to sort or filter behavior propagate everywhere Explore-style views are used, instead of drifting per page.
  • The visibility logic is documented with a clear comment explaining the actual business rule (logged-in users see PUBLIC and PROTECTED, visitors see PUBLIC only) rather than restating the ternary itself. That's the right use of a comment: it explains a permission decision the code alone can't convey.
  • Re-validating `returnUrl` with `getSafeRedirectPath` right before the final `navigateTo` call, even though it was already sanitized on the way in, is exactly the kind of defense-in-depth that prevents an open-redirect vulnerability from ever reaching a user-visible navigation.

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.