fix: name the switches in Admin Settings rows (WCAG 4.1.2, 1.3.1) (#27510)

`common/Switch.svelte` already accepts `id`, `ariaLabel` and `ariaLabelledbyId`, but **not one of the 148 `<Switch>` instances under `src/lib/components/admin/` passes any of them**.

`admin/Settings/AdminSettingRow.svelte` renders the row label as a plain `<div>` and the control in a **sibling** slot, so there is nothing tying them together. bits-ui renders the switch as a `<button role="switch">` whose subtree is a text free thumb, so it has no accessible name from any source.

A screen reader user working through Admin Settings hears a long run of "switch, on" and "switch, off" with no indication of what any of them controls.

Breaks WCAG 4.1.2 Name, Role, Value (Level A) and 1.3.1 Info and Relationships (Level A).

Fix: `AdminSettingRow` mints a per instance id, puts it on the label element, and hands it to the default slot, so each row's switch can point at the label that is already rendered next to it. This is the pattern `chat/Settings/Interface.svelte` already uses by hand in 45 places, hoisted into the shared row component so call sites stop hand authoring ids.

`aria-labelledby` rather than a wrapping `<label>`: per HTML-AAM a `<button>` takes its name from `aria-labelledby`, then `aria-label`, then its own subtree, never from an associated `<label>`. `chat/Settings/Subagents.svelte` already wraps two switches in a `<label>` and they are still unnamed, which is the same trap. Using the existing label element also guarantees the accessible name is byte identical to the visible text, which keeps voice control working.

The `description` paragraph deliberately sits outside the referenced element, so verbose help text is not pulled into the name.

Scope: this covers the **72** switches that live inside an `AdminSettingRow`, which is every switch that flows through the shared row component. There are no rows containing more than one switch, so nothing is silently skipped.

The remaining 76 admin switches are not in this component and are not touched. 64 of them are in `admin/Users/Groups/Permissions.svelte`, which hand rolls its own row markup, and the other 12 are per entity toggles in lists and dropdowns where the label is a dynamic row name. `Permissions.svelte` is the worst remaining case, 64 toggles with near identical adjacent labels, and it needs either its own labelling pass or a conversion to `AdminSettingRow` that changes its visual styling. Either way that is not an accessibility only diff and belongs in its own PR.

All 12 touched files compile with the Svelte compiler with no new warnings and are Prettier clean.

Severity: Serious. Admin Settings is unusable with a screen reader.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
This commit is contained in:
Classic298 authored and GitHub committed 2026-07-26 17:35:34 -04:00
1 parent 94b1b7e6b6
commit 7effaa05d1
12 files changed
+213 -72

No files matched your search

@@ -168,23 +168,29 @@
<AdminSettingRow
label={$i18n.t('New Sign Ups')}
description={$i18n.t('Allow new users to create accounts.')}
let:labelId
>
<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
<Switch bind:state={adminConfig.ENABLE_SIGNUP} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('API Keys')}
description={$i18n.t('Allow users to create API keys for programmatic access.')}
let:labelId
>
<Switch bind:state={adminConfig.ENABLE_API_KEYS} />
<Switch bind:state={adminConfig.ENABLE_API_KEYS} ariaLabelledbyId={labelId} />
</AdminSettingRow>
{#if adminConfig?.ENABLE_API_KEYS}
<AdminSettingRow
label={$i18n.t('API Key Endpoint Restrictions')}
description={$i18n.t('Limit API keys to configured endpoints.')}
let:labelId
>
<Switch bind:state={adminConfig.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS} />
<Switch
bind:state={adminConfig.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
{#if adminConfig?.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS}
@@ -238,8 +244,9 @@
<AdminSettingRow
label={$i18n.t('Admin Details')}
description={$i18n.t('Show admin contact details while an account waits for approval.')}
let:labelId
>
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} ariaLabelledbyId={labelId} />
</AdminSettingRow>
{#if adminConfig.SHOW_ADMIN_DETAILS}
@@ -288,8 +295,9 @@
<AdminSettingRow
label={$i18n.t('LDAP')}
description={$i18n.t('Allow users to authenticate with an LDAP directory.')}
let:labelId
>
<Switch bind:state={ENABLE_LDAP} />
<Switch bind:state={ENABLE_LDAP} ariaLabelledbyId={labelId} />
</AdminSettingRow>
{#if ENABLE_LDAP}
@@ -441,8 +449,9 @@
<AdminSettingRow
label={$i18n.t('TLS')}
description={$i18n.t('Use TLS when connecting to the LDAP server.')}
let:labelId
>
<Switch bind:state={LDAP_SERVER.use_tls} />
<Switch bind:state={LDAP_SERVER.use_tls} ariaLabelledbyId={labelId} />
</AdminSettingRow>
{#if LDAP_SERVER.use_tls}
@@ -460,8 +469,9 @@
<AdminSettingRow
label={$i18n.t('Validate Certificate')}
description={$i18n.t('Verify the LDAP server certificate when TLS is enabled.')}
let:labelId
>
<Switch bind:state={LDAP_SERVER.validate_cert} />
<Switch bind:state={LDAP_SERVER.validate_cert} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingField
@@ -481,16 +491,18 @@
<AdminSettingRow
label={$i18n.t('Group Mapping')}
description={$i18n.t('Map LDAP groups to Open WebUI groups.')}
let:labelId
>
<Switch bind:state={LDAP_SERVER.enable_group_management} />
<Switch bind:state={LDAP_SERVER.enable_group_management} ariaLabelledbyId={labelId} />
</AdminSettingRow>
{#if LDAP_SERVER.enable_group_management}
<AdminSettingRow
label={$i18n.t('Auto-Create Groups')}
description={$i18n.t('Create missing groups from LDAP groups.')}
let:labelId
>
<Switch bind:state={LDAP_SERVER.enable_group_creation} />
<Switch bind:state={LDAP_SERVER.enable_group_creation} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingField
@@ -631,22 +643,28 @@
<AdminSettingRow
label={$i18n.t('OAuth Signup')}
description={$i18n.t('Allow users to create accounts through OAuth.')}
let:labelId
>
<Switch bind:state={oauthConfig.ENABLE_OAUTH_SIGNUP} />
<Switch bind:state={oauthConfig.ENABLE_OAUTH_SIGNUP} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Merge Accounts by Email')}
description={$i18n.t('Link OAuth sign-ins to existing accounts with the same email.')}
let:labelId
>
<Switch bind:state={oauthConfig.OAUTH_MERGE_ACCOUNTS_BY_EMAIL} />
<Switch
bind:state={oauthConfig.OAUTH_MERGE_ACCOUNTS_BY_EMAIL}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Auto Redirect')}
description={$i18n.t('Send users directly to the OAuth provider from the sign-in page.')}
let:labelId
>
<Switch bind:state={oauthConfig.OAUTH_AUTO_REDIRECT} />
<Switch bind:state={oauthConfig.OAUTH_AUTO_REDIRECT} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingField
@@ -663,8 +681,12 @@
<AdminSettingRow
label={$i18n.t('Role Mapping')}
description={$i18n.t('Map OAuth claims to Open WebUI roles.')}
let:labelId
>
<Switch bind:state={oauthConfig.ENABLE_OAUTH_ROLE_MANAGEMENT} />
<Switch
bind:state={oauthConfig.ENABLE_OAUTH_ROLE_MANAGEMENT}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
{#if oauthConfig.ENABLE_OAUTH_ROLE_MANAGEMENT}
@@ -707,16 +729,24 @@
<AdminSettingRow
label={$i18n.t('Group Mapping')}
description={$i18n.t('Map OAuth claims to Open WebUI groups.')}
let:labelId
>
<Switch bind:state={oauthConfig.ENABLE_OAUTH_GROUP_MANAGEMENT} />
<Switch
bind:state={oauthConfig.ENABLE_OAUTH_GROUP_MANAGEMENT}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
{#if oauthConfig.ENABLE_OAUTH_GROUP_MANAGEMENT}
<AdminSettingRow
label={$i18n.t('Auto-Create Groups')}
description={$i18n.t('Create missing groups from OAuth claims.')}
let:labelId
>
<Switch bind:state={oauthConfig.ENABLE_OAUTH_GROUP_CREATION} />
<Switch
bind:state={oauthConfig.ENABLE_OAUTH_GROUP_CREATION}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
@@ -747,22 +777,28 @@
<AdminSettingRow
label={$i18n.t('Update Email')}
description={$i18n.t('Refresh the account email from OAuth on sign-in.')}
let:labelId
>
<Switch bind:state={oauthConfig.OAUTH_UPDATE_EMAIL_ON_LOGIN} />
<Switch bind:state={oauthConfig.OAUTH_UPDATE_EMAIL_ON_LOGIN} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Update Name')}
description={$i18n.t('Refresh the account name from OAuth on sign-in.')}
let:labelId
>
<Switch bind:state={oauthConfig.OAUTH_UPDATE_NAME_ON_LOGIN} />
<Switch bind:state={oauthConfig.OAUTH_UPDATE_NAME_ON_LOGIN} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Update Picture')}
description={$i18n.t('Refresh the profile picture from OAuth on sign-in.')}
let:labelId
>
<Switch bind:state={oauthConfig.OAUTH_UPDATE_PICTURE_ON_LOGIN} />
<Switch
bind:state={oauthConfig.OAUTH_UPDATE_PICTURE_ON_LOGIN}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
</AdminSettingSection>
{/if}