Rams MCP · The full engine, now in your coding agent
n8n-io on GitHub

n8n-io/n8n

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

30 files reviewed·August 1, 2026

View on GitHub

Elevated

Accessibility needs attention.

12issues
Critical, Serious & Moderate · top 6 shown below

Top fix

Give the execution list link a real href for keyboard access

See the fix

Verdict

A clean-looking interaction pattern keeps getting built on non-interactive elements, so copy, navigation, and expand actions look right but work only for a mouse. The AiGatewaySelector proves the team can model ARIA well, which makes the nested radio-in-pill bug and silent toggles feel like fixable slips, not a design gap.

Files Rams reviewed

packages/frontend/editor-ui/src/app/components/AboutModal.vue

packages/frontend/editor-ui/src/app/components/ActivationModal.vue

packages/frontend/editor-ui/src/app/components/AiGatewaySelector.vue

packages/frontend/editor-ui/src/app/components/AiStarsIcon.vue

packages/frontend/editor-ui/src/app/components/AnimatedSpinner.vue

packages/frontend/editor-ui/src/app/components/Banner.vue

packages/frontend/editor-ui/src/app/components/BottomMenu.vue

packages/frontend/editor-ui/src/app/components/BreakpointsObserver.vue

packages/frontend/editor-ui/src/app/components/ChatEmbedModal.vue

packages/frontend/editor-ui/src/app/components/CollectionWorkflowCard.vue

packages/frontend/editor-ui/src/app/components/ConnectionTracker.vue

packages/frontend/editor-ui/src/app/components/CopyInput.vue

packages/frontend/editor-ui/src/app/components/DependencyPill.vue

packages/frontend/editor-ui/src/app/components/Draggable.vue

packages/frontend/editor-ui/src/app/components/DraggableTarget.vue

packages/frontend/editor-ui/src/app/components/DropArea/DropArea.vue

packages/frontend/editor-ui/src/app/components/DuplicateWorkflowDialog.vue

packages/frontend/editor-ui/src/app/components/DynamicModalLoader.vue

packages/frontend/editor-ui/src/app/components/Feedback.vue

packages/frontend/editor-ui/src/app/components/FocusSidebar.vue

packages/frontend/editor-ui/src/app/components/FreeAiCreditsCallout.vue

packages/frontend/editor-ui/src/app/components/FromAiParametersModal.vue

packages/frontend/editor-ui/src/app/components/ImportWorkflowUrlModal.vue

packages/frontend/editor-ui/src/app/components/IntersectionObserver.vue

packages/frontend/editor-ui/src/app/components/KeyboardShortcutTooltip.vue

packages/frontend/editor-ui/src/app/components/MainHeader/ActionsDropdownMenu.vue

packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue

packages/frontend/editor-ui/src/app/components/MainHeader/TabBar.vue

packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowPublishModal.vue

packages/frontend/editor-ui/src/app/components/MainSidebar.vue

86/100

Accessibility

4 critical1 serious
AccessibilityCritical

packages/frontend/editor-ui/src/app/components/ActivationModal.vue:111

Execution list link has no href, so it can't be reached by keyboard

The <a @click="showExecutionsList"> and the neighboring <a @click="showSettings">"saveExecutions"</a> both render as anchors with no href attribute, relying only on a click handler for navigation.

Why it matters

Anchors without an href are not in the tab order and don't respond to Enter/Space, so keyboard and screen-reader users can't open the executions list at all.

Fix

Replace the bare anchor with a button element or add role='button' and tabindex='0' plus a keydown handler so it is focusable and activatable.

<a @click="showExecutionsList">
	{{ i18n.baseText('activationModal.executionList') }}
</a>
<button type="button" :class="$style.inlineLink" @click="showExecutionsList">
	{{ i18n.baseText('activationModal.executionList') }}
</button>
AccessibilityCritical

packages/frontend/editor-ui/src/app/components/AboutModal.vue:119

Copy debug info only works with a mouse, locking out keyboard users

The debug info row wraps a click handler around a plain <div> (@click="copyDebugInfoToClipboard") with an N8nLink nested inside it purely for visual styling. The div itself carries no tabindex, role, or keyboard handler, so the link inside is the only focusable element but clicking it does nothing since the handler lives on its parent.

Why it matters

Keyboard and screen-reader users tabbing to "Copy" land on a link that does nothing, so they can never copy debug info to report a bug.

Fix

Move the click handler onto the focusable N8nLink itself so keyboard activation and mouse activation trigger the same action.

<div :class="$style.debugInfo" @click="copyDebugInfoToClipboard">
	<N8nLink>{{ i18n.baseText('about.debug.message') }}</N8nLink>
</div>
<div :class="$style.debugInfo">
	<N8nLink @click="copyDebugInfoToClipboard">{{ i18n.baseText('about.debug.message') }}</N8nLink>
</div>
AccessibilityCritical

packages/frontend/editor-ui/src/app/components/AiGatewaySelector.vue:108

Balance pill nested inside a radio button breaks its own click target

The N8nActionPill showing wallet balance (:clickable="!readonly", @click="onBadgeClick") renders inside the <button role="radio"> that toggles gateway selection. Nesting a clickable element inside another interactive control is invalid HTML.

Why it matters

Browsers handle nested interactive elements inconsistently, so clicking the balance pill can also fire the parent radio's selectGateway handler, or the pill's own click/focus can be swallowed for keyboard and screen-reader users.

Fix

Move the balance pill outside the radio button, positioning it visually alongside the card instead of nested within its interactive boundary.

<button ... role="radio" @click="selectGateway">
	...
	<N8nActionPill
		v-if="aiGatewayEnabled && balance !== undefined"
		:clickable="!readonly"
		...
		@click="onBadgeClick"
	/>
</button>
<div :class="$style.cardRow">
	<button ... role="radio" @click="selectGateway">
		...
	</button>
	<N8nActionPill
		v-if="aiGatewayEnabled && balance !== undefined"
		:clickable="!readonly"
		...
		@click="onBadgeClick"
	/>
</div>
AccessibilityCritical

packages/frontend/editor-ui/src/app/components/AnimatedSpinner.vue:2

Loading spinner is invisible to screen readers with no status role

The root <svg> in AnimatedSpinner has width/height and stroke paths but no role="status", aria-label, or aria-hidden="true".

Why it matters

Screen reader users get no announcement that content is loading, and if the SVG is exposed by default it reads as unlabeled visual noise instead of a loading indicator.

Fix

Add role='status' with an aria-label describing the loading state, or aria-hidden='true' if a visible text label already conveys loading elsewhere.

<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg role="status" aria-label="Loading" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
AccessibilitySerious

packages/frontend/editor-ui/src/app/components/Banner.vue:96

"More details" toggle gives no state signal to assistive tech

The N8nLink labeled "More details" (@click="expand") reveals a details <div> further down but carries no aria-expanded or aria-controls attribute linking it to that region.

Why it matters

Screen reader users hear a generic link with no indication that activating it expands a hidden panel, or that the panel now exists once expanded.

Fix

Add aria-expanded bound to the expanded state and aria-controls pointing to the details region's id.

<N8nLink v-if="details && !expanded" :bold="true" size="small" @click="expand">
  <span :class="$style.moreDetails">More details</span>
</N8nLink>
<N8nLink v-if="details && !expanded" :bold="true" size="small" :aria-expanded="expanded" aria-controls="banner-details" @click="expand">
  <span :class="$style.moreDetails">More details</span>
</N8nLink>
98/100

Motion

1 serious
MotionSerious

packages/frontend/editor-ui/src/app/components/AnimatedSpinner.vue:39

Spinner animation runs forever with no reduced-motion guard

The .line class applies animation: fade 1.6s infinite to all eight spokes with staggered delays, and there is no @media (prefers-reduced-motion: reduce) block anywhere in the style section.

Why it matters

Vestibular-sensitive users get a perpetual flashing animation with no way to opt out, which can trigger discomfort on any screen where this spinner appears.

Fix

Wrap the animation in a prefers-reduced-motion media query that disables or slows it for users who've requested reduced motion.

.line {
	animation: fade 1.6s infinite;
}
.line {
	animation: fade 1.6s infinite;
}

@media (prefers-reduced-motion: reduce) {
	.line {
		animation: none;
		opacity: 1;
	}
}

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

UX

No issues found

Craft

No issues found

Working well

  • AiGatewaySelector models the credential choice correctly: a wrapping role="radiogroup" plus role="radio" and aria-checked on each card gives assistive tech a proper mutually-exclusive control, which is exactly right for this pattern even though the nested balance pill inside it is a separate problem.
  • ActivationModal's checkbox uses N8nCheckbox with a label slot ("generic.dontShowAgain") rather than a bare input, keeping the label correctly associated with the control for screen readers.
  • AnimatedSpinner staggers each spoke's animation-delay by 0.2 seconds across all eight lines, producing a convincing rotation illusion from pure CSS without any JavaScript driving it.

Scored August 1, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored May 13, 2026: 59/100. This rescore on v0.0.3: 59/100.

This page is an automated design review of n8n-io/n8n’s UI code: 30 files read against 308 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