diff --git a/src/web/app/desktop/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
similarity index 53%
rename from src/web/app/desktop/views/components/autocomplete.vue
rename to src/web/app/common/views/components/autocomplete.vue
index a99d405e82e7828e629c4a8e318c14b665e05292..19808049273c77c082cbfce1aceaa008027a772b 100644
--- a/src/web/app/desktop/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -1,26 +1,40 @@
 <template>
-<div class="mk-autocomplete">
-	<ol class="users" ref="users" v-if="users.length > 0">
-		<li v-for="user in users" @click="complete(user)" @keydown="onKeydown" tabindex="-1">
+<div class="mk-autocomplete" @contextmenu.prevent="() => {}">
+	<ol class="users" ref="suggests" v-if="users.length > 0">
+		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
 			<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
 			<span class="name">{{ user.name }}</span>
 			<span class="username">@{{ user.username }}</span>
 		</li>
 	</ol>
+	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
+		<li v-for="emoji in emojis" @click="complete(type, pictograph.dic[emoji])" @keydown="onKeydown" tabindex="-1">
+			<span class="emoji">{{ pictograph.dic[emoji] }}</span>
+			<span class="name" v-html="emoji.replace(q, `<b>${q}</b>`)"></span>
+		</li>
+	</ol>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import * as pictograph from 'pictograph';
 import contains from '../../../common/scripts/contains';
 
 export default Vue.extend({
-	props: ['q', 'textarea', 'complete', 'close'],
+	props: ['type', 'q', 'textarea', 'complete', 'close'],
 	data() {
 		return {
 			fetching: true,
 			users: [],
-			select: -1
+			emojis: [],
+			select: -1,
+			pictograph
+		}
+	},
+	computed: {
+		items(): HTMLCollection {
+			return (this.$refs.suggests as Element).children;
 		}
 	},
 	mounted() {
@@ -30,13 +44,19 @@ export default Vue.extend({
 			el.addEventListener('mousedown', this.onMousedown);
 		});
 
-		(this as any).api('users/search_by_username', {
-			query: this.q,
-			limit: 30
-		}).then(users => {
-			this.users = users;
-			this.fetching = false;
-		});
+		if (this.type == 'user') {
+			(this as any).api('users/search_by_username', {
+				query: this.q,
+				limit: 30
+			}).then(users => {
+				this.users = users;
+				this.fetching = false;
+			});
+		} else if (this.type == 'emoji') {
+			const emojis = Object.keys(pictograph.dic).sort((a, b) => a.length - b.length);
+			const matched = emojis.filter(e => e.indexOf(this.q) > -1);
+			this.emojis = matched.filter((x, i) => i <= 30);
+		}
 	},
 	beforeDestroy() {
 		this.textarea.removeEventListener('keydown', this.onKeydown);
@@ -61,7 +81,7 @@ export default Vue.extend({
 				case 13: // [ENTER]
 					if (this.select !== -1) {
 						cancel();
-						this.complete(this.users[this.select]);
+						(this.items[this.select] as any).click();
 					} else {
 						this.close();
 					}
@@ -93,24 +113,22 @@ export default Vue.extend({
 		},
 
 		selectNext() {
-			if (++this.select >= this.users.length) this.select = 0;
+			if (++this.select >= this.items.length) this.select = 0;
 			this.applySelect();
 		},
 
 		selectPrev() {
-			if (--this.select < 0) this.select = this.users.length - 1;
+			if (--this.select < 0) this.select = this.items.length - 1;
 			this.applySelect();
 		},
 
 		applySelect() {
-			const els = (this.$refs.users as Element).children;
-
-			Array.from(els).forEach(el => {
+			Array.from(this.items).forEach(el => {
 				el.removeAttribute('data-selected');
 			});
 
-			els[this.select].setAttribute('data-selected', 'true');
-			(els[this.select] as any).focus();
+			this.items[this.select].setAttribute('data-selected', 'true');
+			(this.items[this.select] as any).focus();
 		}
 	}
 });
@@ -126,7 +144,7 @@ export default Vue.extend({
 	border solid 1px rgba(0, 0, 0, 0.1)
 	border-radius 4px
 
-	> .users
+	> ol
 		display block
 		margin 0
 		padding 4px 0
@@ -149,42 +167,47 @@ export default Vue.extend({
 
 			&:hover
 			&[data-selected='true']
-				color #fff
 				background $theme-color
 
-				.name
-					color #fff
-
-				.username
-					color #fff
+				&, *
+					color #fff !important
 
 			&:active
-				color #fff
 				background darken($theme-color, 10%)
 
-				.name
-					color #fff
-
-				.username
-					color #fff
-
-			.avatar
-				vertical-align middle
-				min-width 28px
-				min-height 28px
-				max-width 28px
-				max-height 28px
-				margin 0 8px 0 0
-				border-radius 100%
-
-			.name
-				margin 0 8px 0 0
-				/*font-weight bold*/
-				font-weight normal
-				color rgba(0, 0, 0, 0.8)
-
-			.username
-				font-weight normal
-				color rgba(0, 0, 0, 0.3)
+				&, *
+					color #fff !important
+
+	> .users > li
+
+		.avatar
+			vertical-align middle
+			min-width 28px
+			min-height 28px
+			max-width 28px
+			max-height 28px
+			margin 0 8px 0 0
+			border-radius 100%
+
+		.name
+			margin 0 8px 0 0
+			/*font-weight bold*/
+			font-weight normal
+			color rgba(0, 0, 0, 0.8)
+
+		.username
+			font-weight normal
+			color rgba(0, 0, 0, 0.3)
+
+	> .emojis > li
+
+		.emoji
+			display inline-block
+			margin 0 4px 0 0
+			width 24px
+
+		.name
+			font-weight normal
+			color rgba(0, 0, 0, 0.8)
 
 </style>
diff --git a/src/web/app/common/views/components/messaging-room.form.vue b/src/web/app/common/views/components/messaging-room.form.vue
index b89365a5d84a38425972b4dec16f8a3822fec9e5..aa07217b32f27c968b20eeae7c150db91489639c 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-messaging-form">
-	<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%"></textarea>
+	<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%" v-autocomplete></textarea>
 	<div class="file" v-if="file">{{ file.name }}</div>
 	<mk-uploader ref="uploader"/>
 	<button class="send" @click="send" :disabled="sending" title="%i18n:common.send%">
diff --git a/src/web/app/desktop/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
similarity index 57%
rename from src/web/app/desktop/views/directives/autocomplete.ts
rename to src/web/app/common/views/directives/autocomplete.ts
index 53fa5a4df26fb672aecbf9d5fe6a0ff77ff06c4d..bd9c9cb611a09982c329e497028fb7ae200e67c1 100644
--- a/src/web/app/desktop/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -25,11 +25,11 @@ class Autocomplete {
 	 * 対象のテキストエリアを与えてインスタンスを初期化します。
 	 */
 	constructor(textarea) {
-		// BIND ---------------------------------
-		this.onInput =  this.onInput.bind(this);
+		//#region BIND
+		this.onInput = this.onInput.bind(this);
 		this.complete = this.complete.bind(this);
-		this.close =    this.close.bind(this);
-		// --------------------------------------
+		this.close = this.close.bind(this);
+		//#endregion
 
 		this.suggestion = null;
 		this.textarea = textarea;
@@ -60,14 +60,19 @@ class Autocomplete {
 		const text = this.textarea.value.substr(0, caret);
 
 		const mentionIndex = text.lastIndexOf('@');
-
-		if (mentionIndex == -1) return;
-
-		const username = text.substr(mentionIndex + 1);
-
-		if (!username.match(/^[a-zA-Z0-9-]+$/)) return;
-
-		this.open('user', username);
+		const emojiIndex = text.lastIndexOf(':');
+
+		if (mentionIndex != -1 && mentionIndex > emojiIndex) {
+			const username = text.substr(mentionIndex + 1);
+			if (!username.match(/^[a-zA-Z0-9-]+$/)) return;
+			this.open('user', username);
+		}
+
+		if (emojiIndex != -1 && emojiIndex > mentionIndex) {
+			const emoji = text.substr(emojiIndex + 1);
+			if (!emoji.match(/^[\+\-a-z_]+$/)) return;
+			this.open('emoji', emoji);
+		}
 	}
 
 	/**
@@ -88,14 +93,14 @@ class Autocomplete {
 			}
 		}).$mount();
 
-		// ~ サジェストを表示すべき位置を計算 ~
-
+		//#region サジェストを表示すべき位置を計算
 		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
 
 		const rect = this.textarea.getBoundingClientRect();
 
-		const x = rect.left + window.pageXOffset + caretPosition.left;
-		const y = rect.top + window.pageYOffset + caretPosition.top;
+		const x = rect.left + window.pageXOffset + caretPosition.left - this.textarea.scrollLeft;
+		const y = rect.top + window.pageYOffset + caretPosition.top - this.textarea.scrollTop;
+		//#endregion
 
 		this.suggestion.$el.style.left = x + 'px';
 		this.suggestion.$el.style.top = y + 'px';
@@ -119,24 +124,39 @@ class Autocomplete {
 	/**
 	 * オートコンプリートする
 	 */
-	private complete(user) {
+	private complete(type, value) {
 		this.close();
 
-		const value = user.username;
-
 		const caret = this.textarea.selectionStart;
-		const source = this.textarea.value;
 
-		const before = source.substr(0, caret);
-		const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
-		const after = source.substr(caret);
+		if (type == 'user') {
+			const source = this.textarea.value;
 
-		// 結果を挿入する
-		this.textarea.value = trimmedBefore + '@' + value + ' ' + after;
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
+			const after = source.substr(caret);
 
-		// キャレットを戻す
-		this.textarea.focus();
-		const pos = caret + value.length;
-		this.textarea.setSelectionRange(pos, pos);
+			// 挿入
+			this.textarea.value = trimmedBefore + '@' + value.username + ' ' + after;
+
+			// キャレットを戻す
+			this.textarea.focus();
+			const pos = caret + value.username.length;
+			this.textarea.setSelectionRange(pos, pos);
+		} else if (type == 'emoji') {
+			const source = this.textarea.value;
+
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
+			const after = source.substr(caret);
+
+			// 挿入
+			this.textarea.value = trimmedBefore + value + after;
+
+			// キャレットを戻す
+			this.textarea.focus();
+			const pos = caret + value.length;
+			this.textarea.setSelectionRange(pos, pos);
+		}
 	}
 }
diff --git a/src/web/app/common/views/directives/focus.ts b/src/web/app/common/views/directives/focus.ts
deleted file mode 100644
index b4fbcb6a87fd29bec944f539643b4a2caf295471..0000000000000000000000000000000000000000
--- a/src/web/app/common/views/directives/focus.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export default {
-	inserted(el) {
-		el.focus();
-	}
-};
diff --git a/src/web/app/common/views/directives/index.ts b/src/web/app/common/views/directives/index.ts
index 358866f500628f63e3811893c6bd08842fa1343c..268f07a950839f26ebbc258f99b50289b7c1e43a 100644
--- a/src/web/app/common/views/directives/index.ts
+++ b/src/web/app/common/views/directives/index.ts
@@ -1,5 +1,5 @@
 import Vue from 'vue';
 
-import focus from './focus';
+import autocomplete from './autocomplete';
 
-Vue.directive('focus', focus);
+Vue.directive('autocomplete', autocomplete);
diff --git a/src/web/app/desktop/views/directives/index.ts b/src/web/app/desktop/views/directives/index.ts
index 3d0c73b6b21adf8ecfca809cf263918da294dc76..324e07596d90b0d6f6263488fbb6c1c17f3987cd 100644
--- a/src/web/app/desktop/views/directives/index.ts
+++ b/src/web/app/desktop/views/directives/index.ts
@@ -1,8 +1,6 @@
 import Vue from 'vue';
 
 import userPreview from './user-preview';
-import autocomplete from './autocomplete';
 
 Vue.directive('userPreview', userPreview);
 Vue.directive('user-preview', userPreview);
-Vue.directive('autocomplete', autocomplete);