mastodon/mastodon
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
More findings
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
Accessibility
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' />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> */}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}
>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>UX
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]);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
Color
Spacing
Components
Motion
Craft
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.