diff --git a/src/client/app/common/hotkey.ts b/src/client/app/common/hotkey.ts
new file mode 100644
index 000000000..10cbeea54
--- /dev/null
+++ b/src/client/app/common/hotkey.ts
@@ -0,0 +1,79 @@
+import keyCode from './keycode';
+
+const getKeyMap = keymap => Object.keys(keymap).map(input => {
+	const result = {} as any;
+
+	const { keyup, keydown } = keymap[input];
+
+	input.split('+').forEach(keyName => {
+		switch (keyName.toLowerCase()) {
+			case 'ctrl':
+			case 'alt':
+			case 'shift':
+			case 'meta':
+				result[keyName] = true;
+				break;
+			default:
+				result.keyCode = keyCode(keyName);
+		}
+	});
+
+	result.callback = {
+		keydown: keydown || keymap[input],
+		keyup
+	};
+
+	return result;
+});
+
+const ignoreElemens = ['input', 'textarea'];
+
+export default {
+	install(Vue) {
+		Vue.directive('hotkey', {
+			bind(el, binding) {
+				el._hotkey_global = binding.modifiers.global === true;
+
+				el._keymap = getKeyMap(binding.value);
+
+				el.dataset.reservedKeyCodes = el._keymap.map(key => `'${key.keyCode}'`).join(' ');
+
+				el._keyHandler = e => {
+					const reservedKeyCodes = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeyCodes || '' : '';
+					if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
+
+					for (const hotkey of el._keymap) {
+						if (el._hotkey_global && reservedKeyCodes.includes(`'${e.keyCode}'`)) break;
+
+						const callback = hotkey.keyCode === e.keyCode &&
+							!!hotkey.ctrl === e.ctrlKey &&
+							!!hotkey.alt === e.altKey &&
+							!!hotkey.shift === e.shiftKey &&
+							!!hotkey.meta === e.metaKey &&
+							hotkey.callback[e.type];
+
+						if (callback) {
+							e.preventDefault();
+							e.stopPropagation();
+							callback(e);
+						}
+					}
+				};
+
+				if (el._hotkey_global) {
+					document.addEventListener('keydown', el._keyHandler);
+				} else {
+					el.addEventListener('keydown', el._keyHandler);
+				}
+			},
+
+			unbind(el) {
+				if (el._hotkey_global) {
+					document.removeEventListener('keydown', el._keyHandler);
+				} else {
+					el.removeEventListener('keydown', el._keyHandler);
+				}
+			}
+		});
+	}
+};
diff --git a/src/client/app/common/keycode.ts b/src/client/app/common/keycode.ts
new file mode 100644
index 000000000..c5ea6cb48
--- /dev/null
+++ b/src/client/app/common/keycode.ts
@@ -0,0 +1,139 @@
+export default searchInput => {
+	// Keyboard Events
+	if (searchInput && typeof searchInput === 'object') {
+		const hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
+		if (hasKeyCode) {
+			searchInput = hasKeyCode;
+		}
+	}
+
+	// Numbers
+	// if (typeof searchInput === 'number') {
+	//   return names[searchInput]
+	// }
+
+	// Everything else (cast to string)
+	const search = String(searchInput);
+
+	// check codes
+	const foundNamedKeyCodes = codes[search.toLowerCase()];
+	if (foundNamedKeyCodes) {
+		return foundNamedKeyCodes;
+	}
+
+	// check aliases
+	const foundNamedKeyAliases = aliases[search.toLowerCase()];
+	if (foundNamedKeyAliases) {
+		return foundNamedKeyAliases;
+	}
+
+	// weird character?
+	if (search.length === 1) {
+		return search.charCodeAt(0);
+	}
+
+	return undefined;
+};
+
+/**
+ * Get by name
+ *
+ *   exports.code['enter'] // => 13
+ */
+
+export const codes = {
+	'backspace': 8,
+	'tab': 9,
+	'enter': 13,
+	'shift': 16,
+	'ctrl': 17,
+	'alt': 18,
+	'pause/break': 19,
+	'caps lock': 20,
+	'esc': 27,
+	'space': 32,
+	'page up': 33,
+	'page down': 34,
+	'end': 35,
+	'home': 36,
+	'left': 37,
+	'up': 38,
+	'right': 39,
+	'down': 40,
+	// 'add': 43,
+	'insert': 45,
+	'delete': 46,
+	'command': 91,
+	'left command': 91,
+	'right command': 93,
+	'numpad *': 106,
+	// 'numpad +': 107,
+	'numpad +': 43,
+	'numpad add': 43, // as a trick
+	'numpad -': 109,
+	'numpad .': 110,
+	'numpad /': 111,
+	'num lock': 144,
+	'scroll lock': 145,
+	'my computer': 182,
+	'my calculator': 183,
+	';': 186,
+	'=': 187,
+	',': 188,
+	'-': 189,
+	'.': 190,
+	'/': 191,
+	'`': 192,
+	'[': 219,
+	'\\': 220,
+	']': 221,
+	"'": 222
+};
+
+// Helper aliases
+
+export const aliases = {
+	'windows': 91,
+	'⇧': 16,
+	'⌥': 18,
+	'⌃': 17,
+	'⌘': 91,
+	'ctl': 17,
+	'control': 17,
+	'option': 18,
+	'pause': 19,
+	'break': 19,
+	'caps': 20,
+	'return': 13,
+	'escape': 27,
+	'spc': 32,
+	'pgup': 33,
+	'pgdn': 34,
+	'ins': 45,
+	'del': 46,
+	'cmd': 91
+};
+
+/*!
+* Programatically add the following
+*/
+
+// lower case chars
+for (let i = 97; i < 123; i++) {
+	codes[String.fromCharCode(i)] = i - 32;
+}
+
+// numbers
+for (let i = 48; i < 58; i++) {
+	codes[i - 48] = i;
+}
+
+// function keys
+for (let i = 1; i < 13; i++) {
+	codes['f' + i] = i + 111;
+}
+
+// numpad keys
+for (let i = 0; i < 10; i++) {
+	codes['numpad ' + i] = i + 96;
+}
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index a4828c987..58985658c 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-reaction-picker">
+<div class="mk-reaction-picker" v-hotkey.global="keymap">
 	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<div class="popover" :class="{ compact, big }" ref="popover">
 		<p v-if="!compact">{{ title }}</p>
@@ -31,28 +31,51 @@ export default Vue.extend({
 			type: Object,
 			required: true
 		},
+
 		source: {
 			required: true
 		},
+
 		compact: {
 			type: Boolean,
 			required: false,
 			default: false
 		},
+
 		cb: {
 			required: false
 		},
+
 		big: {
 			type: Boolean,
 			required: false,
 			default: false
 		}
 	},
+
 	data() {
 		return {
 			title: placeholder
 		};
 	},
+
+	computed: {
+		keymap(): any {
+			return {
+				'1': () => this.react('like'),
+				'2': () => this.react('love'),
+				'3': () => this.react('laugh'),
+				'4': () => this.react('hmm'),
+				'5': () => this.react('surprise'),
+				'6': () => this.react('congrats'),
+				'7': () => this.react('angry'),
+				'8': () => this.react('confused'),
+				'9': () => this.react('rip'),
+				'0': () => this.react('pudding'),
+			};
+		}
+	},
+
 	mounted() {
 		this.$nextTick(() => {
 			const popover = this.$refs.popover as any;
@@ -88,6 +111,7 @@ export default Vue.extend({
 			});
 		});
 	},
+
 	methods: {
 		react(reaction) {
 			(this as any).api('notes/reactions/create', {
@@ -95,15 +119,19 @@ export default Vue.extend({
 				reaction: reaction
 			}).then(() => {
 				if (this.cb) this.cb();
+				this.$emit('closed');
 				this.destroyDom();
 			});
 		},
+
 		onMouseover(e) {
 			this.title = e.target.title;
 		},
+
 		onMouseout(e) {
 			this.title = placeholder;
 		},
+
 		close() {
 			(this.$refs.backdrop as any).style.pointerEvents = 'none';
 			anime({
@@ -120,7 +148,10 @@ export default Vue.extend({
 				scale: 0.5,
 				duration: 200,
 				easing: 'easeInBack',
-				complete: () => this.destroyDom()
+				complete: () => {
+					this.$emit('closed');
+					this.destroyDom();
+				}
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
index b894f0e10..933d31f29 100644
--- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
+++ b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
 	<span slot="header">
 		<span v-html="title" :class="$style.title"></span>
 		<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>
diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
index 0c4643fdc..03d6fd163 100644
--- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
+++ b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
 	<span slot="header">
 		<span v-html="title" :class="$style.title"></span>
 	</span>
diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue
index 1f45b6432..191579538 100644
--- a/src/client/app/desktop/views/components/drive-window.vue
+++ b/src/client/app/desktop/views/components/drive-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout">
+<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout">
 	<template slot="header">
 		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
 		<span :class="$style.title">%fa:cloud%%i18n:@drive%</span>
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index fdab7bc1c..d5214adb2 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window width="400px" height="550px" @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="destroyDom">
 	<span slot="header" :class="$style.header">
 		<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
 	</span>
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index 7cca833a8..aa9f2bde7 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window width="400px" height="550px" @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="destroyDom">
 	<span slot="header" :class="$style.header">
 		<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
 	</span>
diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue
index 7c6cb9cd4..594eae58f 100644
--- a/src/client/app/desktop/views/components/game-window.vue
+++ b/src/client/app/desktop/views/components/game-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
 	<span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span>
 	<mk-reversi :class="$style.content" @gamed="g => game = g"/>
 </mk-window>
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index d45cc82e1..79c9a9a51 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -237,6 +237,10 @@ export default Vue.extend({
 
 		warp(date) {
 			(this.$refs.tl as any).warp(date);
+		},
+
+		focus() {
+			(this.$refs.tl as any).focus();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/input-dialog.vue b/src/client/app/desktop/views/components/input-dialog.vue
index e2cf4e48f..cf7c09ea5 100644
--- a/src/client/app/desktop/views/components/input-dialog.vue
+++ b/src/client/app/desktop/views/components/input-dialog.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy">
+<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom">
 	<span slot="header" :class="$style.header">
 		%fa:i-cursor%{{ title }}
 	</span>
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 41b421b0e..370637760 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
 	<span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span>
 	<mk-messaging-room :user="user" :class="$style.content"/>
 </mk-window>
diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue
index 9580c5061..a8f0fc68b 100644
--- a/src/client/app/desktop/views/components/messaging-window.vue
+++ b/src/client/app/desktop/views/components/messaging-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" width="500px" height="560px" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
 	<span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span>
 	<mk-messaging :class="$style.content" @navigate="navigate"/>
 </mk-window>
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 46a866f9a..fadf47e62 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
+<div class="note" tabindex="-1" v-hotkey="keymap" :title="title">
 	<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
 		<x-sub :note="p.reply"/>
 	</div>
@@ -111,6 +111,18 @@ export default Vue.extend({
 	},
 
 	computed: {
+		keymap(): any {
+			return {
+				'r': this.reply,
+				'a': this.react,
+				'n': this.renote,
+				'up': this.focusBefore,
+				'shift+tab': this.focusBefore,
+				'down': this.focusAfter,
+				'tab': this.focusAfter,
+			};
+		},
+
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
@@ -223,64 +235,39 @@ export default Vue.extend({
 		reply() {
 			(this as any).os.new(MkPostFormWindow, {
 				reply: this.p
-			});
+			}).$once('closed', this.focus);
 		},
 
 		renote() {
 			(this as any).os.new(MkRenoteFormWindow, {
 				note: this.p
-			});
+			}).$once('closed', this.focus);
 		},
 
 		react() {
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
 				note: this.p
-			});
+			}).$once('closed', this.focus);
 		},
 
 		menu() {
 			(this as any).os.new(MkNoteMenu, {
 				source: this.$refs.menuButton,
 				note: this.p
-			});
+			}).$once('closed', this.focus);
 		},
 
-		onKeydown(e) {
-			let shouldBeCancel = true;
+		focus() {
+			this.$el.focus();
+		},
 
-			switch (true) {
-				case e.which == 38: // [↑]
-				case e.which == 74: // [j]
-				case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
-					focus(this.$el, e => e.previousElementSibling);
-					break;
+		focusBefore() {
+			focus(this.$el, e => e.previousElementSibling);
+		},
 
-				case e.which == 40: // [↓]
-				case e.which == 75: // [k]
-				case e.which == 9: // [Tab]
-					focus(this.$el, e => e.nextElementSibling);
-					break;
-
-				case e.which == 81: // [q]
-				case e.which == 69: // [e]
-					this.renote();
-					break;
-
-				case e.which == 70: // [f]
-				case e.which == 76: // [l]
-					//this.like();
-					break;
-
-				case e.which == 82: // [r]
-					this.reply();
-					break;
-
-				default:
-					shouldBeCancel = false;
-			}
-
-			if (shouldBeCancel) e.preventDefault();
+		focusAfter() {
+			focus(this.$el, e => e.nextElementSibling);
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index ec9aa285d..469f62c08 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -12,7 +12,7 @@
 	<!-- トランジションを有効にするとなぜかメモリリークする -->
 	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div">
 		<template v-for="(note, i) in _notes">
-			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
+			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
 				<span>%fa:angle-up%{{ note._datetext }}</span>
 				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
@@ -89,7 +89,7 @@ export default Vue.extend({
 		},
 
 		focus() {
-			(this.$el as any).children[0].focus();
+			(this.$refs.note as any)[0].focus();
 		},
 
 		onNoteUpdated(i, note) {
diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
index a88c96d1b..ade84f6bb 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy">
+<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed">
 	<span slot="header" class="mk-post-form-window--header">
 		<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
 		<span v-if="!reply">%i18n:@note%</span>
@@ -53,6 +53,10 @@ export default Vue.extend({
 		},
 		onPosted() {
 			(this.$refs.window as any).close();
+		},
+		onWindowClosed() {
+			this.$emit('closed');
+			this.destroyDom();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue
index 2f59733d9..cc25ba8e3 100644
--- a/src/client/app/desktop/views/components/progress-dialog.vue
+++ b/src/client/app/desktop/views/components/progress-dialog.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy">
+<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom">
 	<span slot="header">{{ title }}<mk-ellipsis/></span>
 	<div :class="$style.body">
 		<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>
diff --git a/src/client/app/desktop/views/components/received-follow-requests-window.vue b/src/client/app/desktop/views/components/received-follow-requests-window.vue
index 26b7ec259..d8a94f6cb 100644
--- a/src/client/app/desktop/views/components/received-follow-requests-window.vue
+++ b/src/client/app/desktop/views/components/received-follow-requests-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
 	<span slot="header">%fa:envelope R% %i18n:@title%</span>
 
 	<div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">
diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue
index df9d2f7fc..6c9cb59d4 100644
--- a/src/client/app/desktop/views/components/renote-form-window.vue
+++ b/src/client/app/desktop/views/components/renote-form-window.vue
@@ -1,7 +1,7 @@
 <template>
-<mk-window ref="window" is-modal @closed="$destroy">
+<mk-window ref="window" is-modal @closed="onWindowClosed">
 	<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
-	<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
+	<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
 </mk-window>
 </template>
 
@@ -10,25 +10,32 @@ import Vue from 'vue';
 
 export default Vue.extend({
 	props: ['note'],
-	mounted() {
-		document.addEventListener('keydown', this.onDocumentKeydown);
-	},
-	beforeDestroy() {
-		document.removeEventListener('keydown', this.onDocumentKeydown);
+
+	computed: {
+		keymap(): any {
+			return {
+				'esc': this.close,
+				'ctrl+enter': this.post
+			};
+		}
 	},
+
 	methods: {
-		onDocumentKeydown(e) {
-			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
-				if (e.which == 27) { // Esc
-					(this.$refs.window as any).close();
-				}
-			}
+		post() {
+			(this.$refs.form as any).ok();
+		},
+		close() {
+			(this.$refs.window as any).close();
 		},
 		onPosted() {
 			(this.$refs.window as any).close();
 		},
 		onCanceled() {
 			(this.$refs.window as any).close();
+		},
+		onWindowClosed() {
+			this.$emit('closed');
+			this.destroyDom();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
index b4cc57028..424771774 100644
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ b/src/client/app/desktop/views/components/settings-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
+<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom">
 	<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
 	<mk-settings :initial-page="initialPage" @done="close"/>
 </mk-window>
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index c8aa36f17..ff73bde95 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -152,14 +152,11 @@ export default Vue.extend({
 			});
 		}
 
-		document.addEventListener('keydown', this.onKeydown);
-
 		this.fetch();
 	},
 
 	beforeDestroy() {
 		this.$emit('beforeDestroy');
-		document.removeEventListener('keydown', this.onKeydown);
 	},
 
 	methods: {
@@ -212,14 +209,6 @@ export default Vue.extend({
 		warp(date) {
 			this.date = date;
 			this.fetch();
-		},
-
-		onKeydown(e) {
-			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
-				if (e.which == 84) { // t
-					this.focus();
-				}
-			}
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index ccc35f95f..9f421a68e 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -92,6 +92,10 @@ export default Vue.extend({
 			});
 		},
 
+		focus() {
+			(this.$refs.tl as any).focus();
+		},
+
 		warp(date) {
 			(this.$refs.tl as any).warp(date);
 		},
diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue
index d410c3d98..a28cb3029 100644
--- a/src/client/app/desktop/views/components/ui.vue
+++ b/src/client/app/desktop/views/components/ui.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-ui" :style="style">
+<div class="mk-ui" :style="style" v-hotkey.global="keymap">
 	<x-header class="header" v-show="!zenMode"/>
 	<div class="content">
 		<slot></slot>
@@ -16,11 +16,13 @@ export default Vue.extend({
 	components: {
 		XHeader
 	},
+
 	data() {
 		return {
 			zenMode: false
 		};
 	},
+
 	computed: {
 		style(): any {
 			if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
@@ -28,27 +30,24 @@ export default Vue.extend({
 				backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null,
 				backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })`
 			};
+		},
+
+		keymap(): any {
+			return {
+				'p': this.post,
+				'n': this.post,
+				'z': this.toggleZenMode
+			};
 		}
 	},
-	mounted() {
-		document.addEventListener('keydown', this.onKeydown);
-	},
-	beforeDestroy() {
-		document.removeEventListener('keydown', this.onKeydown);
-	},
+
 	methods: {
-		onKeydown(e) {
-			if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
+		post() {
+			(this as any).apis.post();
+		},
 
-			if (e.which == 80 || e.which == 78) { // p or n
-				e.preventDefault();
-				(this as any).apis.post();
-			}
-
-			if (e.which == 90) { // z
-				e.preventDefault();
-				this.zenMode = !this.zenMode;
-			}
+		toggleZenMode() {
+			this.zenMode = !this.zenMode;
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 72ae9cf4e..75253e078 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
 	<span slot="header">%fa:list% %i18n:@title%</span>
 
 	<div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index 30f0ec558..e6886956e 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -190,8 +190,8 @@ export default Vue.extend({
 			});
 
 			setTimeout(() => {
-				this.destroyDom();
 				this.$emit('closed');
+				this.destroyDom();
 			}, 300);
 		},
 
diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue
index c7ff0904e..e595ef4c3 100644
--- a/src/client/app/desktop/views/pages/home.vue
+++ b/src/client/app/desktop/views/pages/home.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<mk-home :mode="mode" @loaded="loaded"/>
+	<mk-home :mode="mode" @loaded="loaded" ref="home" v-hotkey.global="keymap"/>
 </mk-ui>
 </template>
 
@@ -15,6 +15,13 @@ export default Vue.extend({
 			default: 'timeline'
 		}
 	},
+	computed: {
+		keymap(): any {
+			return {
+				't': this.focus
+			};
+		}
+	},
 	mounted() {
 		document.title = (this as any).os.instanceName;
 
@@ -23,6 +30,9 @@ export default Vue.extend({
 	methods: {
 		loaded() {
 			Progress.done();
+		},
+		focus() {
+			this.$refs.home.focus();
 		}
 	}
 });
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index db3852da6..3a03f8492 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -8,6 +8,7 @@ import VueRouter from 'vue-router';
 import * as TreeView from 'vue-json-tree-view';
 import VAnimateCss from 'v-animate-css';
 import VModal from 'vue-js-modal';
+import VueHotkey from './common/hotkey';
 
 import App from './app.vue';
 import checkForUpdate from './common/scripts/check-for-update';
@@ -19,6 +20,7 @@ Vue.use(VueRouter);
 Vue.use(TreeView);
 Vue.use(VAnimateCss);
 Vue.use(VModal);
+Vue.use(VueHotkey);
 
 // Register global directives
 require('./common/views/directives');