diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 712ce8a31f..0d2ffbe88d 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -7,6 +7,11 @@
 			<span class="username">@{{ user | acct }}</span>
 		</li>
 	</ol>
+	<ol class="hashtags" ref="suggests" v-if="hashtags.length > 0">
+		<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
+			<span class="name">{{ hashtag }}</span>
+		</li>
+	</ol>
 	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
 		<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
 			<span class="emoji">{{ emoji.emoji }}</span>
@@ -48,33 +53,33 @@ emjdb.sort((a, b) => a.name.length - b.name.length);
 
 export default Vue.extend({
 	props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'],
+
 	data() {
 		return {
 			fetching: true,
 			users: [],
+			hashtags: [],
 			emojis: [],
 			select: -1,
 			emojilib
 		}
 	},
+
 	computed: {
 		items(): HTMLCollection {
 			return (this.$refs.suggests as Element).children;
 		}
 	},
+
 	updated() {
 		//#region 位置調整
-		const margin = 32;
-
-		if (this.x + this.$el.offsetWidth > window.innerWidth - margin) {
-			this.$el.style.left = (this.x - this.$el.offsetWidth) + 'px';
-			this.$el.style.marginLeft = '-16px';
+		if (this.x + this.$el.offsetWidth > window.innerWidth) {
+			this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
 		} else {
 			this.$el.style.left = this.x + 'px';
-			this.$el.style.marginLeft = '0';
 		}
 
-		if (this.y + this.$el.offsetHeight > window.innerHeight - margin) {
+		if (this.y + this.$el.offsetHeight > window.innerHeight) {
 			this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px';
 			this.$el.style.marginTop = '0';
 		} else {
@@ -83,6 +88,7 @@ export default Vue.extend({
 		}
 		//#endregion
 	},
+
 	mounted() {
 		this.textarea.addEventListener('keydown', this.onKeydown);
 
@@ -100,6 +106,7 @@ export default Vue.extend({
 			});
 		});
 	},
+
 	beforeDestroy() {
 		this.textarea.removeEventListener('keydown', this.onKeydown);
 
@@ -107,6 +114,7 @@ export default Vue.extend({
 			el.removeEventListener('mousedown', this.onMousedown);
 		});
 	},
+
 	methods: {
 		exec() {
 			this.select = -1;
@@ -117,7 +125,8 @@ export default Vue.extend({
 			}
 
 			if (this.type == 'user') {
-				const cache = sessionStorage.getItem(this.q);
+				const cacheKey = 'autocomplete:user:' + this.q;
+				const cache = sessionStorage.getItem(cacheKey);
 				if (cache) {
 					const users = JSON.parse(cache);
 					this.users = users;
@@ -131,7 +140,26 @@ export default Vue.extend({
 						this.fetching = false;
 
 						// キャッシュ
-						sessionStorage.setItem(this.q, JSON.stringify(users));
+						sessionStorage.setItem(cacheKey, JSON.stringify(users));
+					});
+				}
+			} else if (this.type == 'hashtag') {
+				const cacheKey = 'autocomplete:hashtag:' + this.q;
+				const cache = sessionStorage.getItem(cacheKey);
+				if (cache) {
+					const hashtags = JSON.parse(cache);
+					this.hashtags = hashtags;
+					this.fetching = false;
+				} else {
+					(this as any).api('hashtags/search', {
+						query: this.q,
+						limit: 30
+					}).then(hashtags => {
+						this.hashtags = hashtags;
+						this.fetching = false;
+
+						// キャッシュ
+						sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
 					});
 				}
 			} else if (this.type == 'emoji') {
@@ -260,6 +288,8 @@ root(isDark)
 				user-select none
 
 			&:hover
+				background isDark ? rgba(#fff, 0.1) : rgba(#000, 0.1)
+
 			&[data-selected='true']
 				background $theme-color
 
@@ -292,6 +322,14 @@ root(isDark)
 			vertical-align middle
 			color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3)
 
+
+	> .hashtags > li
+
+		.name
+			vertical-align middle
+			margin 0 8px 0 0
+			color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
+
 	> .emojis > li
 
 		.emoji
@@ -300,11 +338,11 @@ root(isDark)
 			width 24px
 
 		.name
-			color rgba(#000, 0.8)
+			color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
 
 		.alias
 			margin 0 0 0 8px
-			color rgba(#000, 0.3)
+			color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3)
 
 .mk-autocomplete[data-darkmode]
 	root(true)
diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts
index 94635d301a..7ec377111b 100644
--- a/src/client/app/common/views/directives/autocomplete.ts
+++ b/src/client/app/common/views/directives/autocomplete.ts
@@ -67,15 +67,27 @@ class Autocomplete {
 	 * テキスト入力時
 	 */
 	private onInput() {
-		const caret = this.textarea.selectionStart;
-		const text = this.text.substr(0, caret);
+		const caretPos = this.textarea.selectionStart;
+		const text = this.text.substr(0, caretPos);
 
 		const mentionIndex = text.lastIndexOf('@');
+		const hashtagIndex = text.lastIndexOf('#');
 		const emojiIndex = text.lastIndexOf(':');
 
+		const start = Math.min(
+			mentionIndex == -1 ? Infinity : mentionIndex,
+			hashtagIndex == -1 ? Infinity : hashtagIndex,
+			emojiIndex == -1 ? Infinity : emojiIndex);
+
+		if (start == Infinity) return;
+
+		const isMention = mentionIndex == start;
+		const isHashtag = hashtagIndex == start;
+		const isEmoji = emojiIndex == start;
+
 		let opened = false;
 
-		if (mentionIndex != -1 && mentionIndex > emojiIndex) {
+		if (isMention) {
 			const username = text.substr(mentionIndex + 1);
 			if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
 				this.open('user', username);
@@ -83,7 +95,15 @@ class Autocomplete {
 			}
 		}
 
-		if (emojiIndex != -1 && emojiIndex > mentionIndex) {
+		if (isHashtag || opened == false) {
+			const hashtag = text.substr(hashtagIndex + 1);
+			if (hashtag != '' && !hashtag.includes(' ') && !hashtag.includes('\n')) {
+				this.open('hashtag', hashtag);
+				opened = true;
+			}
+		}
+
+		if (isEmoji || opened == false) {
 			const emoji = text.substr(emojiIndex + 1);
 			if (emoji != '' && emoji.match(/^[\+\-a-z0-9_]+$/)) {
 				this.open('emoji', emoji);
@@ -173,6 +193,22 @@ class Autocomplete {
 				const pos = trimmedBefore.length + (value.username.length + 2);
 				this.textarea.setSelectionRange(pos, pos);
 			});
+		} else if (type == 'hashtag') {
+			const source = this.text;
+
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
+			const after = source.substr(caret);
+
+			// 挿入
+			this.text = trimmedBefore + '#' + value + ' ' + after;
+
+			// キャレットを戻す
+			this.vm.$nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (value.length + 2);
+				this.textarea.setSelectionRange(pos, pos);
+			});
 		} else if (type == 'emoji') {
 			const source = this.text;
 
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 1015a44115..52ba95e87a 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -16,7 +16,7 @@
 			<a @click="addVisibleUser">+%i18n:@add-visible-user%</a>
 		</div>
 		<input v-show="useCw" v-model="cw" placeholder="%i18n:@cw-placeholder%">
-		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder"></textarea>
+		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="'text'"></textarea>
 		<div class="attaches" v-show="files.length != 0">
 			<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 				<div class="file" v-for="file in files" :key="file.id">
diff --git a/src/models/hashtag.ts b/src/models/hashtag.ts
new file mode 100644
index 0000000000..f5b6156055
--- /dev/null
+++ b/src/models/hashtag.ts
@@ -0,0 +1,13 @@
+import * as mongo from 'mongodb';
+import db from '../db/mongodb';
+
+const Hashtag = db.get<IHashtags>('hashtags');
+Hashtag.createIndex('tag', { unique: true });
+Hashtag.createIndex('mentionedUserIdsCount');
+export default Hashtag;
+
+export interface IHashtags {
+	tag: string;
+	mentionedUserIds: mongo.ObjectID[];
+	mentionedUserIdsCount: number;
+}
diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts
new file mode 100644
index 0000000000..988a786a08
--- /dev/null
+++ b/src/server/api/endpoints/hashtags/search.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import Hashtag from '../../../../models/hashtag';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		ja: 'ハッシュタグを検索します。'
+	},
+
+	requireCredential: false,
+
+	params: {
+		limit: $.num.optional.range(1, 100).note({
+			default: 10,
+			desc: {
+				ja: '最大数'
+			}
+		}),
+
+		query: $.str.note({
+			desc: {
+				ja: 'クエリ'
+			}
+		}),
+
+		offset: $.num.optional.min(0).note({
+			default: 0,
+			desc: {
+				ja: 'オフセット'
+			}
+		})
+	}
+};
+
+export default (params: any) => new Promise(async (res, rej) => {
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) throw psErr;
+
+	const hashtags = await Hashtag
+		.find({
+			tag: new RegExp(ps.query.toLowerCase())
+		}, {
+			sort: {
+				count: -1
+			},
+			limit: ps.limit,
+			skip: ps.offset
+		});
+
+	res(hashtags.map(tag => tag.tag));
+});
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index aec0e78964..6629e691b7 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -20,6 +20,7 @@ import UserList from '../../models/user-list';
 import resolveUser from '../../remote/resolve-user';
 import Meta from '../../models/meta';
 import config from '../../config';
+import registerHashtag from '../register-hashtag';
 
 type Type = 'reply' | 'renote' | 'quote' | 'mention';
 
@@ -64,7 +65,6 @@ export default async (user: IUser, data: {
 	geo?: any;
 	poll?: any;
 	viaMobile?: boolean;
-	tags?: string[];
 	cw?: string;
 	visibility?: string;
 	visibleUsers?: IUser[];
@@ -75,7 +75,7 @@ export default async (user: IUser, data: {
 	if (data.visibility == null) data.visibility = 'public';
 	if (data.viaMobile == null) data.viaMobile = false;
 
-	let tags = data.tags || [];
+	let tags: string[] = [];
 
 	let tokens: any[] = null;
 
@@ -149,6 +149,9 @@ export default async (user: IUser, data: {
 
 	res(note);
 
+	// ハッシュタグ登録
+	tags.map(tag => registerHashtag(user, tag));
+
 	//#region Increment notes count
 	if (isLocalUser(user)) {
 		Meta.update({}, {
diff --git a/src/services/register-hashtag.ts b/src/services/register-hashtag.ts
new file mode 100644
index 0000000000..ca6b74783b
--- /dev/null
+++ b/src/services/register-hashtag.ts
@@ -0,0 +1,28 @@
+import { IUser } from '../models/user';
+import Hashtag from '../models/hashtag';
+
+export default async function(user: IUser, tag: string) {
+	tag = tag.toLowerCase();
+
+	const index = await Hashtag.findOne({ tag });
+
+	if (index != null) {
+		// 自分が初めてこのタグを使ったなら
+		if (!index.mentionedUserIds.some(id => id.equals(user._id))) {
+			Hashtag.update({ tag }, {
+				$push: {
+					mentionedUserIds: user._id
+				},
+				$inc: {
+					mentionedUserIdsCount: 1
+				}
+			});
+		}
+	} else {
+		Hashtag.insert({
+			tag,
+			mentionedUserIds: [user._id],
+			mentionedUserIdsCount: 1
+		});
+	}
+}