diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9528708623..a7dd59ec3f 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -811,7 +811,10 @@ repo_and_org_access = Repository and Organization Access
 permissions_public_only = Public only
 permissions_access_all = All (public, private, and limited)
 select_permissions = Select permissions
-scoped_token_desc = Selected token scopes limit authentication only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information.
+permission_no_access = No Access
+permission_read = Read
+permission_write = Read and Write
+access_token_desc = Selected token permissions limit authorization only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information.
 at_least_one_permission = You must select at least one permission to create a token
 permissions_list = Permissions:
 
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 2b7db82dae..b889f9c0cb 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -69,20 +69,17 @@
 					<summary class="gt-pb-4 gt-pl-2">
 						{{.locale.Tr "settings.select_permissions"}}
 					</summary>
-					<div class="activity meta">
-						<i>{{$.locale.Tr "settings.scoped_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i>
+					<p class="activity meta">
+						<i>{{$.locale.Tr "settings.access_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i>
+					</p>
+					<div class="scoped-access-token-mount">
+						<scoped-access-token-selector
+							:is-admin="{{if .IsAdmin}}true{{else}}false{{end}}"
+							no-access-label="{{.locale.Tr "settings.permission_no_access"}}"
+							read-label="{{.locale.Tr "settings.permission_read"}}"
+							write-label="{{.locale.Tr "settings.permission_write"}}"
+						></scoped-access-token-selector>
 					</div>
-					<scoped-access-token-category category="activitypub"></scoped-access-token-category>
-					{{if .IsAdmin}}
-						<scoped-access-token-category category="admin"></scoped-access-token-category>
-					{{end}}
-					<scoped-access-token-category category="issue"></scoped-access-token-category>
-					<scoped-access-token-category category="misc"></scoped-access-token-category>
-					<scoped-access-token-category category="notification"></scoped-access-token-category>
-					<scoped-access-token-category category="organization"></scoped-access-token-category>
-					<scoped-access-token-category category="package"></scoped-access-token-category>
-					<scoped-access-token-category category="repository"></scoped-access-token-category>
-					<scoped-access-token-category category="user"></scoped-access-token-category>
 				</details>
 				<div id="scoped-access-warning" class="ui warning message center gt-db gt-hidden">
 					{{.locale.Tr "settings.at_least_one_permission"}}
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 689c3f4a29..c9cc1d8032 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -1,6 +1,7 @@
 @import "./modules/normalize.css";
 @import "./modules/animations.css";
 @import "./modules/button.css";
+@import "./modules/select.css";
 @import "./modules/tippy.css";
 @import "./modules/modal.css";
 @import "./modules/breadcrumb.css";
diff --git a/web_src/css/modules/select.css b/web_src/css/modules/select.css
new file mode 100644
index 0000000000..57a87888e0
--- /dev/null
+++ b/web_src/css/modules/select.css
@@ -0,0 +1,25 @@
+.gitea-select {
+  position: relative;
+}
+
+.gitea-select select {
+  appearance: none; /* hide default triangle */
+}
+
+/* ::before and ::after pseudo elements don't work on select elements,
+   so we need to put it on the parent. */
+.gitea-select::after {
+  position: absolute;
+  top: 12px;
+  right: 8px;
+  pointer-events: none;
+  content: '';
+  width: 14px;
+  height: 14px;
+  mask-size: cover;
+  -webkit-mask-size: cover;
+  mask-image: var(--octicon-chevron-right);
+  -webkit-mask-image: var(--octicon-chevron-right);
+  transform: rotate(90deg); /* point the chevron down */
+  background: currentcolor;
+}
diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue
index 769f3262b4..b4b9b979ea 100644
--- a/web_src/js/components/ScopedAccessTokenSelector.vue
+++ b/web_src/js/components/ScopedAccessTokenSelector.vue
@@ -1,97 +1,100 @@
 <template>
-  <div class="scoped-access-token-category">
-    <div class="field gt-pl-2">
-      <label class="checkbox-label">
-        <input
-          ref="category"
-          v-model="categorySelected"
-          class="scope-checkbox scoped-access-token-input"
-          type="checkbox"
-          name="scope"
-          :value="'write:' + category"
-          @input="onCategoryInput"
-        >
-        {{ category }}
-      </label>
-    </div>
-    <div class="field gt-pl-4">
-      <div class="inline field">
-        <label class="checkbox-label">
-          <input
-            ref="read"
-            v-model="readSelected"
-            :disabled="disableIndividual || writeSelected"
-            class="scope-checkbox scoped-access-token-input"
-            type="checkbox"
-            name="scope"
-            :value="'read:' + category"
-            @input="onIndividualInput"
-          >
-          read:{{ category }}
-        </label>
-      </div>
-      <div class="inline field">
-        <label class="checkbox-label">
-          <input
-            ref="write"
-            v-model="writeSelected"
-            :disabled="disableIndividual"
-            class="scope-checkbox scoped-access-token-input"
-            type="checkbox"
-            name="scope"
-            :value="'write:' + category"
-            @input="onIndividualInput"
-          >
-          write:{{ category }}
-        </label>
-      </div>
+  <div v-for="category in categories" :key="category" class="field gt-pl-2 gt-pb-2 access-token-category">
+    <label class="category-label" :for="'access-token-scope-' + category">
+      {{ category }}
+    </label>
+    <div class="gitea-select">
+      <select
+        class="ui selection access-token-select"
+        name="scope"
+        :id="'access-token-scope-' + category"
+      >
+        <option value="">
+          {{ noAccessLabel }}
+        </option>
+        <option :value="'read:' + category">
+          {{ readLabel }}
+        </option>
+        <option :value="'write:' + category">
+          {{ writeLabel }}
+        </option>
+      </select>
     </div>
   </div>
 </template>
 
 <script>
 import {createApp} from 'vue';
-import {showElem} from '../utils/dom.js';
+import {hideElem, showElem} from '../utils/dom.js';
 
 const sfc = {
   props: {
-    category: {
+    isAdmin: {
+      type: Boolean,
+      required: true,
+    },
+    noAccessLabel: {
+      type: String,
+      required: true,
+    },
+    readLabel: {
+      type: String,
+      required: true,
+    },
+    writeLabel: {
       type: String,
       required: true,
     },
   },
 
-  data: () => ({
-    categorySelected: false,
-    disableIndividual: false,
-    readSelected: false,
-    writeSelected: false,
-  }),
+  computed: {
+    categories() {
+      const categories = [
+        'activitypub',
+      ];
+      if (this.isAdmin) {
+        categories.push('admin');
+      }
+      categories.push(
+        'issue',
+        'misc',
+        'notification',
+        'organization',
+        'package',
+        'repository',
+        'user');
+      return categories;
+    }
+  },
+
+  mounted() {
+    document.getElementById('scoped-access-submit').addEventListener('click', this.onClickSubmit);
+  },
+
+  unmounted() {
+    document.getElementById('scoped-access-submit').removeEventListener('click', this.onClickSubmit);
+  },
 
   methods: {
-    /**
-     * When entire category is toggled
-     * @param {Event} e
-     */
-    onCategoryInput(e) {
+    onClickSubmit(e) {
       e.preventDefault();
-      this.disableIndividual = this.$refs.category.checked;
-      this.writeSelected = this.$refs.category.checked;
-      this.readSelected = this.$refs.category.checked;
-    },
 
-    /**
-     * When an individual level of category is toggled
-     * @param {Event} e
-     */
-    onIndividualInput(e) {
-      e.preventDefault();
-      if (this.$refs.write.checked) {
-        this.readSelected = true;
+      const warningEl = document.getElementById('scoped-access-warning');
+      // check that at least one scope has been selected
+      for (const el of document.getElementsByClassName('access-token-select')) {
+        if (el.value) {
+          // Hide the error if it was visible from previous attempt.
+          hideElem(warningEl);
+          // Submit the form.
+          document.getElementById('scoped-access-form').submit();
+          // Don't show the warning.
+          return;
+        }
       }
-      this.categorySelected = this.$refs.write.checked;
-    },
-  }
+      // no scopes selected, show validation error
+      showElem(warningEl);
+    }
+  },
 };
 
 export default sfc;
@@ -100,39 +103,11 @@ export default sfc;
  * Initialize category toggle sections
  */
 export function initScopedAccessTokenCategories() {
-  for (const el of document.getElementsByTagName('scoped-access-token-category')) {
-    const category = el.getAttribute('category');
-    createApp(sfc, {
-      category,
-    }).mount(el);
+  for (const el of document.getElementsByClassName('scoped-access-token-mount')) {
+    createApp({})
+      .component('scoped-access-token-selector', sfc)
+      .mount(el);
   }
-
-  document.getElementById('scoped-access-submit')?.addEventListener('click', (e) => {
-    e.preventDefault();
-    // check that at least one scope has been selected
-    for (const el of document.getElementsByClassName('scoped-access-token-input')) {
-      if (el.checked) {
-        document.getElementById('scoped-access-form').submit();
-      }
-    }
-    // no scopes selected, show validation error
-    showElem(document.getElementById('scoped-access-warning'));
-  });
 }
 
 </script>
-
-<style scoped>
-.scoped-access-token-category {
-  padding-top: 10px;
-  padding-bottom: 10px;
-}
-
-.checkbox-label {
-  cursor: pointer;
-}
-
-.scope-checkbox {
-  margin: 4px 5px 0 0;
-}
-</style>