diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 7737e6044eb97c2008513606ac1df58a51646cf2..001749468acc8e1bbc4f191b4bbd9bad77eb11a5 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -509,6 +509,7 @@ deleteAll: "全て削除"
 showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
 newNoteRecived: "新しいノートがあります"
 sounds: "サウンド"
+sound: "サウンド"
 listen: "聴く"
 none: "なし"
 showInPage: "ページで表示"
diff --git a/packages/client/src/pages/settings/sounds.sound.vue b/packages/client/src/pages/settings/sounds.sound.vue
new file mode 100644
index 0000000000000000000000000000000000000000..62627c6333d523d7be988455a907f9aae70f9238
--- /dev/null
+++ b/packages/client/src/pages/settings/sounds.sound.vue
@@ -0,0 +1,45 @@
+<template>
+<div class="_formRoot">
+	<FormSelect v-model="type">
+		<template #label>{{ i18n.ts.sound }}</template>
+		<option v-for="x in soundsTypes" :key="x" :value="x">{{ x == null ? i18n.ts.none : x }}</option>
+	</FormSelect>
+	<FormRange v-model="volume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`" class="_formBlock">
+		<template #label>{{ i18n.ts.volume }}</template>
+	</FormRange>
+
+	<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+		<FormButton inline @click="listen"><i class="ti ti-player-play"></i> {{ i18n.ts.listen }}</FormButton>
+		<FormButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</FormButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import FormSelect from '@/components/form/select.vue';
+import FormButton from '@/components/MkButton.vue';
+import FormRange from '@/components/form/range.vue';
+import { i18n } from '@/i18n';
+import { playFile, soundsTypes } from '@/scripts/sound';
+
+const props = defineProps<{
+	type: string;
+	volume: number;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'update', result: { type: string; volume: number; }): void;
+}>();
+
+let type = $ref(props.type);
+let volume = $ref(props.volume);
+
+function listen() {
+	playFile(type, volume);
+}
+
+function save() {
+	emit('update', { type, volume });
+}
+</script>
diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue
index 2bea491e6741059fe41e0a49a775dbe8d4761fc3..ef60b2c3c9fb66f79da631b91ed3f6f6df0c1037 100644
--- a/packages/client/src/pages/settings/sounds.vue
+++ b/packages/client/src/pages/settings/sounds.vue
@@ -6,11 +6,12 @@
 
 	<FormSection>
 		<template #label>{{ i18n.ts.sounds }}</template>
-		<FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)">
-			{{ $t('_sfx.' + type) }}
-			<template #suffix>{{ sounds[type].type || i18n.ts.none }}</template>
-			<template #suffixIcon><i class="ti ti-chevron-down"></i></template>
-		</FormLink>
+		<FormFolder v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;">
+			<template #label>{{ $t('_sfx.' + type) }}</template>
+			<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
+
+			<XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/>
+		</FormFolder>
 	</FormSection>
 
 	<FormButton danger class="_formBlock" @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</FormButton>
@@ -19,10 +20,12 @@
 
 <script lang="ts" setup>
 import { computed, ref } from 'vue';
+import XSound from './sounds.sound.vue';
 import FormRange from '@/components/form/range.vue';
 import FormButton from '@/components/MkButton.vue';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
+import FormFolder from '@/components/form/folder.vue';
 import * as os from '@/os';
 import { ColdDeviceStorage } from '@/store';
 import { playFile } from '@/scripts/sound';
@@ -50,71 +53,10 @@ const sounds = ref({
 	channel: ColdDeviceStorage.get('sound_channel'),
 });
 
-const soundsTypes = [
-	null,
-	'syuilo/up',
-	'syuilo/down',
-	'syuilo/pope1',
-	'syuilo/pope2',
-	'syuilo/waon',
-	'syuilo/popo',
-	'syuilo/triple',
-	'syuilo/poi1',
-	'syuilo/poi2',
-	'syuilo/pirori',
-	'syuilo/pirori-wet',
-	'syuilo/pirori-square-wet',
-	'syuilo/square-pico',
-	'syuilo/reverved',
-	'syuilo/ryukyu',
-	'syuilo/kick',
-	'syuilo/snare',
-	'syuilo/queue-jammed',
-	'aisha/1',
-	'aisha/2',
-	'aisha/3',
-	'noizenecio/kick_gaba1',
-	'noizenecio/kick_gaba2',
-	'noizenecio/kick_gaba3',
-	'noizenecio/kick_gaba4',
-	'noizenecio/kick_gaba5',
-	'noizenecio/kick_gaba6',
-	'noizenecio/kick_gaba7',
-];
-
-async function edit(type) {
-	const { canceled, result } = await os.form(i18n.t('_sfx.' + type), {
-		type: {
-			type: 'enum',
-			enum: soundsTypes.map(x => ({
-				value: x,
-				label: x == null ? i18n.ts.none : x,
-			})),
-			label: i18n.ts.sound,
-			default: sounds.value[type].type,
-		},
-		volume: {
-			type: 'range',
-			min: 0,
-			max: 1,
-			step: 0.05,
-			textConverter: (v) => `${Math.floor(v * 100)}%`,
-			label: i18n.ts.volume,
-			default: sounds.value[type].volume,
-		},
-		listen: {
-			type: 'button',
-			content: i18n.ts.listen,
-			action: (_, values) => {
-				playFile(values.type, values.volume);
-			},
-		},
-	});
-	if (canceled) return;
-
+async function updated(type, sound) {
 	const v = {
-		type: result.type,
-		volume: result.volume,
+		type: sound.type,
+		volume: sound.volume,
 	};
 
 	ColdDeviceStorage.set('sound_' + type, v);
diff --git a/packages/client/src/scripts/sound.ts b/packages/client/src/scripts/sound.ts
index 2b8279b3df37c216b509e90c792430f82e35ae38..9d1f6032356122344271e44ad9edf6d409d2f38a 100644
--- a/packages/client/src/scripts/sound.ts
+++ b/packages/client/src/scripts/sound.ts
@@ -2,6 +2,38 @@ import { ColdDeviceStorage } from '@/store';
 
 const cache = new Map<string, HTMLAudioElement>();
 
+export const soundsTypes = [
+	null,
+	'syuilo/up',
+	'syuilo/down',
+	'syuilo/pope1',
+	'syuilo/pope2',
+	'syuilo/waon',
+	'syuilo/popo',
+	'syuilo/triple',
+	'syuilo/poi1',
+	'syuilo/poi2',
+	'syuilo/pirori',
+	'syuilo/pirori-wet',
+	'syuilo/pirori-square-wet',
+	'syuilo/square-pico',
+	'syuilo/reverved',
+	'syuilo/ryukyu',
+	'syuilo/kick',
+	'syuilo/snare',
+	'syuilo/queue-jammed',
+	'aisha/1',
+	'aisha/2',
+	'aisha/3',
+	'noizenecio/kick_gaba1',
+	'noizenecio/kick_gaba2',
+	'noizenecio/kick_gaba3',
+	'noizenecio/kick_gaba4',
+	'noizenecio/kick_gaba5',
+	'noizenecio/kick_gaba6',
+	'noizenecio/kick_gaba7',
+] as const;
+
 export function getAudio(file: string, useCache = true): HTMLAudioElement {
 	let audio: HTMLAudioElement;
 	if (useCache && cache.has(file)) {