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

mastodon/mastodon

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 · top 6 shown below

Top fix

Add Escape handling and focus trap to alt text popover

See the fix

Verdict

Static accessible-naming is handled well, but every interactive edge, focus, Escape, error states, gets dropped. The biggest risk is silent failure: block and mute actions give users no signal when they fail.

Files Rams reviewed

app/javascript/mastodon/components/_theme_playground/index.tsx

app/javascript/mastodon/components/account/index.tsx

app/javascript/mastodon/components/account_bio/index.tsx

app/javascript/mastodon/components/account_header/index.tsx

app/javascript/mastodon/components/account_list_item/index.tsx

app/javascript/mastodon/components/alert/index.tsx

app/javascript/mastodon/components/alt_text_badge/index.tsx

app/javascript/mastodon/components/badge/index.tsx

app/javascript/mastodon/components/button/index.tsx

app/javascript/mastodon/components/callout/index.tsx

app/javascript/mastodon/components/callout_inline/index.tsx

app/javascript/mastodon/components/carousel/index.tsx

app/javascript/mastodon/components/character_counter/index.tsx

app/javascript/mastodon/components/display_name/index.tsx

app/javascript/mastodon/components/dropdown/index.tsx

app/javascript/mastodon/components/edited_timestamp/index.tsx

app/javascript/mastodon/components/emoji/index.tsx

app/javascript/mastodon/components/empty_state/index.tsx

app/javascript/mastodon/components/familiar_followers/index.tsx

app/javascript/mastodon/components/hotkeys/index.tsx

app/javascript/mastodon/components/list_item/index.tsx

app/javascript/mastodon/components/mini_card/index.tsx

app/javascript/mastodon/components/modal_shell/index.tsx

app/javascript/mastodon/components/navigation_focus_target/index.tsx

app/javascript/mastodon/components/popover/index.tsx

app/javascript/mastodon/components/relative_timestamp/index.tsx

app/javascript/mastodon/components/scrollable_list/index.jsx

app/javascript/mastodon/components/tab_list/index.tsx

app/javascript/mastodon/components/truncated_list/index.tsx

app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx

92/100

Accessibility

4 serious
AccessibilitySerious

app/javascript/mastodon/components/_theme_playground/index.tsx:27

Search input has no accessible name beyond a vanishing placeholder

The nav search field in ThemePlayground renders as `<input type='text' placeholder='Search' />` with no `aria-label`, `aria-labelledby`, or associated `<label>`. Placeholder text disappears the moment the field is focused or filled, and screen readers do not reliably announce placeholder as a field name.

Why it matters

A screen reader user tabbing into this field hears no name for what it does, so they can't tell it's a search box without guessing from context.

Fix

Give every input a persistent accessible name via aria-label or a bound label, not placeholder text alone.

<input type='text' placeholder='Search' />
<input type='text' placeholder='Search' aria-label='Search' />
AccessibilitySerious

app/javascript/mastodon/components/account/index.tsx:258

VerifiedBadge nests an anchor inside the account row's own link

The account row renders as `<Link className='account__display-name focusable' ...>` and `{verification} {muteTimeRemaining}` is rendered inside it, where `verification` is a VerifiedBadge that renders its own `<a>`. An `<a>` nested inside another `<a>` (the outer Link renders as one) is invalid HTML and browsers resolve the click/focus target inconsistently.

Why it matters

Clicking or tab-focusing the verified badge can trigger the outer profile link instead of, or in addition to, the badge's own link, so users land on the wrong destination.

Fix

Never nest an interactive anchor inside another anchor; move the badge outside the Link or render it as non-interactive text within this context.

{verification} {muteTimeRemaining}
{/* render verification outside the wrapping <Link>, e.g. as a sibling element after the closing </Link> */}
AccessibilitySerious

app/javascript/mastodon/components/alt_text_badge/index.tsx:68

Alt text popover traps no focus and ignores the Escape key

The `role='dialog'` element (`aria-labelledby={titleId}`, `ref={popoverRef}`) inside the Popover has a `tabIndex={0}` and mouse handlers but no keydown handler for Escape and no focus trap, so Tab can move focus past the dialog to the rest of the page while it's open.

Why it matters

Keyboard users can tab out of an open dialog into background content, or get stuck with no way to dismiss it via Escape, breaking the expected modal interaction pattern.

Fix

Trap focus within an open dialog and close it on Escape.

<div // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
  className='info-tooltip dropdown-animation'
  role='dialog'
  aria-labelledby={titleId}
  ref={popoverRef}
  id={popoverId}
  onMouseDown={handleMouseDown}
  onMouseUp={handleMouseUp}
  tabIndex={0}
>
<div
  className='info-tooltip dropdown-animation'
  role='dialog'
  aria-labelledby={titleId}
  ref={popoverRef}
  id={popoverId}
  onMouseDown={handleMouseDown}
  onMouseUp={handleMouseUp}
  onKeyDown={(e) => { if (e.key === 'Escape') handleClose(); }}
  tabIndex={0}
>
AccessibilitySerious

app/javascript/mastodon/components/_theme_playground/index.tsx:52

Icon-only menu button has no accessible label for the more-options control

The `<button className={classes.menuButton}>` wrapping `<Icon id='more' icon={MoreHorizIcon} />` carries no `aria-label`, `title`, or visible text. There's no other text content inside the button.

Why it matters

A screen reader announces this control as just "button" with no purpose, so a keyboard or assistive-tech user can't tell it opens the account menu without trial and error.

Fix

Label every icon-only button with aria-label describing its action.

<button type='button' className={classes.menuButton}>
  <Icon id='more' icon={MoreHorizIcon} />
</button>
<button type='button' className={classes.menuButton} aria-label='More options'>
  <Icon id='more' icon={MoreHorizIcon} />
</button>
96/100

UX

2 serious
UXSerious

app/javascript/mastodon/components/alt_text_badge/index.tsx:29

Alt text badge moves focus with a raw setTimeout instead of waiting on mount

`handleClick` calls `setOpen((v) => !v)` then `setTimeout(() => { popoverRef.current?.focus(); }, 0)`. This assumes the Popover has mounted and positioned within a single macrotask tick.

Why it matters

On a slow render the popover may not exist yet when the timeout fires, so focus silently fails to move into the dialog and keyboard users lose track of where focus went.

Fix

Move focus after the referenced element actually mounts, not on a fixed timer.

const handleClick = useCallback(() => {
  setOpen((v) => !v);
  setTimeout(() => {
    popoverRef.current?.focus();
  }, 0);
}, [setOpen]);
const handleClick = useCallback(() => {
  setOpen((v) => !v);
}, [setOpen]);

useEffect(() => {
  if (open) popoverRef.current?.focus();
}, [open]);
UXSerious

app/javascript/mastodon/components/account/index.tsx:103

Block and mute actions fail silently with no error feedback

`handleBlock` dispatches `unblockAccount(id)` or `blockAccount(id)` and `handleMute` dispatches `unmuteAccount(id)` or `initMuteModal(account)` with no `.catch` or error handling attached to either dispatch.

Why it matters

If the block or mute request fails on the server, the button state never updates and the user has no indication anything went wrong, so they assume the action succeeded when it didn't.

Fix

Catch dispatch failures and surface an error state to the user, matching the pattern used elsewhere in this codebase.

const handleBlock = useCallback(() => {
  if (relationship?.blocking) {
    dispatch(unblockAccount(id));
  } else {
    dispatch(blockAccount(id));
  }
}, [dispatch, id, relationship]);
const handleBlock = useCallback(() => {
  const action = relationship?.blocking ? unblockAccount(id) : blockAccount(id);
  dispatch(action).catch((error: unknown) => dispatch(showAlertForError(error)));
}, [dispatch, id, relationship]);

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

  • Elsewhere in the account list flow, `handleAddToLists` wraps `apiFollowAccount` in a `.catch` that dispatches `showAlertForError`, giving users real feedback on failure, a pattern the block/mute handlers in this same file should adopt.
  • The nav links in ThemePlayground correctly use `aria-current={activeLink === 'Saved' ? 'page' : undefined}` to mark the active tab, giving screen readers a real state signal instead of relying on a highlight color alone.
  • The alt text badge's dialog correctly wires `aria-labelledby={titleId}` to its `<h4>`, giving the popover a real accessible name for screen readers even though the dialog needs focus-trap work.

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: 86/100.

This page is an automated design review of mastodon/mastodon’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