diff --git a/CHANGELOG.md b/CHANGELOG.md
index c089aae27927508904b6ab7b139c78efe056caf1..c39a447c594308d43ff77f2f006acbcc86afcaf4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,7 @@
 - Enhance: MFMの属性でオートコンプリートが使用できるように #12735
 - Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
 - Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
+- Enhance: コードのシンタックスハイライトにテーマを適用できるように
 - Fix: ネイティブモードの絵文字がモノクロにならないように
 - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index c655ff416ef3d74a58fce17262c84a1a5c14d8c6..68c50c4c690cad17b2840e5eb0805b10634aa577 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <!-- eslint-disable vue/no-v-html -->
 <template>
-<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
+<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
 </template>
 
 <script lang="ts" setup>
 import { ref, computed, watch } from 'vue';
 import { bundledLanguagesInfo } from 'shiki';
 import type { BuiltinLanguage } from 'shiki';
-import { getHighlighter } from '@/scripts/code-highlighter.js';
+import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
+import { defaultStore } from '@/store.js';
 
 const props = defineProps<{
 	code: string;
@@ -21,11 +22,23 @@ const props = defineProps<{
 }>();
 
 const highlighter = await getHighlighter();
-
+const darkMode = defaultStore.reactiveState.darkMode;
 const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
+
+const [lightThemeName, darkThemeName] = await Promise.all([
+	getTheme('light', true),
+	getTheme('dark', true),
+]);
+
 const html = computed(() => highlighter.codeToHtml(props.code, {
 	lang: codeLang.value,
-	theme: 'dark-plus',
+	themes: {
+		fallback: 'dark-plus',
+		light: lightThemeName,
+		dark: darkThemeName,
+	},
+	defaultColor: false,
+	cssVariablePrefix: '--shiki-',
 }));
 
 async function fetchLanguage(to: string): Promise<void> {
@@ -64,6 +77,15 @@ watch(() => props.lang, (to) => {
 	margin: .5em 0;
 	overflow: auto;
 	border-radius: 8px;
+	border: 1px solid var(--divider);
+
+	color: var(--shiki-fallback);
+	background-color: var(--shiki-fallback-bg);
+
+	& span {
+		color: var(--shiki-fallback);
+		background-color: var(--shiki-fallback-bg);
+	}
 
 	& pre,
 	& code {
@@ -71,6 +93,26 @@ watch(() => props.lang, (to) => {
 	}
 }
 
+.light.codeBlockRoot :global(.shiki) {
+	color: var(--shiki-light);
+	background-color: var(--shiki-light-bg);
+
+	& span {
+		color: var(--shiki-light);
+		background-color: var(--shiki-light-bg);
+	}
+}
+
+.dark.codeBlockRoot :global(.shiki) {
+	color: var(--shiki-dark);
+	background-color: var(--shiki-dark-bg);
+
+	& span {
+		color: var(--shiki-dark);
+		background-color: var(--shiki-dark-bg);
+	}
+}
+
 .codeBlockRoot.codeEditor {
 	min-width: 100%;
 	height: 100%;
@@ -79,6 +121,7 @@ watch(() => props.lang, (to) => {
 		padding: 12px;
 		margin: 0;
 		border-radius: 6px;
+		border: none;
 		min-height: 130px;
 		pointer-events: none;
 		min-width: calc(100% - 24px);
@@ -90,6 +133,11 @@ watch(() => props.lang, (to) => {
 		text-rendering: inherit;
     text-transform: inherit;
     white-space: pre;
+
+		& span {
+			display: inline-block;
+			min-height: 1em;
+		}
 	}
 }
 </style>
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index 251e6ade00b9763fd6747ea1de48a9849fa72249..6c14738937f2efb0ef54269c4e69ae6a56f73bd2 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -53,7 +53,6 @@ function copy() {
 }
 
 .codeBlockCopyButton {
-	color: #D4D4D4;
 	position: absolute;
 	top: 8px;
 	right: 8px;
@@ -67,8 +66,7 @@ function copy() {
 .codeBlockFallbackRoot {
 	display: block;
 	overflow-wrap: anywhere;
-	color: #D4D4D4;
-	background: #1E1E1E;
+	background: var(--bg);
 	padding: 1em;
 	margin: .5em 0;
 	overflow: auto;
@@ -93,8 +91,8 @@ function copy() {
 	border-radius: 8px;
 	padding: 24px;
 	margin-top: 4px;
-	color: #D4D4D4;
-	background: #1E1E1E;
+	color: var(--fg);
+	background: var(--bg);
 }
 
 .codePlaceholderContainer {
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index c8c3deb610a1185ebb70b5e76094bcf2f1497e71..3cf8234e72da55ecda45cfb4e2e044de1290e07f 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -196,10 +196,11 @@ watch(v, newValue => {
 	resize: none;
 	text-align: left;
 	color: transparent;
-	caret-color: rgb(225, 228, 232);
+	caret-color: var(--fg);
 	background-color: transparent;
 	border: 0;
 	border-radius: 6px;
+	box-sizing: border-box;
 	outline: 0;
 	min-width: calc(100% - 24px);
 	height: 100%;
@@ -210,6 +211,6 @@ watch(v, newValue => {
 }
 
 .textarea::selection {
-	color: #fff;
+	color: var(--bg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue
index 5340c1fd5fa30a713fa54e70dbfb096cae73276e..6a9d97ab5a3ee0eb0abe74ef30da48b5d6aaf554 100644
--- a/packages/frontend/src/components/MkCodeInline.vue
+++ b/packages/frontend/src/components/MkCodeInline.vue
@@ -18,8 +18,7 @@ const props = defineProps<{
 	display: inline-block;
 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 	overflow-wrap: anywhere;
-	color: #D4D4D4;
-	background: #1E1E1E;
+	background: var(--bg);
 	padding: .1em;
 	border-radius: .3em;
 }
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 8dd2b9129d6449a23bba66526c7481938b0695a1..3e8ff9938790dd393e6e2c1b8037b1cf798202bf 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<div :class="$style.caption"><slot name="caption"></slot></div>
 
-	<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 </div>
 </template>
 
@@ -138,6 +138,7 @@ function show() {
 			active: computed(() => v.value === option.props?.value),
 			action: () => {
 				v.value = option.props?.value;
+				changed.value = true;
 				emit('changeByUser', v.value);
 			},
 		});
@@ -288,6 +289,10 @@ function show() {
 	padding-left: 6px;
 }
 
+.save {
+	margin: 8px 0 0 0;
+}
+
 .chevron {
 	transition: transform 0.1s ease-out;
 }
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index dedac10270a7e8cba42f2744158d779c8847a220..1d6fec52900810d926b91e11ed13984d7f63460c 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js';
 import { fetchThemes, getThemes } from '@/theme-store.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
+import { unisonReload } from '@/scripts/unison-reload.js';
+import * as os from '@/os.js';
+
+async function reloadAsk() {
+	const { canceled } = await os.confirm({
+		type: 'info',
+		text: i18n.ts.reloadToApplySetting,
+	});
+	if (canceled) return;
+
+	unisonReload();
+}
 
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
@@ -124,6 +136,7 @@ const lightThemeId = computed({
 		}
 	},
 });
+
 const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
 const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
 const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
@@ -141,7 +154,7 @@ watch(wallpaper, () => {
 	} else {
 		miLocalStorage.setItem('wallpaper', wallpaper.value);
 	}
-	location.reload();
+	reloadAsk();
 });
 
 onActivated(() => {
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index 68c36ca1b4b6d63f036c4a762042b56f6014e91c..043b6efd73a54c0cd474532557501e597758347b 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
 import { defaultStore } from '@/store.js';
 import { useStream } from '@/stream.js';
 import { deepClone } from '@/scripts/clone.js';
+import { deepMerge } from '@/scripts/merge.js';
 
 type StateDef = Record<string, {
 	where: 'account' | 'device' | 'deviceAccount';
@@ -84,29 +85,9 @@ export class Storage<T extends StateDef> {
 		return typeof value === 'object' && value !== null && !Array.isArray(value);
 	}
 
-	/**
-	 * valueにないキーをdefからもらう(再帰的)\
-	 * nullはそのまま、undefinedはdefの値
-	 **/
-	private mergeObject<X>(value: X, def: X): X {
-		if (this.isPureObject(value) && this.isPureObject(def)) {
-			const result = structuredClone(value) as X;
-			for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
-				if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
-					result[k] = v;
-				} else if (this.isPureObject(v) && this.isPureObject(result[k])) {
-					const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
-					result[k] = this.mergeObject<typeof v>(child, v);
-				}
-			}
-			return result;
-		}
-		return value;
-	}
-
 	private mergeState<X>(value: X, def: X): X {
 		if (this.isPureObject(value) && this.isPureObject(def)) {
-			const merged = this.mergeObject(value, def);
+			const merged = deepMerge(value, def);
 
 			if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
 
diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts
index 5adb3f606f4356070c67a8b031e08c2f4391bfff..c6a520e9132bb76aa1d0e0b8edccc788c942466c 100644
--- a/packages/frontend/src/router/main.ts
+++ b/packages/frontend/src/router/main.ts
@@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter {
 		return this.supplier().resolve(path);
 	}
 
+	init(): void {
+		this.supplier().init();
+	}
+
 	eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
 		return this.supplier().eventNames();
 	}
diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts
index ac38faefaa272d0d14d146a21ef62d7d26a1ed74..6d3a1c8c79765b587433d79b31f5fee6e95e63e5 100644
--- a/packages/frontend/src/scripts/clone.ts
+++ b/packages/frontend/src/scripts/clone.ts
@@ -8,13 +8,13 @@
 // あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
 // https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
 
-type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
+export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
 
 export function deepClone<T extends Cloneable>(x: T): T {
 	if (typeof x === 'object') {
 		if (x === null) return x;
 		if (Array.isArray(x)) return x.map(deepClone) as T;
-		const obj = {} as Record<string, Cloneable>;
+		const obj = {} as Record<string | number | symbol, Cloneable>;
 		for (const [k, v] of Object.entries(x)) {
 			obj[k] = v === undefined ? undefined : deepClone(v);
 		}
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index bc05ec94d54cc83c346aacdcc6e066f6dc9b8fcf..b11dfed41a60b945edb09322706748fd3e26fe38 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -1,9 +1,51 @@
+import { bundledThemesInfo } from 'shiki';
 import { getHighlighterCore, loadWasm } from 'shiki/core';
 import darkPlus from 'shiki/themes/dark-plus.mjs';
-import type { Highlighter, LanguageRegistration } from 'shiki';
+import { unique } from './array.js';
+import { deepClone } from './clone.js';
+import { deepMerge } from './merge.js';
+import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki';
+import { ColdDeviceStorage } from '@/store.js';
+import lightTheme from '@/themes/_light.json5';
+import darkTheme from '@/themes/_dark.json5';
 
 let _highlighter: Highlighter | null = null;
 
+export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
+export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
+export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
+	const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme'));
+
+	if (theme.base) {
+		const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
+		if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
+	}
+	
+	if (theme.codeHighlighter) {
+		let _res: ThemeRegistration = {};
+		if (theme.codeHighlighter.base === '_none_') {
+			_res = deepClone(theme.codeHighlighter.overrides);
+		} else {
+			const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
+			_res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
+		}
+		if (_res.name == null) {
+			_res.name = theme.id;
+		}
+		_res.type = mode;
+
+		if (getName) {
+			return _res.name;
+		}
+		return _res;
+	}
+
+	if (getName) {
+		return 'dark-plus';
+	}
+	return darkPlus;
+}
+
 export async function getHighlighter(): Promise<Highlighter> {
 	if (!_highlighter) {
 		return await initHighlighter();
@@ -13,11 +55,17 @@ export async function getHighlighter(): Promise<Highlighter> {
 
 export async function initHighlighter() {
 	const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
-
+	
 	await loadWasm(import('shiki/onig.wasm?init'));
 
+	// テーマの重複を消す
+	const themes = unique([
+		darkPlus,
+		...(await Promise.all([getTheme('light'), getTheme('dark')])),
+	]);
+
 	const highlighter = await getHighlighterCore({
-		themes: [darkPlus],
+		themes,
 		langs: [
 			import('shiki/langs/javascript.mjs'),
 			{
@@ -27,6 +75,20 @@ export async function initHighlighter() {
 		],
 	});
 
+	ColdDeviceStorage.watch('lightTheme', async () => {
+		const newTheme = await getTheme('light');
+		if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+			highlighter.loadTheme(newTheme);
+		}
+	});
+
+	ColdDeviceStorage.watch('darkTheme', async () => {
+		const newTheme = await getTheme('dark');
+		if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+			highlighter.loadTheme(newTheme);
+		}
+	});
+
 	_highlighter = highlighter;
 
 	return highlighter;
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts
new file mode 100644
index 0000000000000000000000000000000000000000..60097051fab66e003c00e86e90c5164eeea75575
--- /dev/null
+++ b/packages/frontend/src/scripts/merge.ts
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { deepClone } from './clone.js';
+import type { Cloneable } from './clone.js';
+
+function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
+	return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+/**
+ * valueにないキーをdefからもらう(再帰的)\
+ * nullはそのまま、undefinedはdefの値
+ **/
+export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: X, def: X): X {
+	if (isPureObject(value) && isPureObject(def)) {
+		const result = deepClone(value as Cloneable) as X;
+		for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
+			if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
+				result[k] = v;
+			} else if (isPureObject(v) && isPureObject(result[k])) {
+				const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record<string | number | symbol, unknown>;
+				result[k] = deepMerge<typeof v>(child, v);
+			}
+		}
+		return result;
+	}
+	return value;
+}
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index 21ef85fe7ac906310537a079af05edafe8536b67..d3bd9ba4bc30af419d835bf711b7e06697c23801 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -6,6 +6,7 @@
 import { ref } from 'vue';
 import tinycolor from 'tinycolor2';
 import { deepClone } from './clone.js';
+import type { BuiltinTheme } from 'shiki';
 import { globalEvents } from '@/events.js';
 import lightTheme from '@/themes/_light.json5';
 import darkTheme from '@/themes/_dark.json5';
@@ -18,6 +19,13 @@ export type Theme = {
 	desc?: string;
 	base?: 'dark' | 'light';
 	props: Record<string, string>;
+	codeHighlighter?: {
+		base: BuiltinTheme;
+		overrides?: Record<string, any>;
+	} | {
+		base: '_none_';
+		overrides: Record<string, any>;
+	};
 };
 
 export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
@@ -53,7 +61,7 @@ export const getBuiltinThemesRef = () => {
 	return builtinThemes;
 };
 
-let timeout = null;
+let timeout: number | null = null;
 
 export function applyTheme(theme: Theme, persist = true) {
 	if (timeout) window.clearTimeout(timeout);
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index afc35bb825db551dc090a927d40e88f39bac66b1..641a506679c4edfaec0c777aefe63c1e32f612c5 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { miLocalStorage } from './local-storage.js';
 import type { SoundType } from '@/scripts/sound.js';
+import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki';
 import { Storage } from '@/pizzax.js';
 import { hemisphere } from '@/scripts/intl-const.js';
 
diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5
index 3f5822977abcb6d978b37de025cdab8c00449c68..c82a95686821a8128be43a3490566962681abc3b 100644
--- a/packages/frontend/src/themes/_dark.json5
+++ b/packages/frontend/src/themes/_dark.json5
@@ -94,4 +94,8 @@
 		X16: ':alpha<0.7<@panel',
 		X17: ':alpha<0.8<@bg',
 	},
+
+	codeHighlighter: {
+		base: 'one-dark-pro',
+	},
 }
diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5
index 6ebfcaafeb5319b3b557ff02e83af2c3ebaad587..63bc030916d81ff096f1a1f6a3d06d2819bc2b8e 100644
--- a/packages/frontend/src/themes/_light.json5
+++ b/packages/frontend/src/themes/_light.json5
@@ -94,4 +94,8 @@
 		X16: ':alpha<0.7<@panel',
 		X17: ':alpha<0.8<@bg',
 	},
+
+	codeHighlighter: {
+		base: 'catppuccin-latte',
+	},
 }