open-webui/open-webui
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
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
Accessibility
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"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();
}
}}
>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');
}
}}
>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
/>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"Motion
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
Color
Spacing
Components
UX
Craft
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.