diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index bc0120c9a..d46d53665 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -14,7 +14,8 @@
 	</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>
+			<span class="emoji" v-if="emoji.url"><img :src="emoji.url" :alt="emoji.emoji"/></span>
+			<span class="emoji" v-else>{{ emoji.emoji }}</span>
 			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
 			<span class="alias" v-if="emoji.alias">({{ emoji.alias }})</span>
 		</li>
@@ -169,22 +170,45 @@ export default Vue.extend({
 				}
 			} else if (this.type == 'emoji') {
 				const matched = [];
+				const max = 30;
+
+				const customEmojis = (this.os.getMetaSync() || { emojis: [] }).emojis;
+				customEmojis.some(x => {
+					if (x.name.startsWith(this.q)) matched.push({
+						name: x.name,
+						emoji: `:${x.name}:`,
+						url: x.url
+					});
+					return matched.length == max;
+				});
+				customEmojis.some(x => {
+					const alias = (x.aliases || []).find(a => a.startsWith(this.q));
+					if (alias) matched.push({
+						alias: x.name,
+						name: alias,
+						emoji: `:${x.name}:`,
+						url: x.url
+					});
+					return matched.length == max;
+				});
+
 				emjdb.some(x => {
 					if (x.name.indexOf(this.q) == 0 && !x.alias && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-					return matched.length == 30;
+					return matched.length == max;
 				});
-				if (matched.length < 30) {
+				if (matched.length < max) {
 					emjdb.some(x => {
 						if (x.name.indexOf(this.q) == 0 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-						return matched.length == 30;
+						return matched.length == max;
 					});
 				}
-				if (matched.length < 30) {
+				if (matched.length < max) {
 					emjdb.some(x => {
 						if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-						return matched.length == 30;
+						return matched.length == max;
 					});
 				}
+
 				this.emojis = matched;
 			}
 		},
@@ -340,6 +364,10 @@ export default Vue.extend({
 			margin 0 4px 0 0
 			width 24px
 
+			> img
+				width 24px
+				vertical-align bottom
+
 		.name
 			color var(--autocompleteItemText)
 
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts
index d36a0a371..c45bb1022 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.ts
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts
@@ -3,7 +3,6 @@ import * as emojilib from 'emojilib';
 import { length } from 'stringz';
 import parse from '../../../../../mfm/parse';
 import getAcct from '../../../../../misc/acct/render';
-import { url } from '../../../config';
 import MkUrl from './url.vue';
 import MkGoogle from './google.vue';
 import { concat } from '../../../../../prelude/array';
@@ -186,6 +185,21 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'emoji': {
+					//#region カスタム絵文字
+					const customEmojis = (this.os.getMetaSync() || { emojis: [] }).emojis;
+					const customEmoji = customEmojis.find(e => e.name == token.emoji || (e.aliases || []).includes(token.emoji));
+					if (customEmoji) {
+						return [createElement('img', {
+							attrs: {
+								src: customEmoji.url,
+								alt: token.emoji,
+								title: token.emoji,
+								style: 'height: 2.5em; vertical-align: middle;'
+							}
+						})];
+					}
+					//#endregion
+
 					const emoji = emojilib.lib[token.emoji];
 					return [createElement('span', emoji ? emoji.char : token.content)];
 				}
diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts
index e2cc64d79..1a5c5daed 100644
--- a/src/client/app/common/views/directives/autocomplete.ts
+++ b/src/client/app/common/views/directives/autocomplete.ts
@@ -222,13 +222,15 @@ class Autocomplete {
 			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
 			const after = source.substr(caret);
 
+			if (value.startsWith(':')) value = value + ' ';
+
 			// 挿入
 			this.text = trimmedBefore + value + after;
 
 			// キャレットを戻す
 			this.vm.$nextTick(() => {
 				this.textarea.focus();
-				const pos = trimmedBefore.length + 1;
+				const pos = trimmedBefore.length + (value.startsWith(':') ? value.length : 1);
 				this.textarea.setSelectionRange(pos, pos);
 			});
 		}
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index d50c4e15b..9c351e904 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -510,6 +510,14 @@ export default class MiOS extends EventEmitter {
 		return promise;
 	}
 
+	/**
+	 * Misskeyのメタ情報を取得します
+	 */
+	@autobind
+	public getMetaSync() {
+		return this.meta ? this.meta.data : null;
+	}
+
 	/**
 	 * Misskeyのメタ情報を取得します
 	 * @param force キャッシュを無視するか否か
diff --git a/src/models/meta.ts b/src/models/meta.ts
index 3c0347485..7ecb4990d 100644
--- a/src/models/meta.ts
+++ b/src/models/meta.ts
@@ -15,4 +15,24 @@ export type IMeta = {
 	disableLocalTimeline?: boolean;
 	hidedTags?: string[];
 	bannerUrl?: string;
+
+	/**
+	 * カスタム絵文字定義
+	 */
+	emojis?: {
+		/**
+		 * 絵文字名 (例: thinking_ai)
+		 */
+		name: string;
+
+		/**
+		 * エイリアス
+		 */
+		aliases?: string[];
+
+		/**
+		 * 絵文字画像のURL
+		 */
+		url: string;
+	}[];
 };
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index f0ebfbe93..f45efd7fe 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -17,6 +17,12 @@ export const meta = {
 			}
 		}),
 
+		emojis: $.arr($.obj()).optional.note({
+			desc: {
+				'ja-JP': 'カスタム絵文字定義'
+			}
+		}),
+
 		disableRegistration: $.bool.optional.nullable.note({
 			desc: {
 				'ja-JP': '招待制か否か'
@@ -53,6 +59,10 @@ export default (params: any) => new Promise(async (res, rej) => {
 		set.broadcasts = ps.broadcasts;
 	}
 
+	if (ps.emojis) {
+		set.emojis = ps.emojis;
+	}
+
 	if (typeof ps.disableRegistration === 'boolean') {
 		set.disableRegistration = ps.disableRegistration;
 	}
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 0cd584231..8bc029383 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -50,6 +50,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 		hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined,
 		bannerUrl: meta.bannerUrl,
 		maxNoteTextLength: config.maxNoteTextLength,
+		emojis: meta.emojis,
 
 		features: {
 			registration: !meta.disableRegistration,