n8n-io/n8n
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
Elevated
Accessibility needs attention.
More findings
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
Accessibility
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>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>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>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">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>Motion
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
Color
Spacing
Components
UX
Craft
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.