Repository: mattermost/mattermost
Pull request #30584: MM-63313 Make theme setting radio buttons horizontal and update text 
Author: login: hmhealey
Labels: name: 4: Reviews Complete
description: All reviewers have approved the pull request
name: Changelog/Done
description: Required changelog entry has been written
name: Docs/Not Needed
description: Does not require documentation
name: release-note
description: Denotes a PR that will be considered when it comes time to generate release notes.
Created: 2025-03-27T21:33:23Z | Merged: 2025-04-17T16:05:35Z

#### Summary
This is some followup stuff that I prepared to https://github.com/mattermost/mattermost/pull/29596 a month ago and then forgot to submit after the meetup.

In that PR, we moved the theme settings so that they're no longer in between the radio buttons for default/custom to make some accessibility stuff work, and that got us to take another look at the layout of that setting.

These changes:
- Make the radio buttons horizontal instead of stacking them
- Change the labels to be more self-explanatory
- Add a title to match other settings and make the top margin look the same as other settings

#### Ticket Link
MM-63313
MM-61583

#### Screenshots
Before|After
-|-
![Screenshot 2025-03-27 at 5 25 51 PM](https://github.com/user-attachments/assets/9228c254-2981-472d-aa31-88b11b7bbbbc)|![Screenshot 2025-03-27 at 5 24 04 PM](https://github.com/user-attachments/assets/c0a5fa5a-98e3-493e-b7fb-7ee1a0da1208)
![Screenshot 2025-03-27 at 5 25 55 PM](https://github.com/user-attachments/assets/3e4a13cd-e6b9-455f-8691-74856005986d)|![Screenshot 2025-03-27 at 5 24 08 PM](https://github.com/user-attachments/assets/39c93ba8-8224-404f-8e26-7aa5d68bdcbf)
![Screenshot 2025-03-27 at 5 25 58 PM](https://github.com/user-attachments/assets/6c7600a0-3a95-411e-8320-457197906b2a)|![Screenshot 2025-03-27 at 5 24 37 PM](https://github.com/user-attachments/assets/6113b81a-d623-4887-ba68-dea3cf868280)

#### Release Note
```release-note
Made minor improvements to the layout of the theme settings
```



Diff:
diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/settings/display_settings.ts b/e2e-tests/playwright/lib/src/ui/components/channels/settings/display_settings.ts
new file mode 100644
index 00000000000..11ce1851bb0
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/ui/components/channels/settings/display_settings.ts
@@ -0,0 +1,58 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect, Locator} from '@playwright/test';
+
+export type DisplaySettingsSection =
+    | 'theme'
+    | 'collapsedReplyThreads'
+    | 'clockDisplay'
+    | 'teammateNameDisplay'
+    | 'availabilityStatusOnPosts'
+    | 'lastActiveTime'
+    | 'timezone'
+    | 'showLinkPreviews'
+    | 'collapseImagePreviews'
+    | 'clickToReply'
+    | 'channelDisplayMode'
+    | 'oneClickReactions'
+    | 'language';
+
+const sectionTitles: Record<DisplaySettingsSection, string> = {
+    theme: 'Theme',
+    collapsedReplyThreads: 'Threaded Discussions',
+    clockDisplay: 'Clock Display',
+    teammateNameDisplay: 'Teammate Name Display',
+    availabilityStatusOnPosts: 'Show online availability on profile images',
+    lastActiveTime: 'Share last active time',
+    timezone: 'Timezone',
+    showLinkPreviews: 'Website Link Previews',
+    collapseImagePreviews: 'Default Appearance of Image Previews',
+    clickToReply: 'Click to open threads',
+    channelDisplayMode: 'Channel Display',
+    oneClickReactions: 'Quick reactions on messages',
+    language: 'Language',
+};
+
+export default class DisplaySettings {
+    readonly container: Locator;
+
+    constructor(container: Locator) {
+        this.container = container;
+    }
+
+    async toBeVisible() {
+        await expect(this.container).toBeVisible();
+    }
+
+    async expandSection(section: DisplaySettingsSection) {
+        await this.container.getByText(sectionTitles[section]).click();
+        await this.verifySectionIsExpanded(section);
+    }
+
+    async verifySectionIsExpanded(section: DisplaySettingsSection) {
+        await expect(this.container.locator('.section-min', {hasText: sectionTitles[section]})).not.toBeVisible();
+
+        await expect(this.container.locator('.section-max', {hasText: sectionTitles[section]})).toBeVisible();
+    }
+}
diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/settings/settings_modal.ts b/e2e-tests/playwright/lib/src/ui/components/channels/settings/settings_modal.ts
index eab26391242..7e8c282cc91 100644
--- a/e2e-tests/playwright/lib/src/ui/components/channels/settings/settings_modal.ts
+++ b/e2e-tests/playwright/lib/src/ui/components/channels/settings/settings_modal.ts
@@ -3,6 +3,7 @@
 
 import {Locator, expect} from '@playwright/test';
 
+import DisplaySettings from './display_settings';
 import NotificationsSettings from './notification_settings';
 
 export default class SettingsModal {
@@ -11,11 +12,17 @@ export default class SettingsModal {
     readonly notificationsSettingsTab;
     readonly notificationsSettings;
 
+    readonly displaySettingsTab;
+    readonly displaySettings;
+
     constructor(container: Locator) {
         this.container = container;
 
         this.notificationsSettingsTab = container.locator('#notificationsButton');
         this.notificationsSettings = new NotificationsSettings(container.locator('#notificationsSettings'));
+
+        this.displaySettingsTab = container.locator('#displayButton');
+        this.displaySettings = new DisplaySettings(container.locator('#displaySettings'));
     }
 
     async toBeVisible() {
@@ -27,6 +34,17 @@ export default class SettingsModal {
         await this.notificationsSettingsTab.click();
 
         await this.notificationsSettings.toBeVisible();
+
+        return this.notificationsSettings;
+    }
+
+    async openDisplayTab() {
+        await expect(this.displaySettingsTab).toBeVisible();
+        await this.displaySettingsTab.click();
+
+        await this.displaySettings.toBeVisible();
+
+        return this.displaySettings;
     }
 
     async closeModal() {
diff --git a/e2e-tests/playwright/lib/src/ui/components/global_header.ts b/e2e-tests/playwright/lib/src/ui/components/global_header.ts
index 5c001e2fe65..481c03c25a8 100644
--- a/e2e-tests/playwright/lib/src/ui/components/global_header.ts
+++ b/e2e-tests/playwright/lib/src/ui/components/global_header.ts
@@ -3,7 +3,10 @@
 
 import {Locator, expect} from '@playwright/test';
 
+import {ChannelsPage} from '../pages';
+
 export default class GlobalHeader {
+    readonly channelsPage: ChannelsPage;
     readonly container: Locator;
 
     readonly accountMenuButton;
@@ -12,7 +15,8 @@ export default class GlobalHeader {
     readonly settingsButton;
     readonly searchBox;
 
-    constructor(container: Locator) {
+    constructor(channelsPage: ChannelsPage, container: Locator) {
+        this.channelsPage = channelsPage;
         this.container = container;
 
         this.accountMenuButton = container.getByRole('button', {name: "'s account menu"});
@@ -34,6 +38,10 @@ export default class GlobalHeader {
     async openSettings() {
         await expect(this.settingsButton).toBeVisible();
         await this.settingsButton.click();
+
+        await this.channelsPage.settingsModal.toBeVisible();
+
+        return this.channelsPage.settingsModal;
     }
 
     async openRecentMentions() {
diff --git a/e2e-tests/playwright/lib/src/ui/pages/channels.ts b/e2e-tests/playwright/lib/src/ui/pages/channels.ts
index 4e4bda789b0..0345dac85ab 100644
--- a/e2e-tests/playwright/lib/src/ui/pages/channels.ts
+++ b/e2e-tests/playwright/lib/src/ui/pages/channels.ts
@@ -35,7 +35,7 @@ export default class ChannelsPage {
         this.page = page;
 
         // The main areas of the app
-        this.globalHeader = new components.GlobalHeader(page.locator('#global-header'));
+        this.globalHeader = new components.GlobalHeader(this, page.locator('#global-header'));
         this.searchPopover = new components.SearchPopover(page.locator('#searchPopover'));
         this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view'));
         this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer'));
diff --git a/e2e-tests/playwright/specs/accessibility/channels/theme_settings.spec.ts b/e2e-tests/playwright/specs/accessibility/channels/theme_settings.spec.ts
new file mode 100644
index 00000000000..c601e0d0a12
--- /dev/null
+++ b/e2e-tests/playwright/specs/accessibility/channels/theme_settings.spec.ts
@@ -0,0 +1,119 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect, test} from '@mattermost/playwright-lib';
+
+test('Theme settings should be keyboard accessible', async ({axe, pw}) => {
+    const {user} = await pw.initSetup();
+
+    // # Log in as a user in new browser context
+    const {page, channelsPage} = await pw.testBrowser.login(user);
+
+    // # Initialize Axe
+    const ab = axe.builder(page).disableRules([
+        'color-contrast',
+
+        // Known issue: These fail due to the way we've grouped plugin setting tabs together in the LHS
+        'aria-required-children',
+        'aria-required-parent',
+    ]);
+
+    // # Visit default channel page
+    await channelsPage.goto();
+    await channelsPage.toBeVisible();
+
+    // # Open settings modal
+    const settingsModal = await channelsPage.globalHeader.openSettings();
+
+    // * The settings modal should have no accessibility violations
+    let accessibilityScanResults = await ab.analyze();
+    expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open display tab
+    await settingsModal.container.focus();
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('ArrowDown');
+
+    // * The display tab should be open
+    const {displaySettings} = settingsModal;
+    await displaySettings.toBeVisible();
+
+    // * The display tab should have no accessibility violations
+    accessibilityScanResults = await ab.analyze();
+    expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open the theme section
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Space');
+
+    // * The theme section should be open
+    await displaySettings.verifySectionIsExpanded('theme');
+
+    // * The Premade Themes option should be focused
+    await expect(page.getByLabel('Premade Themes')).toBeFocused();
+
+    // * The theme section for premade themes should have no accessibility violations
+    accessibilityScanResults = await ab.analyze();
+    expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // * Should be able to tab through the options
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Denim'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Sapphire'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Quartz'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Indigo'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Onyx'})).toBeFocused();
+    await page.keyboard.press('Tab');
+
+    // Note: There's no "Apply to all your teams" option because this user is only on one team
+    await expect(page.getByRole('link', {name: 'See other themes'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Save'})).toBeFocused();
+
+    // # Go back to the premade/custom option
+    await page.getByLabel('Premade Themes').focus();
+
+    // # Switch to the custom theme section
+    await page.keyboard.press('ArrowDown');
+
+    // * The Custom Theme option should be focused
+    await expect(page.getByLabel('Custom Theme')).toBeFocused();
+
+    // * Check the ARIA of the Sidebar Styles section
+    await page.keyboard.press('Tab');
+    const sidebarStyles = page.getByRole('button', {name: 'Sidebar Styles'});
+    await expect(sidebarStyles).toBeFocused();
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'false');
+
+    // * Check that we can tab over the collapsed section to the next section and then back again
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Center Channel Styles'})).toBeFocused();
+
+    await page.keyboard.press('Shift+Tab');
+    await expect(sidebarStyles).toBeFocused();
+
+    // * Check that we can expand the section
+    await page.keyboard.press('Enter');
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'true');
+
+    // # Wait for the expanding animation to be open
+    await expect(page.getByLabel('Sidebar Styles')).toHaveCSS('overflow-y', 'visible');
+
+    // * Check that we can tab through color pickers
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Text', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Header BG')).toBeFocused();
+
+    // * The theme section for custom themes should have no accessibility violations
+    accessibilityScanResults = await ab.analyze();
+    expect(accessibilityScanResults.violations).toHaveLength(0);
+});
diff --git a/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts b/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts
index 8e1e139a51c..c2a795153b4 100644
--- a/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts
+++ b/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts
@@ -26,16 +26,15 @@ test('MM-T5465-1 Should add the keyword when enter, comma or tab is pressed on t
     await channelsPage.centerView.postCreate.postMessage('Hello World');
 
     // # Open settings modal
-    await channelsPage.globalHeader.openSettings();
-    await channelsPage.settingsModal.toBeVisible();
+    const settingsModal = await channelsPage.globalHeader.openSettings();
 
     // # Open notifications tab
-    await channelsPage.settingsModal.openNotificationsTab();
+    const notificationsSettings = await settingsModal.openNotificationsTab();
 
     // # Open keywords that get highlighted section
-    await channelsPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+    await notificationsSettings.expandSection('keysWithHighlight');
 
-    const keywordsInput = await channelsPage.settingsModal.notificationsSettings.getKeywordsInput();
+    const keywordsInput = await notificationsSettings.getKeywordsInput();
 
     // # Enter keyword 1
     await keywordsInput.fill(keywords[0]);
@@ -56,7 +55,7 @@ test('MM-T5465-1 Should add the keyword when enter, comma or tab is pressed on t
     await keywordsInput.press('Enter');
 
     // * Verify that the keywords have been added to the collapsed description
-    const keysWithHighlightDesc = channelsPage.settingsModal.notificationsSettings.keysWithHighlightDesc;
+    const keysWithHighlightDesc = notificationsSettings.keysWithHighlightDesc;
     await keysWithHighlightDesc.waitFor();
     for (const keyword of keywords.slice(0, 3)) {
         expect(await keysWithHighlightDesc).toContainText(keyword);
@@ -77,25 +76,24 @@ test('MM-T5465-2 Should highlight the keywords when a message is sent with the k
     await channelsPage.toBeVisible();
 
     // # Open settings modal
-    await channelsPage.globalHeader.openSettings();
-    await channelsPage.settingsModal.toBeVisible();
+    const settingsModal = await channelsPage.globalHeader.openSettings();
 
     // # Open notifications tab
-    await channelsPage.settingsModal.openNotificationsTab();
+    const notificationsSettings = await settingsModal.openNotificationsTab();
 
     // # Open keywords that get highlighted section
-    await channelsPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+    await notificationsSettings.expandSection('keysWithHighlight');
 
     // # Enter the keyword
-    const keywordsInput = await channelsPage.settingsModal.notificationsSettings.getKeywordsInput();
+    const keywordsInput = await notificationsSettings.getKeywordsInput();
     await keywordsInput.fill(keywords[3]);
     await keywordsInput.press('Tab');
 
     // # Save the keyword
-    await channelsPage.settingsModal.notificationsSettings.save();
+    await notificationsSettings.save();
 
     // # Close the settings modal
-    await channelsPage.settingsModal.closeModal();
+    await settingsModal.closeModal();
 
     // # Post a message without the keyword
     const messageWithoutKeyword = 'This message does not contain the keyword';
@@ -132,25 +130,24 @@ test('MM-T5465-3 Should highlight the keywords when a message is sent with the k
     await channelsPage.toBeVisible();
 
     // # Open settings modal
-    await channelsPage.globalHeader.openSettings();
-    await channelsPage.settingsModal.toBeVisible();
+    const settingsModal = await channelsPage.globalHeader.openSettings();
 
     // # Open notifications tab
-    await channelsPage.settingsModal.openNotificationsTab();
+    const notificationsSettings = await settingsModal.openNotificationsTab();
 
     // # Open keywords that get highlighted section
-    await channelsPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+    await notificationsSettings.expandSection('keysWithHighlight');
 
     // # Enter the keyword
-    const keywordsInput = await channelsPage.settingsModal.notificationsSettings.getKeywordsInput();
+    const keywordsInput = await notificationsSettings.getKeywordsInput();
     await keywordsInput.fill(keywords[3]);
     await keywordsInput.press('Tab');
 
     // # Save the keyword
-    await channelsPage.settingsModal.notificationsSettings.save();
+    await notificationsSettings.save();
 
     // # Close the settings modal
-    await channelsPage.settingsModal.closeModal();
+    await settingsModal.closeModal();
 
     // # Post a message without the keyword
     const messageWithoutKeyword = 'This message does not contain the keyword';
@@ -189,25 +186,24 @@ test('MM-T5465-4 Highlighted keywords should not appear in the Recent Mentions',
     await channelsPage.toBeVisible();
 
     // # Open settings modal
-    await channelsPage.globalHeader.openSettings();
-    await channelsPage.settingsModal.toBeVisible();
+    const settingsModal = await channelsPage.globalHeader.openSettings();
 
     // # Open notifications tab
-    await channelsPage.settingsModal.openNotificationsTab();
+    const notificationsSettings = await settingsModal.openNotificationsTab();
 
     // # Open keywords that get highlighted section
-    await channelsPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+    await notificationsSettings.expandSection('keysWithHighlight');
 
     // # Enter the keyword
-    const keywordsInput = await channelsPage.settingsModal.notificationsSettings.getKeywordsInput();
+    const keywordsInput = await notificationsSettings.getKeywordsInput();
     await keywordsInput.fill(keywords[0]);
     await keywordsInput.press('Tab');
 
     // # Save the keyword
-    await channelsPage.settingsModal.notificationsSettings.save();
+    await notificationsSettings.save();
 
     // # Close the settings modal
-    await channelsPage.settingsModal.closeModal();
+    await settingsModal.closeModal();
 
     // # Open the recent mentions
     await channelsPage.globalHeader.openRecentMentions();
@@ -250,25 +246,24 @@ test('MM-T5465-5 Should highlight keywords in message sent from another user', a
     await channelsPage.toBeVisible();
 
     // # Open settings modal
-    await channelsPage.globalHeader.openSettings();
-    await channelsPage.settingsModal.toBeVisible();
+    const settingsModal = await channelsPage.globalHeader.openSettings();
 
     // # Open notifications tab
-    await channelsPage.settingsModal.openNotificationsTab();
+    const notificationsSettings = await settingsModal.openNotificationsTab();
 
     // # Open keywords that get highlighted section
-    await channelsPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+    await notificationsSettings.expandSection('keysWithHighlight');
 
     // # Enter the keyword
-    const keywordsInput = await channelsPage.settingsModal.notificationsSettings.getKeywordsInput();
+    const keywordsInput = await notificationsSettings.getKeywordsInput();
     await keywordsInput.fill(keywords[0]);
     await keywordsInput.press('Tab');
 
     // # Save the keyword
-    await channelsPage.settingsModal.notificationsSettings.save();
+    await notificationsSettings.save();
 
     // # Close the settings modal
-    await channelsPage.settingsModal.closeModal();
+    await settingsModal.closeModal();
 
     // * Verify that the keywords are highlighted in the last message received
     const lastPostWithHighlight = await channelsPage.centerView.getLastPost();
diff --git a/webapp/channels/src/components/user_settings/display/user_settings_theme/color_chooser/__snapshots__/color_chooser.test.tsx.snap b/webapp/channels/src/components/user_settings/display/user_settings_theme/color_chooser/__snapshots__/color_chooser.test.tsx.snap
index b7d948e3a44..ca664fc24fa 100644
--- a/webapp/channels/src/components/user_settings/display/user_settings_theme/color_chooser/__snapshots__/color_chooser.test.tsx.snap
+++ b/webapp/channels/src/components/user_settings/display/user_settings_theme/color_chooser/__snapshots__/color_chooser.test.tsx.snap
@@ -4,6 +4,7 @@ exports[`components/user_settings/display/ColorChooser should match, init 1`] =
 <Fragment>
   <label
     className="custom-label"
+    htmlFor="choose-color-inputColorValue"
   >
     Choose color
   </label>
diff --git a/webapp/channels/src/components/user_settings/display/user_settings_theme/color_chooser/color_chooser.tsx b/webapp/channels/src/components/user_settings/display/user_settings_theme/color_chooser/color_chooser.tsx
index 5ba5369eec3..6dc787ab037 100644
--- a/webapp/channels/src/components/user_settings/display/user_settings_theme/color_chooser/color_chooser.tsx
+++ b/webapp/channels/src/components/user_settings/display/user_settings_theme/color_chooser/color_chooser.tsx
@@ -19,7 +19,12 @@ export default function ColorChooser(props: Props) {
 
     return (
         <>
-            <label className='custom-label'>{props.label}</label>
+            <label
+                className='custom-label'
+                htmlFor={`${props.id}-inputColorValue`}
+            >
+                {props.label}
+            </label>
             <ColorInput
                 id={props.id}
                 value={props.value}
diff --git a/webapp/channels/src/components/user_settings/display/user_settings_theme/user_settings_theme.tsx b/webapp/channels/src/components/user_settings/display/user_settings_theme/user_settings_theme.tsx
index d3436715907..02927d1dad4 100644
--- a/webapp/channels/src/components/user_settings/display/user_settings_theme/user_settings_theme.tsx
+++ b/webapp/channels/src/components/user_settings/display/user_settings_theme/user_settings_theme.tsx
@@ -185,48 +185,39 @@ export default class ThemeSetting extends React.PureComponent<Props, State> {
             if (this.props.allowCustomThemes) {
                 inputs.push(
                     <div
-                        className='radio'
-                        key='premadeThemeColorLabel'
+                        key='premadeCustom'
+                        className='user-settings__radio-group-inline'
                     >
-                        <label>
-                            <input
-                                id='standardThemes'
-                                type='radio'
-                                name='theme'
-                                checked={!displayCustom}
-                                onChange={this.updateType.bind(this, 'premade')}
-                                aria-controls='premadeThemesSection'
-                            />
-                            <FormattedMessage
-                                id='user.settings.display.theme.themeColors'
-                                defaultMessage='Theme Colors'
-                            />
-                        </label>
-                        <br/>
-                    </div>,
-                );
-            }
-
-            if (this.props.allowCustomThemes) {
-                inputs.push(
-                    <div
-                        className='radio'
-                        key='customThemeColorLabel'
-                    >
-                        <label>
-                            <input
-                                id='customThemes'
-                                type='radio'
-                                name='theme'
-                                checked={displayCustom}
-                                onChange={this.updateType.bind(this, 'custom')}
-                                aria-controls='customThemesSection'
-                            />
-                            <FormattedMessage
-                                id='user.settings.display.theme.customTheme'
-                                defaultMessage='Custom Theme'
-                            />
-                        </label>
+                        <div className='radio radio-inline'>
+                            <label>
+                                <input
+                                    id='standardThemes'
+                                    type='radio'
+                                    name='theme'
+                                    checked={!displayCustom}
+                                    onChange={this.updateType.bind(this, 'premade')}
+                                />
+                                <FormattedMessage
+                                    id='user.settings.display.theme.premadeThemes'
+                                    defaultMessage='Premade Themes'
+                                />
+                            </label>
+                        </div>
+                        <div className='radio radio-inline'>
+                            <label>
+                                <input
+                                    id='customThemes'
+                                    type='radio'
+                                    name='theme'
+                                    checked={displayCustom}
+                                    onChange={this.updateType.bind(this, 'custom')}
+                                />
+                                <FormattedMessage
+                                    id='user.settings.display.theme.customTheme'
+                                    defaultMessage='Custom Theme'
+                                />
+                            </label>
+                        </div>
                     </div>,
                 );
 
@@ -271,6 +262,12 @@ export default class ThemeSetting extends React.PureComponent<Props, State> {
 
             themeUI = (
                 <SettingItemMax
+                    title={
+                        <FormattedMessage
+                            id='user.settings.display.theme.title'
+                            defaultMessage='Theme'
+                        />
+                    }
                     inputs={
                         <fieldset>
                             <legend className='hidden-label'>
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 84e8f14ae79..86d86882930 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -5691,7 +5691,7 @@
   "user.settings.display.theme.customTheme": "Custom Theme",
   "user.settings.display.theme.describe": "Open to manage your theme",
   "user.settings.display.theme.otherThemes": "See other themes",
-  "user.settings.display.theme.themeColors": "Theme Colors",
+  "user.settings.display.theme.premadeThemes": "Premade Themes",
   "user.settings.display.theme.title": "Theme",
   "user.settings.display.timezone": "Timezone",
   "user.settings.display.title": "Display Settings",
diff --git a/webapp/channels/src/sass/routes/_settings.scss b/webapp/channels/src/sass/routes/_settings.scss
index bec112465e1..d1f90329432 100644
--- a/webapp/channels/src/sass/routes/_settings.scss
+++ b/webapp/channels/src/sass/routes/_settings.scss
@@ -353,9 +353,11 @@
                             white-space: nowrap;
                         }
 
-                        .premadeThemeButton{
+                        .premadeThemeButton {
+                            padding: 0;
                             border: none;
                             background: none;
+                            text-align: start;
                         }
 
                         label {
@@ -704,6 +706,11 @@
     resize: none;
 }
 
+.user-settings__radio-group-inline {
+    // Counteract the margin-left added by Bootstrap's radio-inline class
+    margin-left: -20px;
+}
+
 .user-settings__submit-checkbox {
     padding-top: 0;
     padding-bottom: 20px;


Reviews:
user: login: pvev
body: LGTM! 🎉 

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T16:52:07Z
user: login: hmhealey
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T17:08:20Z
user: login: saturninoabril
body: Thanks, LGTM except for one request to move the test to accessibility folder.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:33:44Z
user: login: saturninoabril
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:34:07Z
user: login: hmhealey
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T18:59:55Z
user: login: hmhealey
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T19:32:02Z
user: login: asaadmahmood
body: This looks good for the most part, the spacing and alignment are off. We just need to make them like master.

![CleanShot 2025-04-10 at 21 56 48@2x](https://github.com/user-attachments/assets/2720d787-f539-4dd3-b86f-1a2ed09b90fd)
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T16:58:17Z
user: login: asaadmahmood
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:19:41Z
user: login: yasserfaraazkhan
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T00:35:58Z

Review comments:
diff_hunk: @@ -0,0 +1,115 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect} from '@playwright/test';
+
+import {test} from '@e2e-support/test_fixture';
+
+test('Theme settings should be keyboard accessible', async ({pw}) => {
+    const {user} = await pw.initSetup();
+
+    // # Log in as a user in new browser context
+    const {page, channelsPage} = await pw.testBrowser.login(user);
+
+    // # Initialize Axe
+    // const ab = axe.builder(page, {disableColorContrast: true});
+
+    // # Visit default channel page
+    await channelsPage.goto();
+    await channelsPage.toBeVisible();
+
+    // # Open settings modal
+    const settingsModal = await channelsPage.globalHeader.openSettings();
+
+    // * The settings modal should have no accessibility violations
+    // let accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open display tab
+    await settingsModal.container.focus();
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('ArrowDown');
+
+    // * The display tab should be open
+    const {displaySettings} = settingsModal;
+    await displaySettings.toBeVisible();
+
+    // * The display tab should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open the theme section
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Space');
+
+    // * The theme section should be open
+    await displaySettings.verifySectionIsExpanded('theme');
+
+    // * The Premade Themes option should be focused
+    await expect(page.getByLabel('Premade Themes')).toBeFocused();
+
+    // * The theme section for premade themes should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // * Should be able to tab through the options
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Denim'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Sapphire'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Quartz'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Indigo'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Onyx'})).toBeFocused();
+    await page.keyboard.press('Tab');
+
+    // Note: There's no "Apply to all your teams" option because this user is only on one team
+    await expect(page.getByRole('link', {name: 'See other themes'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Save'})).toBeFocused();
+
+    // # Go back to the premade/custom option
+    await page.getByLabel('Premade Themes').focus();
+
+    // # Switch to the custom theme section
+    await page.keyboard.press('ArrowDown');
+
+    // * The Custom Theme option should be focused
+    await expect(page.getByLabel('Custom Theme')).toBeFocused();
+
+    // * Check the ARIA of the Sidebar Styles section
+    await page.keyboard.press('Tab');
+    const sidebarStyles = page.getByRole('button', {name: 'Sidebar Styles'});
+    await expect(sidebarStyles).toBeFocused();
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'false');
+
+    // * Check that we can tab over the collapsed section to the next section and then back again
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Center Channel Styles'})).toBeFocused();
+
+    await page.keyboard.press('Shift+Tab');
+    await expect(sidebarStyles).toBeFocused();
+
+    // * Check that we can expand the section
+    await page.keyboard.press('Enter');
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'true');
+
+    // # Wait for the expanding animation to be open
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeVisible();
+
+    // * Check that we can tab through color pickers
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Text', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Header BG')).toBeFocused();
+
+    // * The theme section for custom themes should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
path: e2e-tests/playwright/tests/functional/channels/settings/display/theme_accessibility.spec.ts
user: login: pvev
body: is this expected to be commented? there are other similar in this file. I am not that familiar with playwright.
created_at: 2025-03-28T16:36:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,115 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect} from '@playwright/test';
+
+import {test} from '@e2e-support/test_fixture';
+
+test('Theme settings should be keyboard accessible', async ({pw}) => {
+    const {user} = await pw.initSetup();
+
+    // # Log in as a user in new browser context
+    const {page, channelsPage} = await pw.testBrowser.login(user);
+
+    // # Initialize Axe
+    // const ab = axe.builder(page, {disableColorContrast: true});
+
+    // # Visit default channel page
+    await channelsPage.goto();
+    await channelsPage.toBeVisible();
+
+    // # Open settings modal
+    const settingsModal = await channelsPage.globalHeader.openSettings();
+
+    // * The settings modal should have no accessibility violations
+    // let accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open display tab
+    await settingsModal.container.focus();
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('ArrowDown');
+
+    // * The display tab should be open
+    const {displaySettings} = settingsModal;
+    await displaySettings.toBeVisible();
+
+    // * The display tab should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open the theme section
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Space');
+
+    // * The theme section should be open
+    await displaySettings.verifySectionIsExpanded('theme');
+
+    // * The Premade Themes option should be focused
+    await expect(page.getByLabel('Premade Themes')).toBeFocused();
+
+    // * The theme section for premade themes should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // * Should be able to tab through the options
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Denim'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Sapphire'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Quartz'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Indigo'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Onyx'})).toBeFocused();
+    await page.keyboard.press('Tab');
+
+    // Note: There's no "Apply to all your teams" option because this user is only on one team
+    await expect(page.getByRole('link', {name: 'See other themes'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Save'})).toBeFocused();
+
+    // # Go back to the premade/custom option
+    await page.getByLabel('Premade Themes').focus();
+
+    // # Switch to the custom theme section
+    await page.keyboard.press('ArrowDown');
+
+    // * The Custom Theme option should be focused
+    await expect(page.getByLabel('Custom Theme')).toBeFocused();
+
+    // * Check the ARIA of the Sidebar Styles section
+    await page.keyboard.press('Tab');
+    const sidebarStyles = page.getByRole('button', {name: 'Sidebar Styles'});
+    await expect(sidebarStyles).toBeFocused();
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'false');
+
+    // * Check that we can tab over the collapsed section to the next section and then back again
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Center Channel Styles'})).toBeFocused();
+
+    await page.keyboard.press('Shift+Tab');
+    await expect(sidebarStyles).toBeFocused();
+
+    // * Check that we can expand the section
+    await page.keyboard.press('Enter');
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'true');
+
+    // # Wait for the expanding animation to be open
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeVisible();
+
+    // * Check that we can tab through color pickers
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Text', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Header BG')).toBeFocused();
+
+    // * The theme section for custom themes should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
path: e2e-tests/playwright/tests/functional/channels/settings/display/theme_accessibility.spec.ts
user: login: hmhealey
body: Oh yeah, I forgot to mention that I intentionally commented those out because of other a11y issues in the settings modal.

There might've been a contrast issue, but the problem I remember is that Axe doesn't like the way that we used ARIA to group items in the settings sidebar. There's no standard pattern for grouping tabs together, and apparently the way we tried to do that is wrong. I made a note to look into that more when we get a chance
created_at: 2025-03-28T17:08:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,115 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect} from '@playwright/test';
+
+import {test} from '@mattermost/playwright-lib';
path: e2e-tests/playwright/tests/functional/channels/settings/display/theme_accessibility.spec.ts
user: login: saturninoabril
body: nit: the same `expect` is also available at `@mattermost/playwright-lib`
created_at: 2025-04-03T02:11:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,115 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
path: e2e-tests/playwright/tests/functional/channels/settings/display/theme_accessibility.spec.ts
user: login: saturninoabril
body: Since this is mainly accessibility test, I suggest to move this to `/e2e-tests/playwright/test/specs/accessibility`. So that eventually, we can easily run Accessibility tests in a separate test group.
created_at: 2025-04-03T02:17:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,115 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect} from '@playwright/test';
+
+import {test} from '@e2e-support/test_fixture';
+
+test('Theme settings should be keyboard accessible', async ({pw}) => {
+    const {user} = await pw.initSetup();
+
+    // # Log in as a user in new browser context
+    const {page, channelsPage} = await pw.testBrowser.login(user);
+
+    // # Initialize Axe
+    // const ab = axe.builder(page, {disableColorContrast: true});
+
+    // # Visit default channel page
+    await channelsPage.goto();
+    await channelsPage.toBeVisible();
+
+    // # Open settings modal
+    const settingsModal = await channelsPage.globalHeader.openSettings();
+
+    // * The settings modal should have no accessibility violations
+    // let accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open display tab
+    await settingsModal.container.focus();
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('ArrowDown');
+
+    // * The display tab should be open
+    const {displaySettings} = settingsModal;
+    await displaySettings.toBeVisible();
+
+    // * The display tab should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open the theme section
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Space');
+
+    // * The theme section should be open
+    await displaySettings.verifySectionIsExpanded('theme');
+
+    // * The Premade Themes option should be focused
+    await expect(page.getByLabel('Premade Themes')).toBeFocused();
+
+    // * The theme section for premade themes should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // * Should be able to tab through the options
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Denim'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Sapphire'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Quartz'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Indigo'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Onyx'})).toBeFocused();
+    await page.keyboard.press('Tab');
+
+    // Note: There's no "Apply to all your teams" option because this user is only on one team
+    await expect(page.getByRole('link', {name: 'See other themes'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Save'})).toBeFocused();
+
+    // # Go back to the premade/custom option
+    await page.getByLabel('Premade Themes').focus();
+
+    // # Switch to the custom theme section
+    await page.keyboard.press('ArrowDown');
+
+    // * The Custom Theme option should be focused
+    await expect(page.getByLabel('Custom Theme')).toBeFocused();
+
+    // * Check the ARIA of the Sidebar Styles section
+    await page.keyboard.press('Tab');
+    const sidebarStyles = page.getByRole('button', {name: 'Sidebar Styles'});
+    await expect(sidebarStyles).toBeFocused();
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'false');
+
+    // * Check that we can tab over the collapsed section to the next section and then back again
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Center Channel Styles'})).toBeFocused();
+
+    await page.keyboard.press('Shift+Tab');
+    await expect(sidebarStyles).toBeFocused();
+
+    // * Check that we can expand the section
+    await page.keyboard.press('Enter');
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'true');
+
+    // # Wait for the expanding animation to be open
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeVisible();
+
+    // * Check that we can tab through color pickers
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Text', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Header BG')).toBeFocused();
+
+    // * The theme section for custom themes should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
path: e2e-tests/playwright/tests/functional/channels/settings/display/theme_accessibility.spec.ts
user: login: saturninoabril
body: One option to incrementally run Accessibility tests on a page is by using flags like `disableRules`, `include` and `exclude` (see https://www.npmjs.com/package/@axe-core/playwright). This way, we can protect specific sections/components with Accessibility in place, then gradually lift the flags until the entire page is covered. Also, I'd recommend adding a comment on the related ticket to track any ongoing work with these flags.
created_at: 2025-04-03T02:32:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,115 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect} from '@playwright/test';
+
+import {test} from '@mattermost/playwright-lib';
path: e2e-tests/playwright/tests/functional/channels/settings/display/theme_accessibility.spec.ts
user: login: hmhealey
body: Will do!

We have a similar issue in the web app code with React Testing Library exports that we re-export from our testing utilities. We might want to look into using the `no-restricted-imports` ESLint rule for both of those to push people to using the proper import
created_at: 2025-04-03T18:59:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,115 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect} from '@playwright/test';
+
+import {test} from '@e2e-support/test_fixture';
+
+test('Theme settings should be keyboard accessible', async ({pw}) => {
+    const {user} = await pw.initSetup();
+
+    // # Log in as a user in new browser context
+    const {page, channelsPage} = await pw.testBrowser.login(user);
+
+    // # Initialize Axe
+    // const ab = axe.builder(page, {disableColorContrast: true});
+
+    // # Visit default channel page
+    await channelsPage.goto();
+    await channelsPage.toBeVisible();
+
+    // # Open settings modal
+    const settingsModal = await channelsPage.globalHeader.openSettings();
+
+    // * The settings modal should have no accessibility violations
+    // let accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open display tab
+    await settingsModal.container.focus();
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('ArrowDown');
+
+    // * The display tab should be open
+    const {displaySettings} = settingsModal;
+    await displaySettings.toBeVisible();
+
+    // * The display tab should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // # Open the theme section
+    await page.keyboard.press('Tab');
+    await page.keyboard.press('Space');
+
+    // * The theme section should be open
+    await displaySettings.verifySectionIsExpanded('theme');
+
+    // * The Premade Themes option should be focused
+    await expect(page.getByLabel('Premade Themes')).toBeFocused();
+
+    // * The theme section for premade themes should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
+    // expect(accessibilityScanResults.violations).toHaveLength(0);
+
+    // * Should be able to tab through the options
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Denim'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Sapphire'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Quartz'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Indigo'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Onyx'})).toBeFocused();
+    await page.keyboard.press('Tab');
+
+    // Note: There's no "Apply to all your teams" option because this user is only on one team
+    await expect(page.getByRole('link', {name: 'See other themes'})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Save'})).toBeFocused();
+
+    // # Go back to the premade/custom option
+    await page.getByLabel('Premade Themes').focus();
+
+    // # Switch to the custom theme section
+    await page.keyboard.press('ArrowDown');
+
+    // * The Custom Theme option should be focused
+    await expect(page.getByLabel('Custom Theme')).toBeFocused();
+
+    // * Check the ARIA of the Sidebar Styles section
+    await page.keyboard.press('Tab');
+    const sidebarStyles = page.getByRole('button', {name: 'Sidebar Styles'});
+    await expect(sidebarStyles).toBeFocused();
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'false');
+
+    // * Check that we can tab over the collapsed section to the next section and then back again
+    await page.keyboard.press('Tab');
+    await expect(page.getByRole('button', {name: 'Center Channel Styles'})).toBeFocused();
+
+    await page.keyboard.press('Shift+Tab');
+    await expect(sidebarStyles).toBeFocused();
+
+    // * Check that we can expand the section
+    await page.keyboard.press('Enter');
+    await expect(sidebarStyles).toHaveAttribute('aria-expanded', 'true');
+
+    // # Wait for the expanding animation to be open
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeVisible();
+
+    // * Check that we can tab through color pickers
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar BG', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Text', {exact: true})).toBeFocused();
+    await page.keyboard.press('Tab');
+    await expect(page.getByLabel('Sidebar Header BG')).toBeFocused();
+
+    // * The theme section for custom themes should have no accessibility violations
+    // accessibilityScanResults = await ab.analyze()
path: e2e-tests/playwright/tests/functional/channels/settings/display/theme_accessibility.spec.ts
user: login: hmhealey
body: Great idea. I just went into this and found that one of the failures is actually something added in https://github.com/mattermost/mattermost/pull/29596 which we did wrong (having `aria-controls` on the Premade Themes/Custom Themes inputs), so we can turn on with exceptions to ensure we don't do anything new wrong
created_at: 2025-04-03T19:32:02Z
author_association: MEMBER

PR comments:
user: login: unified-ci-app[bot]
created_at: 2025-03-28T03:48:59Z
body: E2E test triggered successfully for PR #30584. The corresponding commit's status check will be available shortly.

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-28T03:49:45Z
body: E2E test run is starting for commit `5270c9bb5fdba1c025ca0f04bf9306cecd6461f5`.
To check the run progress:
- Cypress: look for commit status `E2E Tests/test` or the access the [Automation Dashboard Cycle URL](https://automation-dashboard.vercel.app/cycle/14121583454_1-5270c9b-pr-onprem-ent).
- Playwright: look for commit status `E2E Tests/test-playwright`.

You can also look at the [E2E test's Workflow Run URL](https://github.com/mattermost/mattermost/actions/runs/14121583454) (run ID `14121583454`).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-28T04:36:46Z
body: E2E test has completed for commit `5270c9bb5fdba1c025ca0f04bf9306cecd6461f5`.
Results summary:
- Cypress: pass rate is `99.84%` (see [Automation Dashboard](https://automation-dashboard.vercel.app/cycle/14121583454_1-5270c9b-pr-onprem-ent) and commit status check `E2E Tests/test`).
- Playwright: pass rate is `98.51%` (see commit status check `E2E Tests/test-playwright`).

The run summary artifacts are available in the corresponding [Workflow Run](https://github.com/mattermost/mattermost/actions/runs/14121583454).

author_association: NONE
user: login: unified-ci-app[bot]
created_at: 2025-03-28T15:50:05Z
body: E2E tests requested: they will start automatically when status check `Enterprise CI/docker-image` is successful.

author_association: CONTRIBUTOR
user: login: hmhealey
created_at: 2025-03-28T15:51:01Z
body: The failing Cypress tests involve Playbooks, and the failing Playwright test is for a message about scheduled posts. Those seem like they're probably flaky or caused by missing something from the latest master
author_association: MEMBER
user: login: unified-ci-app[bot]
created_at: 2025-03-28T16:09:19Z
body: Docker image is now available: E2E test triggered successfully for PR #30584. The corresponding commit's status check will be available shortly.

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-28T16:10:05Z
body: E2E test run is starting for commit `606f9d27e7573c4e7c67b3a5ce6b9c22bed32b40`.
To check the run progress:
- Cypress: look for commit status `E2E Tests/test` or the access the [Automation Dashboard Cycle URL](https://automation-dashboard.vercel.app/cycle/14133147534_1-606f9d2-pr-onprem-ent).
- Playwright: look for commit status `E2E Tests/test-playwright`.

You can also look at the [E2E test's Workflow Run URL](https://github.com/mattermost/mattermost/actions/runs/14133147534) (run ID `14133147534`).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-28T16:57:45Z
body: E2E test has completed for commit `606f9d27e7573c4e7c67b3a5ce6b9c22bed32b40`.
Results summary:
- Cypress: pass rate is `100.00%` (see [Automation Dashboard](https://automation-dashboard.vercel.app/cycle/14133147534_1-606f9d2-pr-onprem-ent) and commit status check `E2E Tests/test`).
- Playwright: pass rate is `100.00%` (see commit status check `E2E Tests/test-playwright`).

The run summary artifacts are available in the corresponding [Workflow Run](https://github.com/mattermost/mattermost/actions/runs/14133147534).

author_association: NONE
user: login: unified-ci-app[bot]
created_at: 2025-04-03T20:49:22Z
body: E2E tests requested: they will start automatically when status check `Enterprise CI/docker-image` is successful.

author_association: CONTRIBUTOR
user: login: unified-ci-app[bot]
created_at: 2025-04-03T21:07:02Z
body: Docker image is now available: E2E test triggered successfully for PR #30584. The corresponding commit's status check will be available shortly.

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T21:07:47Z
body: E2E test run is starting for commit `4e3a4d4db1fcc32aa4f8fb69e74a012f874fe031`.
To check the run progress:
- Cypress: look for commit status `E2E Tests/test` or the access the [Automation Dashboard Cycle URL](https://automation-dashboard.vercel.app/cycle/14252790219_1-4e3a4d4-pr-onprem-ent).
- Playwright: look for commit status `E2E Tests/test-playwright`.

You can also look at the [E2E test's Workflow Run URL](https://github.com/mattermost/mattermost/actions/runs/14252790219) (run ID `14252790219`).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-03T21:54:54Z
body: E2E test has completed for commit `4e3a4d4db1fcc32aa4f8fb69e74a012f874fe031`.
Results summary:
- Cypress: pass rate is `100.00%` (see [Automation Dashboard](https://automation-dashboard.vercel.app/cycle/14252790219_1-4e3a4d4-pr-onprem-ent) and commit status check `E2E Tests/test`).
- Playwright: pass rate is `96.97%` (see commit status check `E2E Tests/test-playwright`).

The run summary artifacts are available in the corresponding [Workflow Run](https://github.com/mattermost/mattermost/actions/runs/14252790219).

author_association: NONE
user: login: hmhealey
created_at: 2025-04-03T22:23:13Z
body: The failing Playwright tests don't seem to be related to my changes
author_association: MEMBER
user: login: hmhealey
created_at: 2025-04-10T19:13:32Z
body: I think that's actually an issue that's already on master. I see what you reported on Hub on 10.6.1, but that change would be on 10.7.0 as well. 
Hub|Community|PR
-|-|-
![Screenshot 2025-04-10 at 3 03 12 PM](https://github.com/user-attachments/assets/d1740a20-99df-4fab-9f14-407cd902d0e5)|![Screenshot 2025-04-10 at 3 01 37 PM](https://github.com/user-attachments/assets/0dcc6913-0510-4773-a467-282db32ae655)|![Screenshot 2025-04-10 at 3 01 27 PM](https://github.com/user-attachments/assets/d227ecda-7fe0-4f01-948b-662955e4f5a9)

I'll see how hard it is to change that, but we may have to merge this one as-is and file a followup for that
author_association: MEMBER
user: login: hmhealey
created_at: 2025-04-10T19:43:25Z
body: I changed the spacing on the premade themes to match what they were before #29596, and I got rid of the extra margin that Bootstrap added before the new radio buttons

Hub|PR
-|-
![Screenshot 2025-04-10 at 15-39-23 Hub](https://github.com/user-attachments/assets/1abf4acd-273f-4d60-a725-aecf506099e7)|![Screenshot 2025-04-10 at 15-39-00 Local](https://github.com/user-attachments/assets/b4fec609-d5c1-4713-bd1a-9a715ec16f0d)

author_association: MEMBER
user: login: hmhealey
created_at: 2025-04-14T18:44:09Z
body: /e2e-test
author_association: MEMBER
user: login: unified-ci-app[bot]
created_at: 2025-04-14T18:44:30Z
body: Not triggering E2E tests: you need to wait until `Enterprise CI/docker-image` status check is successful, before triggering E2E tests.

author_association: CONTRIBUTOR
user: login: hmhealey
created_at: 2025-04-14T19:20:58Z
body: /e2e-test
author_association: MEMBER
user: login: unified-ci-app[bot]
created_at: 2025-04-14T19:21:18Z
body: Not triggering E2E tests: you need to wait until `Enterprise CI/docker-image` status check is successful, before triggering E2E tests.

author_association: CONTRIBUTOR
user: login: unified-ci-app[bot]
created_at: 2025-04-14T21:13:54Z
body: E2E test triggered successfully for PR #30584. The corresponding commit's status check will be available shortly.

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-14T21:14:39Z
body: E2E test run is starting for commit `ed5b80995e034eae703f13da8bae0bba17b8b90f`.
To check the run progress:
- Cypress: look for commit status `E2E Tests/test` or the access the [Automation Dashboard Cycle URL](https://automation-dashboard.vercel.app/cycle/14455892005_1-ed5b809-pr-onprem-ent).
- Playwright: look for commit status `E2E Tests/test-playwright`.

You can also look at the [E2E test's Workflow Run URL](https://github.com/mattermost/mattermost/actions/runs/14455892005) (run ID `14455892005`).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T22:02:08Z
body: E2E test has completed for commit `ed5b80995e034eae703f13da8bae0bba17b8b90f`.
Results summary:
- Cypress: pass rate is `100.00%` (see [Automation Dashboard](https://automation-dashboard.vercel.app/cycle/14455892005_1-ed5b809-pr-onprem-ent) and commit status check `E2E Tests/test`).
- Playwright: pass rate is `100.00%` (see commit status check `E2E Tests/test-playwright`).

The run summary artifacts are available in the corresponding [Workflow Run](https://github.com/mattermost/mattermost/actions/runs/14455892005).

author_association: NONE
user: login: mm-cloud-bot
created_at: 2025-04-17T16:05:41Z
body: Test server destroyed
author_association: NONE