diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52c8bcd42..10ddc33d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,8 @@
 - OAuth 2.0のサポート
 
 ### Client
+- メニューのスイッチの動作を改善
+- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように
 - Enhance: 自分が押したリアクションのデザインを改善
 - Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
 - Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index f3c7c235a..3d4e45b1f 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -35,9 +35,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
-			</span>
+			<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+				<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
+				<span :class="$style.switchText">{{ item.text }}</span>
+			</button>
 			<button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<span>{{ item.text }}</span>
@@ -63,8 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
 import { focusPrev, focusNext } from '@/scripts/focus';
-import MkSwitch from '@/components/MkSwitch.vue';
-import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
+import MkSwitchButton from '@/components/MkSwitch.button.vue';
+import { MenuItem, InnerMenuItem, OuterMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 
@@ -145,17 +146,17 @@ function onItemMouseLeave(item) {
 	if (childCloseTimer) window.clearTimeout(childCloseTimer);
 }
 
-let childrenCache = new WeakMap();
-async function showChildren(item: MenuItem, ev: MouseEvent) {
-	const children = ref([]);
+let childrenCache = new WeakMap<MenuParent, OuterMenuItem[]>();
+async function showChildren(item: MenuParent, ev: MouseEvent) {
+	const children = ref<OuterMenuItem[]>([]);
 	if (childrenCache.has(item)) {
-		children.value = childrenCache.get(item);
+		children.value = childrenCache.get(item)!;
 	} else {
 		if (typeof item.children === 'function') {
 			children.value = [{
 				type: 'pending',
 			}];
-			item.children().then(x => {
+			Promise.resolve(item.children()).then(x => {
 				children.value = x;
 				childrenCache.set(item, x);
 			});
@@ -191,6 +192,11 @@ function focusDown() {
 	focusNext(document.activeElement);
 }
 
+function switchItem(item: MenuSwitch & { ref: any }) {
+	if (item.disabled) return;
+	item.ref = !item.ref;
+}
+
 onMounted(() => {
 	if (props.viaKeyboard) {
 		nextTick(() => {
@@ -357,6 +363,37 @@ onBeforeUnmount(() => {
 	}
 }
 
+.switch {
+	position: relative;
+	display: flex;
+	transition: all 0.2s ease;
+	user-select: none;
+	cursor: pointer;
+}
+
+.switchDisabled {
+	cursor: not-allowed;
+}
+
+.switchButton {
+	margin-left: -2px;
+}
+
+.switchText {
+	margin-left: 8px;
+	margin-top: 2px;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.switchInput {
+	position: absolute;
+	width: 0;
+	height: 0;
+	opacity: 0;
+	margin: 0;
+}
+
 .icon {
 	margin-right: 8px;
 }
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 962a5eef5..6cd1a4c4b 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -408,14 +408,16 @@ function onContextmenu(ev: MouseEvent): void {
 		ev.preventDefault();
 		react();
 	} else {
-		os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
+		const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+		os.contextMenu(menu, ev).then(focus).finally(cleanup);
 	}
 }
 
 function menu(viaKeyboard = false): void {
-	os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
+	const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+	os.popupMenu(menu, menuButton.value, {
 		viaKeyboard,
-	}).then(focus);
+	}).then(focus).finally(cleanup);
 }
 
 async function clip() {
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index c34ba4f83..8acd4e470 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -385,14 +385,16 @@ function onContextmenu(ev: MouseEvent): void {
 		ev.preventDefault();
 		react();
 	} else {
-		os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
+		const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+		os.contextMenu(menu, ev).then(focus).finally(cleanup);
 	}
 }
 
 function menu(viaKeyboard = false): void {
-	os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
+	const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+	os.popupMenu(menu, menuButton.value, {
 		viaKeyboard,
-	}).then(focus);
+	}).then(focus).finally(cleanup);
 }
 
 async function clip() {
diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue
new file mode 100644
index 000000000..1d420f86c
--- /dev/null
+++ b/packages/frontend/src/components/MkSwitch.button.vue
@@ -0,0 +1,88 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<span
+	v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff"
+	:class="{
+		[$style.button]: true,
+		[$style.buttonChecked]: checked,
+		[$style.buttonDisabled]: props.disabled
+	}"
+	data-cy-switch-toggle
+	@click.prevent.stop="toggle"
+>
+	<div :class="{ [$style.knob]: true, [$style.knobChecked]: checked }"></div>
+</span>
+</template>
+
+<script lang="ts" setup>
+import { toRefs, Ref } from 'vue';
+import { i18n } from '@/i18n';
+
+const props = withDefaults(defineProps<{
+	checked: boolean | Ref<boolean>;
+	disabled?: boolean;
+}>(), {
+	disabled: false,
+});
+
+const emit = defineEmits<{
+	(ev: 'toggle'): void;
+}>();
+
+const checked = toRefs(props).checked;
+const toggle = () => {
+	emit('toggle');
+};
+</script>
+
+<style lang="scss" module>
+.button {
+	position: relative;
+	display: inline-flex;
+	flex-shrink: 0;
+	margin: 0;
+	box-sizing: border-box;
+	width: 32px;
+	height: 23px;
+	outline: none;
+	background: var(--switchOffBg);
+	background-clip: content-box;
+	border: solid 1px var(--switchOffBg);
+	border-radius: 999px;
+	cursor: pointer;
+	transition: inherit;
+	user-select: none;
+}
+
+.buttonChecked {
+	background-color: var(--switchOnBg) !important;
+	border-color: var(--switchOnBg) !important;
+}
+
+.buttonDisabled {
+	cursor: not-allowed;
+}
+
+.knob {
+	position: absolute;
+	top: 3px;
+	width: 15px;
+	height: 15px;
+	border-radius: 999px;
+	transition: all 0.2s ease;
+
+	&:not(.knobChecked) {
+		left: 3px;
+		background: var(--switchOffFg);
+	}
+}
+
+.knobChecked {
+	left: 12px;
+	background: var(--switchOnFg);
+}
+</style>
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index 9304f1717..96e2bad49 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -12,9 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:class="$style.input"
 		@keydown.enter="toggle"
 	>
-	<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
-		<div :class="$style.knob"></div>
-	</span>
+	<XButton :checked="checked" :disabled="disabled" @toggle="toggle" />
 	<span :class="$style.body">
 		<!-- TODO: 無名slotの方は廃止 -->
 		<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
@@ -25,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { toRefs, Ref } from 'vue';
-import { i18n } from '@/i18n';
+import XButton from '@/components/MkSwitch.button.vue';
 
 const props = defineProps<{
 	modelValue: boolean | Ref<boolean>;
@@ -36,7 +34,6 @@ const emit = defineEmits<{
 	(ev: 'update:modelValue', v: boolean): void;
 }>();
 
-let button = $shallowRef<HTMLElement>();
 const checked = toRefs(props).modelValue;
 const toggle = () => {
 	if (props.disabled) return;
@@ -66,17 +63,8 @@ const toggle = () => {
 		cursor: not-allowed;
 	}
 
-	&.checked {
-		> .button {
-			background-color: var(--switchOnBg) !important;
-			border-color: var(--switchOnBg) !important;
-
-			> .knob {
-				left: 12px;
-				background: var(--switchOnFg);
-			}
-		}
-	}
+	//&.checked {
+	//}
 }
 
 .input {
@@ -86,36 +74,6 @@ const toggle = () => {
 	opacity: 0;
 	margin: 0;
 }
-
-.button {
-	position: relative;
-	display: inline-flex;
-	flex-shrink: 0;
-	margin: 0;
-	box-sizing: border-box;
-	width: 32px;
-	height: 23px;
-	outline: none;
-	background: var(--switchOffBg);
-	background-clip: content-box;
-	border: solid 1px var(--switchOffBg);
-	border-radius: 999px;
-	cursor: pointer;
-	transition: inherit;
-	user-select: none;
-}
-
-.knob {
-	position: absolute;
-	top: 3px;
-	left: 3px;
-	width: 15px;
-	height: 15px;
-	background: var(--switchOffFg);
-	border-radius: 999px;
-	transition: all 0.2s ease;
-}
-
 .body {
 	margin-left: 12px;
 	margin-top: 2px;
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 50f11f07b..be32af4cf 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -86,7 +86,8 @@ let top = $ref(0);
 let left = $ref(0);
 
 function showMenu(ev: MouseEvent) {
-	os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
+	const { menu, cleanup } = getUserMenu(user);
+	os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
 }
 
 onMounted(() => {
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index b32717c64..069954b93 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -214,7 +214,8 @@ const age = $computed(() => {
 });
 
 function menu(ev) {
-	os.popupMenu(getUserMenu(props.user, router), ev.currentTarget ?? ev.target);
+	const { menu, cleanup } = getUserMenu(props.user, router);
+	os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
 }
 
 function parallaxLoop() {
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index b63c79750..20cea45ee 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -16,6 +16,7 @@ import { defaultStore, noteActions } from '@/store';
 import { miLocalStorage } from '@/local-storage';
 import { getUserMenu } from '@/scripts/get-user-menu';
 import { clipsCache } from '@/cache';
+import { MenuItem } from '@/types/menu';
 
 export async function getNoteClipMenu(props: {
 	note: misskey.entities.Note;
@@ -108,6 +109,8 @@ export function getNoteMenu(props: {
 
 	const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
 
+	const cleanups = [] as (() => void)[];
+
 	function del(): void {
 		os.confirm({
 			type: 'warning',
@@ -233,7 +236,7 @@ export function getNoteMenu(props: {
 		props.translation.value = res;
 	}
 
-	let menu;
+	let menu: MenuItem[];
 	if ($i) {
 		const statePromise = os.api('notes/state', {
 			noteId: appearNote.id,
@@ -295,7 +298,7 @@ export function getNoteMenu(props: {
 				action: () => toggleFavorite(true),
 			}),
 			{
-				type: 'parent',
+				type: 'parent' as const,
 				icon: 'ti ti-paperclip',
 				text: i18n.ts.clip,
 				children: () => getNoteClipMenu(props),
@@ -318,15 +321,17 @@ export function getNoteMenu(props: {
 				text: i18n.ts.pin,
 				action: () => togglePin(true),
 			} : undefined,
-			appearNote.userId !== $i.id ? {
-				type: 'parent',
+			{
+				type: 'parent' as const,
 				icon: 'ti ti-user',
 				text: i18n.ts.user,
 				children: async () => {
-					const user = await os.api('users/show', { userId: appearNote.userId });
-					return getUserMenu(user);
+					const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId });
+					const { menu, cleanup } = getUserMenu(user);
+					cleanups.push(cleanup);
+					return menu;
 				},
-			} : undefined,
+			},
 			/*
 		...($i.isModerator || $i.isAdmin ? [
 			null,
@@ -411,5 +416,13 @@ export function getNoteMenu(props: {
 		}]);
 	}
 
-	return menu;
+	const cleanup = () => {
+		if (_DEV_) console.log('note menu cleanup', cleanups);
+		cleanups.forEach(cleanup => cleanup());
+	};
+
+	return {
+		menu,
+		cleanup,
+	};
 }
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 7f2111be4..445560b0c 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -4,7 +4,7 @@
  */
 
 import { toUnicode } from 'punycode';
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref, watch } from 'vue';
 import * as misskey from 'misskey-js';
 import { i18n } from '@/i18n';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
@@ -19,6 +19,8 @@ import { antennasCache, rolesCache, userListsCache } from '@/cache';
 export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
 	const meId = $i ? $i.id : null;
 
+	const cleanups = [] as (() => void)[];
+
 	async function toggleMute() {
 		if (user.isMuted) {
 			os.apiWithDialog('mute/delete', {
@@ -168,17 +170,32 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
 		text: i18n.ts.addToList,
 		children: async () => {
 			const lists = await userListsCache.fetch(() => os.api('users/lists/list'));
+			return lists.map(list => {
+				const isListed = ref(list.userIds.includes(user.id));
+				cleanups.push(watch(isListed, () => {
+					if (isListed.value) {
+						os.apiWithDialog('users/lists/push', {
+							listId: list.id,
+							userId: user.id,
+						}).then(() => {
+							list.userIds.push(user.id);
+						});
+					} else {
+						os.apiWithDialog('users/lists/pull', {
+							listId: list.id,
+							userId: user.id,
+						}).then(() => {
+							list.userIds.splice(list.userIds.indexOf(user.id), 1);
+						});
+					}
+				}));
 
-			return lists.map(list => ({
-				text: list.name,
-				action: async () => {
-					await os.apiWithDialog('users/lists/push', {
-						listId: list.id,
-						userId: user.id,
-					});
-					userListsCache.delete();
-				},
-			}));
+				return {
+					type: 'switch',
+					text: list.name,
+					ref: isListed,
+				};
+			});
 		},
 	}, {
 		type: 'parent',
@@ -311,5 +328,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
 		}))]);
 	}
 
-	return menu;
+	const cleanup = () => {
+		if (_DEV_) console.log('user menu cleanup', cleanups);
+		cleanups.forEach(cleanup => cleanup());
+	};
+
+	return {
+		menu,
+		cleanup,
+	};
 }
diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts
index edeabe428..b2ba6290c 100644
--- a/packages/frontend/src/types/menu.ts
+++ b/packages/frontend/src/types/menu.ts
@@ -16,7 +16,7 @@ export type MenuA = { type: 'a', href: string, target?: string, download?: strin
 export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
 export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
 export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
-export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] };
+export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] | (() => Promise<OuterMenuItem[]> | OuterMenuItem[]) };
 
 export type MenuPending = { type: 'pending' };