makeplane/plane
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
Elevated
Design risk in this codebase.
Verdict
This set is mostly solid structurally (clean modal patterns, labeled menu items) but the AI response menu is the weak point: two icon-only actions ship with tooltip-only labeling and undersized hit areas, which is a real lockout for assistive tech and a friction point for everyone else. The content-limit banner is a quieter but real miss, a state-change warning that never reaches screen reader users. Fixes across all three files are small, targeted attribute and class additions, not redesigns.
Files Rams reviewed
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx
apps/web/core/components/power-k/ui/pages/work-item-selection-page.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/header.tsx
apps/admin/app/components/404.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/header.tsx
apps/web/core/components/pages/dropdowns/actions.tsx
apps/web/core/components/pages/editor/ai/ask-pi-menu.tsx
apps/web/core/components/pages/editor/ai/menu.tsx
apps/web/core/components/pages/editor/content-limit-banner.tsx
apps/web/core/components/pages/editor/editor-body.tsx
apps/web/core/components/pages/editor/header/logo-picker.tsx
apps/web/core/components/pages/editor/header/root.tsx
apps/web/core/components/pages/editor/page-root.tsx
apps/web/core/components/pages/editor/summary/content-browser.tsx
apps/web/core/components/pages/editor/summary/heading-components.tsx
apps/web/core/components/pages/editor/title.tsx
Spacing
apps/web/core/components/pages/editor/ai/menu.tsx:148
Hardcoded pixel widths on the AI menu bypass the spacing scale
The AI menu container sets "w-[210px]" by default and "w-[700px]" when a task is active, plus "border-[0.5px]", instead of scale-based Tailwind width or a token.
Why it matters
Arbitrary pixel values drift independently from the rest of the layout scale, so future width tweaks (e.g. responsive breakpoints) require hunting down bespoke numbers instead of adjusting one scale step.
Fix
Replace one-off pixel widths with the nearest step on the existing Tailwind width scale or a named token for menu width.
"flex w-[210px] flex-col rounded-md border-[0.5px] border-strong bg-surface-1 shadow-raised-200 transition-all",
{
"w-[700px]": activeTask,
}"flex w-52 flex-col rounded-md border border-strong bg-surface-1 shadow-raised-200 transition-all",
{
"w-[44rem]": activeTask,
}apps/web/core/components/pages/dropdowns/actions.tsx:188
Dropdown menu caps height with an arbitrary viewport value
CustomMenu's optionsClassName="max-h-[90vh]" sizes the page-actions dropdown against 90% of viewport height directly instead of a defined max-height token or scale step.
Why it matters
A viewport-relative arbitrary value behaves unpredictably across small viewports and doesn't align with any other menu's sizing in the app, so menus grow inconsistent max heights across the product.
Fix
Use a fixed max-height token consistent with other CustomMenu instances instead of a one-off viewport-relative arbitrary value.
<CustomMenu placement="bottom-end" optionsClassName="max-h-[90vh]" ellipsis closeOnSelect><CustomMenu placement="bottom-end" optionsClassName="max-h-96" ellipsis closeOnSelect>Accessibility
apps/web/core/components/pages/editor/ai/menu.tsx:238
Icon-only insert and regenerate buttons give screen readers no name
In the AI response menu, the 'Add to next line' button (CornerDownRight icon) and the 'Re-generate response' button (RefreshCcw icon) both render with no text content, no aria-label, and no visually-hidden label. Their only description comes from a Tooltip, which is hover/focus-visible content, not an accessible name. A screen reader lands on both as an unlabeled 'button'.
Why it matters
Screen reader and switch-control users can't tell what either button does or that they're different from each other, so they can't act on the AI response with confidence.
Fix
Add an aria-label matching the tooltip text to every icon-only button so its accessible name doesn't depend on hover state.
<Tooltip tooltipContent="Add to next line">
<button
type="button"
className="grid size-6 flex-shrink-0 place-items-center rounded-sm outline-none hover:bg-layer-1"
onClick={() => handleInsertText(true)}
>
<CornerDownRight className="size-4 text-tertiary" />
</button>
</Tooltip>
<Tooltip tooltipContent="Re-generate response">
<button
type="button"
className="grid size-6 flex-shrink-0 place-items-center rounded-sm outline-none hover:bg-layer-1"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleRegenerate(); }}
disabled={isRegenerating}
>
<RefreshCcw className={cn("size-4 text-tertiary", { "animate-spin": isRegenerating })} />
</button>
</Tooltip><Tooltip tooltipContent="Add to next line">
<button
type="button"
aria-label="Add to next line"
className="grid size-6 flex-shrink-0 place-items-center rounded-sm outline-none hover:bg-layer-1"
onClick={() => handleInsertText(true)}
>
<CornerDownRight className="size-4 text-tertiary" />
</button>
</Tooltip>
<Tooltip tooltipContent="Re-generate response">
<button
type="button"
aria-label="Re-generate response"
className="grid size-6 flex-shrink-0 place-items-center rounded-sm outline-none hover:bg-layer-1"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleRegenerate(); }}
disabled={isRegenerating}
>
<RefreshCcw className={cn("size-4 text-tertiary", { "animate-spin": isRegenerating })} />
</button>
</Tooltip>Get this score on every PR.
Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.
Install Rams freeComponents
apps/web/core/components/pages/editor/ai/menu.tsx:250
24px icon buttons undersized for reliable pointer interaction
The same 'Add to next line' and 'Re-generate response' buttons use className="grid size-6 ...", a 24x24px hit area, sitting next to the larger text button 'Replace selection'.
Why it matters
A hit target under ~44px raises mis-click rates for anyone with limited dexterity or on touch, and it reads as an afterthought next to the taller text button beside it.
Fix
Increase the tappable area to at least 32-36px with padding while keeping the icon at its current visual size.
className="grid size-6 flex-shrink-0 place-items-center rounded-sm outline-none hover:bg-layer-1"className="grid size-8 flex-shrink-0 place-items-center rounded-sm outline-none hover:bg-layer-1"UX
apps/web/core/components/pages/dropdowns/actions.tsx:63
'Move' menu action sets state that no modal ever reads
PageActions declares const [movePageModal, setMovePageModal] = useState(false) and a 'Move' item calls setMovePageModal(true) on click, but unlike DeletePageModal there is no <MovePageModal isOpen={movePageModal} .../> rendered in the component's return, so movePageModal is never consumed.
Why it matters
Clicking 'Move' updates state with no visible effect, so users who expect a move dialog get silent failure and have to guess whether the action registered at all.
Fix
Render the corresponding move-page modal wired to the existing movePageModal state, mirroring the DeletePageModal pattern already in this file.
const [movePageModal, setMovePageModal] = useState(false);
...
action: () => setMovePageModal(true),const [movePageModal, setMovePageModal] = useState(false);
...
action: () => setMovePageModal(true),
// in render:
<MovePageModal isOpen={movePageModal} onClose={() => setMovePageModal(false)} page={page} storeType={storeType} />Typography
Color
Motion
Craft
Working well
- Splitting 'Replace selection' out as a labeled text button while keeping insert/regenerate as icon buttons gives each affordance a distinct visual weight that matches its distinct purpose, so the primary text action reads first.
- The dismiss button in ContentLimitBanner correctly uses aria-label="Dismiss content limit warning" on an icon-only control, which is the right pattern for giving a glyph-only button a real accessible name.
- Menu items throughout PageActions pair an icon with visible title text like 'Lock'/'Unlock' and 'Delete', so screen readers get a real accessible name for free without extra aria-label plumbing.
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: 59/100.
This page is an automated design review of makeplane/plane’s UI code: 30 files read against 309 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.