diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 818a6bbef..da4ad4762 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -84,6 +84,7 @@ add = Add
 add_all = Add All
 remove = Remove
 remove_all = Remove All
+remove_label_str = Remove item "%s"
 edit = Edit
 
 enabled = Enabled
diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl
index ca8c7e6a7..62fb10d89 100644
--- a/templates/base/head_script.tmpl
+++ b/templates/base/head_script.tmpl
@@ -41,6 +41,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
 			copy_error: '{{.locale.Tr "copy_error"}}',
 			error_occurred: '{{.locale.Tr "error.occurred"}}',
 			network_error: '{{.locale.Tr "error.network_error"}}',
+			remove_label_str: '{{.locale.Tr "remove_label_str"}}',
 		},
 	};
 	{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index cdb913280..113ff2e1f 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -4,7 +4,6 @@ import {mqBinarySearch} from '../utils.js';
 import {createDropzone} from './dropzone.js';
 import {initCompColorPicker} from './comp/ColorPicker.js';
 import {showGlobalErrorMessage} from '../bootstrap.js';
-import {attachCheckboxAria, attachDropdownAria} from './aria.js';
 import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
 import {initTooltip} from '../modules/tippy.js';
 import {svg} from '../svg.js';
@@ -123,9 +122,7 @@ export function initGlobalCommon() {
   $uiDropdowns.filter('.slide.up').dropdown({transition: 'slide up'});
   $uiDropdowns.filter('.upward').dropdown({direction: 'upward'});
 
-  attachDropdownAria($uiDropdowns);
-
-  attachCheckboxAria($('.ui.checkbox'));
+  $('.ui.checkbox').checkbox();
 
   $('.tabular.menu .item').tab();
   $('.tabable.menu .item').tab();
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 480661118..7d74ee6b9 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -89,6 +89,8 @@ import {initFormattingReplacements} from './features/formatting.js';
 import {initCopyContent} from './features/copycontent.js';
 import {initCaptcha} from './features/captcha.js';
 import {initRepositoryActionView} from './components/RepoActionView.vue';
+import {initAriaCheckboxPatch} from './modules/aria/checkbox.js';
+import {initAriaDropdownPatch} from './modules/aria/dropdown.js';
 
 // Run time-critical code as soon as possible. This is safe to do because this
 // script appears at the end of <body> and rendered HTML is accessible at that point.
@@ -98,6 +100,9 @@ initFormattingReplacements();
 $.fn.tab.settings.silent = true;
 // Disable the behavior of fomantic to toggle the checkbox when you press enter on a checkbox element.
 $.fn.checkbox.settings.enableEnterKey = false;
+// Use the patches to improve accessibility, these patches are designed to be as independent as possible, make it easy to modify or remove in the future.
+initAriaCheckboxPatch();
+initAriaDropdownPatch();
 
 $(document).ready(() => {
   initGlobalCommon();
diff --git a/web_src/js/features/aria.md b/web_src/js/modules/aria/aria.md
similarity index 88%
rename from web_src/js/features/aria.md
rename to web_src/js/modules/aria/aria.md
index 679cec774..a32d15f46 100644
--- a/web_src/js/features/aria.md
+++ b/web_src/js/modules/aria/aria.md
@@ -23,6 +23,14 @@ To test the aria/accessibility with screen readers, developers can use the follo
   * Double-finger swipe means old single-finger swipe.
 * TODO: on Windows, on Linux, on iOS
 
+# Known Problems
+
+* Tested with Apple VoiceOver: If a dropdown menu/combobox is opened by mouse click, then arrow keys don't work.
+  But if the dropdown is opened by keyboard Tab, then arrow keys work, and from then on, the keys almost work with mouse click too.
+  The clue: when the dropdown is only opened by mouse click, VoiceOver doesn't send 'keydown' events of arrow keys to the DOM,
+  VoiceOver expects to use arrow keys to navigate between some elements, but it couldn't.
+  Users could use Option+ArrowKeys to navigate between menu/combobox items or selection labels if the menu/combobox is opened by mouse click.
+
 # Checkbox
 
 ## Accessibility-friendly Checkbox
@@ -52,9 +60,7 @@ There is still a problem: Fomantic UI checkbox is not friendly to screen readers
 so we add IDs to all the Fomantic UI checkboxes automatically by JS.
 If the `label` part is empty, then the checkbox needs to get the `aria-label` attribute manually.
 
-# Dropdown
-
-## Fomantic UI Dropdown
+# Fomantic Dropdown
 
 Fomantic Dropdown is designed to be used for many purposes:
 
diff --git a/web_src/js/modules/aria/base.js b/web_src/js/modules/aria/base.js
new file mode 100644
index 000000000..c4a01038b
--- /dev/null
+++ b/web_src/js/modules/aria/base.js
@@ -0,0 +1,5 @@
+let ariaIdCounter = 0;
+
+export function generateAriaId() {
+  return `_aria_auto_id_${ariaIdCounter++}`;
+}
diff --git a/web_src/js/modules/aria/checkbox.js b/web_src/js/modules/aria/checkbox.js
new file mode 100644
index 000000000..08af1c2eb
--- /dev/null
+++ b/web_src/js/modules/aria/checkbox.js
@@ -0,0 +1,38 @@
+import $ from 'jquery';
+import {generateAriaId} from './base.js';
+
+const ariaPatchKey = '_giteaAriaPatchCheckbox';
+const fomanticCheckboxFn = $.fn.checkbox;
+
+// use our own `$.fn.checkbox` to patch Fomantic's checkbox module
+export function initAriaCheckboxPatch() {
+  if ($.fn.checkbox === ariaCheckboxFn) throw new Error('initAriaCheckboxPatch could only be called once');
+  $.fn.checkbox = ariaCheckboxFn;
+  ariaCheckboxFn.settings = fomanticCheckboxFn.settings;
+}
+
+// the patched `$.fn.checkbox` checkbox function
+// * it does the one-time attaching on the first call
+function ariaCheckboxFn(...args) {
+  const ret = fomanticCheckboxFn.apply(this, args);
+  for (const el of this) {
+    if (el[ariaPatchKey]) continue;
+    attachInit(el);
+  }
+  return ret;
+}
+
+function attachInit(el) {
+  // Fomantic UI checkbox needs to be something like: <div class="ui checkbox"><label /><input /></div>
+  // It doesn't work well with <label><input />...</label>
+  // To make it work with aria, the "id"/"for" attributes are necessary, so add them automatically if missing.
+  // In the future, refactor to use native checkbox directly, then this patch could be removed.
+  el[ariaPatchKey] = {}; // record that this element has been patched
+  const label = el.querySelector('label');
+  const input = el.querySelector('input');
+  if (!label || !input || input.getAttribute('id')) return;
+
+  const id = generateAriaId();
+  input.setAttribute('id', id);
+  label.setAttribute('for', id);
+}
diff --git a/web_src/js/features/aria.js b/web_src/js/modules/aria/dropdown.js
similarity index 52%
rename from web_src/js/features/aria.js
rename to web_src/js/modules/aria/dropdown.js
index 676f4cd56..70d524cfe 100644
--- a/web_src/js/features/aria.js
+++ b/web_src/js/modules/aria/dropdown.js
@@ -1,14 +1,116 @@
 import $ from 'jquery';
+import {generateAriaId} from './base.js';
 
-let ariaIdCounter = 0;
+const ariaPatchKey = '_giteaAriaPatchDropdown';
+const fomanticDropdownFn = $.fn.dropdown;
 
-function generateAriaId() {
-  return `_aria_auto_id_${ariaIdCounter++}`;
+// use our own `$().dropdown` function to patch Fomantic's dropdown module
+export function initAriaDropdownPatch() {
+  if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once');
+  $.fn.dropdown = ariaDropdownFn;
+  ariaDropdownFn.settings = fomanticDropdownFn.settings;
 }
 
-function attachOneDropdownAria($dropdown) {
-  if ($dropdown.attr('data-aria-attached') || $dropdown.hasClass('custom')) return;
-  $dropdown.attr('data-aria-attached', 1);
+// the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and:
+// * it does the one-time attaching on the first call
+// * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes
+function ariaDropdownFn(...args) {
+  const ret = fomanticDropdownFn.apply(this, args);
+
+  // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
+  // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
+  const needDelegate = (!args.length || typeof args[0] !== 'string');
+  for (const el of this) {
+    const $dropdown = $(el);
+    if (!el[ariaPatchKey]) {
+      attachInit($dropdown);
+    }
+    if (needDelegate) {
+      delegateOne($dropdown);
+    }
+  }
+  return ret;
+}
+
+// make the item has role=option/menuitem, add an id if there wasn't one yet, make items as non-focusable
+// the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element.
+function updateMenuItem(dropdown, item) {
+  if (!item.id) item.id = generateAriaId();
+  item.setAttribute('role', dropdown[ariaPatchKey].listItemRole);
+  item.setAttribute('tabindex', '-1');
+  for (const a of item.querySelectorAll('a')) a.setAttribute('tabindex', '-1');
+}
+
+// make the label item and its "delete icon" has correct aria attributes
+function updateSelectionLabel($label) {
+  // the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
+  if (!$label.attr('id')) $label.attr('id', generateAriaId());
+  $label.attr('tabindex', '-1');
+  $label.find('.delete.icon').attr({
+    'aria-hidden': 'false',
+    'aria-label': window.config.i18n.remove_label_str.replace('%s', $label.attr('data-value')),
+    'role': 'button',
+  });
+}
+
+// delegate the dropdown's template functions and callback functions to add aria attributes.
+function delegateOne($dropdown) {
+  const dropdownCall = fomanticDropdownFn.bind($dropdown);
+
+  // the "template" functions are used for dynamic creation (eg: AJAX)
+  const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()};
+  const dropdownTemplatesMenuOld = dropdownTemplates.menu;
+  dropdownTemplates.menu = function(response, fields, preserveHTML, className) {
+    // when the dropdown menu items are loaded from AJAX requests, the items are created dynamically
+    const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className);
+    const $wrapper = $('<div>').append(menuItems);
+    const $items = $wrapper.find('> .item');
+    $items.each((_, item) => updateMenuItem($dropdown[0], item));
+    $dropdown[0][ariaPatchKey].deferredRefreshAriaActiveItem();
+    return $wrapper.html();
+  };
+  dropdownCall('setting', 'templates', dropdownTemplates);
+
+  // the `onLabelCreate` is used to add necessary aria attributes for dynamically created selection labels
+  const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate');
+  dropdownCall('setting', 'onLabelCreate', function(value, text) {
+    const $label = dropdownOnLabelCreateOld.call(this, value, text);
+    updateSelectionLabel($label);
+    return $label;
+  });
+}
+
+// for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes
+function attachStaticElements($dropdown, $focusable, $menu) {
+  const dropdown = $dropdown[0];
+
+  // prepare static dropdown menu list popup
+  if (!$menu.attr('id')) $menu.attr('id', generateAriaId());
+  $menu.find('> .item').each((_, item) => updateMenuItem(dropdown, item));
+  // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash
+  $menu.attr('role', dropdown[ariaPatchKey].listPopupRole);
+
+  // prepare selection label items
+  $dropdown.find('.ui.label').each((_, label) => updateSelectionLabel($(label)));
+
+  // make the primary element (focusable) aria-friendly
+  $focusable.attr({
+    'role': $focusable.attr('role') ?? dropdown[ariaPatchKey].focusableRole,
+    'aria-haspopup': dropdown[ariaPatchKey].listPopupRole,
+    'aria-controls': $menu.attr('id'),
+    'aria-expanded': 'false',
+  });
+
+  // use tooltip's content as aria-label if there is no aria-label
+  if ($dropdown.hasClass('tooltip') && $dropdown.attr('data-content') && !$dropdown.attr('aria-label')) {
+    $dropdown.attr('aria-label', $dropdown.attr('data-content'));
+  }
+}
+
+function attachInit($dropdown) {
+  const dropdown = $dropdown[0];
+  dropdown[ariaPatchKey] = {};
+  if ($dropdown.hasClass('custom')) return;
 
   // Dropdown has 2 different focusing behaviors
   // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
@@ -23,71 +125,39 @@ function attachOneDropdownAria($dropdown) {
   //    - if the menu item is clickable (eg: <a>), then trigger the click event
   //    - otherwise, the dropdown control (low-level code) handles the Enter event, hides the dropdown menu
 
-  // TODO: multiple selection is not supported yet.
+  // TODO: multiple selection is only partially supported. Check and test them one by one in the future.
 
   const $textSearch = $dropdown.find('input.search').eq(0);
   const $focusable = $textSearch.length ? $textSearch : $dropdown; // the primary element for focus, see comment above
   if (!$focusable.length) return;
 
+  let $menu = $dropdown.find('> .menu');
+  if (!$menu.length) {
+    // some "multiple selection" dropdowns don't have a static menu element in HTML, we need to pre-create it to make it have correct aria attributes
+    $menu = $('<div class="menu"></div>').appendTo($dropdown);
+  }
+
   // There are 2 possible solutions about the role: combobox or menu.
   // The idea is that if there is an input, then it's a combobox, otherwise it's a menu.
   // Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before.
   const isComboBox = $dropdown.find('input').length > 0;
 
-  const focusableRole = isComboBox ? 'combobox' : 'button';
-  const listPopupRole = isComboBox ? 'listbox' : 'menu';
-  const listItemRole = isComboBox ? 'option' : 'menuitem';
+  dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'button';
+  dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : 'menu';
+  dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem';
 
-  // make the item has role=option/menuitem, add an id if there wasn't one yet, make items as non-focusable
-  // the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element.
-  function prepareMenuItem($item) {
-    if (!$item.attr('id')) $item.attr('id', generateAriaId());
-    $item.attr({'role': listItemRole, 'tabindex': '-1'});
-    $item.find('a').attr('tabindex', '-1');
-  }
-
-  // delegate the dropdown's template function to add aria attributes.
-  // the "template" functions are used for dynamic creation (eg: AJAX)
-  const dropdownTemplates = {...$dropdown.dropdown('setting', 'templates')};
-  const dropdownTemplatesMenuOld = dropdownTemplates.menu;
-  dropdownTemplates.menu = function(response, fields, preserveHTML, className) {
-    // when the dropdown menu items are loaded from AJAX requests, the items are created dynamically
-    const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className);
-    const $wrapper = $('<div>').append(menuItems);
-    const $items = $wrapper.find('> .item');
-    $items.each((_, item) => prepareMenuItem($(item)));
-    return $wrapper.html();
-  };
-  $dropdown.dropdown('setting', 'templates', dropdownTemplates);
-
-  // use tooltip's content as aria-label if there is no aria-label
-  if ($dropdown.hasClass('tooltip') && $dropdown.attr('data-content') && !$dropdown.attr('aria-label')) {
-    $dropdown.attr('aria-label', $dropdown.attr('data-content'));
-  }
-
-  // prepare dropdown menu list popup
-  const $menu = $dropdown.find('> .menu');
-  if (!$menu.attr('id')) $menu.attr('id', generateAriaId());
-  $menu.find('> .item').each((_, item) => {
-    prepareMenuItem($(item));
-  });
-  // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash
-  $menu.attr('role', listPopupRole);
-
-  // make the primary element (focusable) aria-friendly
-  $focusable.attr({
-    'role': $focusable.attr('role') ?? focusableRole,
-    'aria-haspopup': listPopupRole,
-    'aria-controls': $menu.attr('id'),
-    'aria-expanded': 'false',
-  });
+  attachDomEvents($dropdown, $focusable, $menu);
+  attachStaticElements($dropdown, $focusable, $menu);
+}
 
+function attachDomEvents($dropdown, $focusable, $menu) {
+  const dropdown = $dropdown[0];
   // when showing, it has class: ".animating.in"
   // when hiding, it has class: ".visible.animating.out"
   const isMenuVisible = () => ($menu.hasClass('visible') && !$menu.hasClass('out')) || $menu.hasClass('in');
 
   // update aria attributes according to current active/selected item
-  const refreshAria = () => {
+  const refreshAriaActiveItem = () => {
     const menuVisible = isMenuVisible();
     $focusable.attr('aria-expanded', menuVisible ? 'true' : 'false');
 
@@ -97,7 +167,7 @@ function attachOneDropdownAria($dropdown) {
     // if the popup is visible and has an active/selected item, use its id as aria-activedescendant
     if (menuVisible) {
       $focusable.attr('aria-activedescendant', $active.attr('id'));
-    } else if (!isComboBox) {
+    } else if (dropdown[ariaPatchKey].listPopupRole === 'menu') {
       // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item
       $focusable.removeAttr('aria-activedescendant');
       $active.removeClass('active').removeClass('selected');
@@ -107,7 +177,8 @@ function attachOneDropdownAria($dropdown) {
   $dropdown.on('keydown', (e) => {
     // here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
     if (e.key === 'Enter') {
-      let $item = $dropdown.dropdown('get item', $dropdown.dropdown('get value'));
+      const dropdownCall = fomanticDropdownFn.bind($dropdown);
+      let $item = dropdownCall('get item', dropdownCall('get value'));
       if (!$item) $item = $menu.find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
       // if the selected item is clickable, then trigger the click event.
       // we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
@@ -119,8 +190,9 @@ function attachOneDropdownAria($dropdown) {
   // do not return any value, jQuery has return-value related behaviors.
   // when the popup is hiding, it's better to have a small "delay", because there is a Fomantic UI animation
   // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation.
-  const deferredRefreshAria = (delay = 0) => { setTimeout(refreshAria, delay) };
-  $dropdown.on('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAria(); });
+  const deferredRefreshAriaActiveItem = (delay = 0) => { setTimeout(refreshAriaActiveItem, delay) };
+  dropdown[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem;
+  $dropdown.on('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); });
 
   // if the dropdown has been opened by focus, do not trigger the next click event again.
   // otherwise the dropdown will be closed immediately, especially on Android with TalkBack
@@ -128,26 +200,26 @@ function attachOneDropdownAria($dropdown) {
   // * mobile event sequence: focus -> mousedown -> mouseup -> click
   // Fomantic may stop propagation of blur event, use capture to make sure we can still get the event
   let ignoreClickPreEvents = 0, ignoreClickPreVisible = 0;
-  $dropdown[0].addEventListener('mousedown', () => {
+  dropdown.addEventListener('mousedown', () => {
     ignoreClickPreVisible += isMenuVisible() ? 1 : 0;
     ignoreClickPreEvents++;
   }, true);
-  $dropdown[0].addEventListener('focus', () => {
+  dropdown.addEventListener('focus', () => {
     ignoreClickPreVisible += isMenuVisible() ? 1 : 0;
     ignoreClickPreEvents++;
-    deferredRefreshAria();
+    deferredRefreshAriaActiveItem();
   }, true);
-  $dropdown[0].addEventListener('blur', () => {
+  dropdown.addEventListener('blur', () => {
     ignoreClickPreVisible = ignoreClickPreEvents = 0;
-    deferredRefreshAria(100);
+    deferredRefreshAriaActiveItem(100);
   }, true);
-  $dropdown[0].addEventListener('mouseup', () => {
+  dropdown.addEventListener('mouseup', () => {
     setTimeout(() => {
       ignoreClickPreVisible = ignoreClickPreEvents = 0;
-      deferredRefreshAria(100);
+      deferredRefreshAriaActiveItem(100);
     }, 0);
   }, true);
-  $dropdown[0].addEventListener('click', (e) => {
+  dropdown.addEventListener('click', (e) => {
     if (isMenuVisible() &&
       ignoreClickPreVisible !== 2 && // dropdown is switch from invisible to visible
       ignoreClickPreEvents === 2 // the click event is related to mousedown+focus
@@ -157,24 +229,3 @@ function attachOneDropdownAria($dropdown) {
     ignoreClickPreEvents = ignoreClickPreVisible = 0;
   }, true);
 }
-
-export function attachDropdownAria($dropdowns) {
-  $dropdowns.each((_, e) => attachOneDropdownAria($(e)));
-}
-
-export function attachCheckboxAria($checkboxes) {
-  $checkboxes.checkbox();
-
-  // Fomantic UI checkbox needs to be something like: <div class="ui checkbox"><label /><input /></div>
-  // It doesn't work well with <label><input />...</label>
-  // To make it work with aria, the "id"/"for" attributes are necessary, so add them automatically if missing.
-  // In the future, refactor to use native checkbox directly, then this patch could be removed.
-  for (const el of $checkboxes) {
-    const label = el.querySelector('label');
-    const input = el.querySelector('input');
-    if (!label || !input || input.getAttribute('id')) continue;
-    const id = generateAriaId();
-    input.setAttribute('id', id);
-    label.setAttribute('for', id);
-  }
-}