Rams MCP · The full engine, now in your coding agent
open-webui on GitHub

open-webui/open-webui

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.

9issues
Critical & Serious · top 6 shown below

Top fix

Restore a visible focus ring on the URL input

See the fix

Verdict

Visual restraint keeps overriding basic accessibility: focus rings, labels, and dismiss controls get stripped rather than redesigned. The autoplaying full-screen onboarding video with no reduced-motion guard is the loudest sign that polish was prioritized over usability.

Files Rams reviewed

src/lib/components/app/AppSidebar.svelte

src/lib/components/AutomationModal.svelte

src/lib/components/ChangelogModal.svelte

src/lib/components/ImportModal.svelte

src/lib/components/NotificationToast.svelte

src/lib/components/OnBoarding.svelte

src/lib/components/admin/Analytics/AnalyticsModelModal.svelte

src/lib/components/admin/Analytics/ChartLine.svelte

src/lib/components/admin/Evaluations.svelte

src/lib/components/admin/Evaluations/FeedbackMenu.svelte

src/lib/components/admin/Evaluations/FeedbackModal.svelte

src/lib/components/admin/Evaluations/Leaderboard.svelte

src/lib/components/admin/Evaluations/LeaderboardModal.svelte

src/lib/components/admin/Evaluations/ModelActivityChart.svelte

src/lib/components/admin/Functions/AddFunctionMenu.svelte

src/lib/components/admin/Functions/FunctionMenu.svelte

src/lib/components/admin/Settings.svelte

src/lib/components/admin/Settings/AdminTabIcon.svelte

src/lib/components/admin/Settings/CodeExecution.svelte

src/lib/components/admin/Settings/Connections.svelte

src/lib/components/admin/Settings/Connections/ManageOllamaModal.svelte

src/lib/components/admin/Settings/Connections/OllamaConnection.svelte

src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte

src/lib/components/admin/Settings/Database.svelte

src/lib/components/admin/Settings/Evaluations.svelte

src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte

src/lib/components/admin/Settings/Evaluations/Model.svelte

src/lib/components/admin/Settings/General.svelte

src/lib/components/admin/Settings/Integrations.svelte

src/lib/components/admin/Settings/Interface/Banners.svelte

88/100

Accessibility

2 critical3 serious
AccessibilityCritical

src/lib/components/ImportModal.svelte:93

URL input strips focus ring with no replacement, hiding keyboard position

The URL input in ImportModal.svelte is the only interactive field in the form besides the submit button, and its class list includes outline-hidden with no focus:ring or focus:border replacement. A keyboard user tabbing through the modal has no visual cue that this field is focused.

Why it matters

Keyboard-only users lose track of where they are in the form, so they either submit blind or abandon the import flow entirely.

Fix

Never remove a focus outline without substituting a visible focus style.

class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden focus:outline-2 focus:outline-offset-2 focus:outline-black dark:focus:outline-white"
AccessibilityCritical

src/lib/components/NotificationToast.svelte:100

Toast root handles keydown for dismissal but is never focusable

The root div in NotificationToast.svelte has role="status", pointer handlers, cursor-pointer, and an on:keydown for Enter/Space that calls clickHandler(), but carries no tabindex. Without tabindex="0" the div can never receive keyboard focus, so the keydown handler is dead code for keyboard users.

Why it matters

Keyboard-only users cannot trigger the toast's primary click action at all, locking them out of whatever the notification links to.

Fix

Any element with a keydown handler for activation must also carry tabindex="0" to be reachable by keyboard.

<div
	role="status"
	aria-live="polite"
	class="group relative flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-800 rounded-3xl px-4 py-3.5 cursor-pointer select-none"
	on:dragstart|preventDefault
	on:pointerdown={onPointerDown}
	on:pointermove={onPointerMove}
	on:pointerup={onPointerUp}
	on:pointercancel={() => (moved = true)}
	on:keydown={(e) => {
		if (e.key === 'Enter' || e.key === ' ') {
			e.preventDefault();
			clickHandler();
		}
	}}
>
<div
	role="status"
	aria-live="polite"
	tabindex="0"
	class="group relative flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-800 rounded-3xl px-4 py-3.5 cursor-pointer select-none"
	on:dragstart|preventDefault
	on:pointerdown={onPointerDown}
	on:pointermove={onPointerMove}
	on:pointerup={onPointerUp}
	on:pointercancel={() => (moved = true)}
	on:keydown={(e) => {
		if (e.key === 'Enter' || e.key === ' ') {
			e.preventDefault();
			clickHandler();
		}
	}}
>
AccessibilitySerious

src/lib/components/app/AppSidebar.svelte:22

Active nav item shown only by a color bar, invisible to screen readers

The 'Home' button in AppSidebar.svelte marks selection with a decorative bar (bg-black dark:bg-white) shown conditionally and a rounded-2xl vs rounded-full shape swap, but the button itself carries no aria-current or aria-pressed. The same pattern repeats on the 'Chat' button.

Why it matters

Screen reader users hear 'Home, button' with no indication it's the active section, so they can't tell which part of the app they're currently in.

Fix

Mark the active item in a nav with aria-current="page" (or aria-pressed for toggle buttons), not color or shape alone.

<button
	aria-label="Home"
	class=" cursor-pointer {selected === 'home' ? 'rounded-2xl' : 'rounded-full'}"
	on:click={() => {
		selected = 'home';

		if (window.electronAPI) {
			window.electronAPI.load('home');
		}
	}}
>
<button
	aria-label="Home"
	aria-current={selected === 'home' ? 'page' : undefined}
	class=" cursor-pointer {selected === 'home' ? 'rounded-2xl' : 'rounded-full'}"
	on:click={() => {
		selected = 'home';

		if (window.electronAPI) {
			window.electronAPI.load('home');
		}
	}}
>
AccessibilitySerious

src/lib/components/ImportModal.svelte:89

'URL' label floats unbound above input, so it is never announced

The div showing {$i18n.t('URL')} sits directly above the url input in ImportModal.svelte but has no id/for or aria-labelledby relationship to it. A screen reader only reads the placeholder 'Enter the URL to import', which disappears the moment the user types.

Why it matters

Once a screen reader user starts typing, they lose the only description of what the field expects, making the form harder to verify before submit.

Fix

Bind every visible field label to its input with a matching id/for pair or aria-labelledby.

<div class=" mb-1 text-xs text-gray-500">{$i18n.t('URL')}</div>

<div class="flex-1">
	<input
		class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
		type="url"
		bind:value={url}
		placeholder={$i18n.t('Enter the URL to import')}
		required
	/>
<div id="import-url-label" class=" mb-1 text-xs text-gray-500">{$i18n.t('URL')}</div>

<div class="flex-1">
	<input
		aria-labelledby="import-url-label"
		class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
		type="url"
		bind:value={url}
		placeholder={$i18n.t('Enter the URL to import')}
		required
	/>
AccessibilitySerious

src/lib/components/NotificationToast.svelte:104

Dismiss button on toast only appears on hover, invisible to keyboard and touch

The 'Dismiss notification' button in NotificationToast.svelte is opacity-0 group-hover:opacity-100 with no group-focus-within or touch-visible variant. Keyboard-focused users and touch users who can't hover never see the close control render.

Why it matters

Users on touch devices or navigating by keyboard have no visible way to dismiss the toast before it times out, forcing them to wait it out.

Fix

Pair any hover-only affordance with a focus-within or always-visible fallback for non-hover input methods.

class="absolute -top-0.5 -left-0.5 p-0.5 rounded-full opacity-0 group-hover:opacity-100 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-opacity z-10"
class="absolute -top-0.5 -left-0.5 p-0.5 rounded-full opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus:opacity-100 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-opacity z-10"
97/100

Motion

1 critical
MotionCritical

src/lib/components/OnBoarding.svelte:52

Full-screen onboarding video autoplays and loops with no reduced-motion guard

The background <video> in OnBoarding.svelte plays autoplay muted loop covering the entire viewport (absolute inset-0 h-full w-full) with no check against prefers-reduced-motion anywhere in the surrounding logic.

Why it matters

Users with vestibular disorders who set prefers-reduced-motion get continuous full-screen motion on the very first screen they see, with no way to opt out.

Fix

Gate autoplaying background motion behind a prefers-reduced-motion media check and fall back to the static poster image.

<video
	bind:this={videoElement}
	class="absolute inset-0 h-full w-full object-cover"
	src="/assets/welcome.mp4"
	autoplay
	muted
	loop
	playsinline
	preload="auto"
	poster="/assets/welcome.webp"
	aria-hidden="true"
></video>
{#if !prefersReducedMotion}
	<video
		bind:this={videoElement}
		class="absolute inset-0 h-full w-full object-cover"
		src="/assets/welcome.mp4"
		autoplay
		muted
		loop
		playsinline
		preload="auto"
		poster="/assets/welcome.webp"
		aria-hidden="true"
	></video>
{:else}
	<img src="/assets/welcome.webp" class="absolute inset-0 h-full w-full object-cover" alt="" aria-hidden="true" />
{/if}

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

  • The "Get started" button pairs focus:ring-2 with focus:outline-hidden, giving keyboard users a visible custom focus indicator instead of just removing the outline.
  • The decorative video and arrow icon both carry aria-hidden="true", keeping screen reader output focused on the actual heading and button text.
  • Both icon-only nav buttons ("Home", "Chat") already carry explicit aria-label attributes, which is the correct fix for icon-only controls.
  • The Save/Create button correctly disables during loading and swaps in a Spinner, preventing double-submit while giving clear feedback.

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 open-webui/open-webui’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