diff --git a/.gitignore b/.gitignore
index 479d57c4..c8beda86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ test/e2e/reports
 selenium-debug.log
 .idea/
 config/local.json
+config/local.*.json
diff --git a/index.html b/index.html
index d1fdd7d4..40db0bbe 100644
--- a/index.html
+++ b/index.html
@@ -8,6 +8,7 @@
     <link rel="stylesheet" href="/static/font/css/animation.css">
     <link rel="stylesheet" href="/static/font/tiresias.css">
     <link rel="stylesheet" href="/static/font/css/lato.css">
+    <link rel="stylesheet" href="/static/mfm.css">
     <!--server-generated-meta-->
     <link rel="icon" type="image/png" href="/favicon.png">
   </head>
diff --git a/package.json b/package.json
index 58aeb363..405b6fe4 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "escape-html": "1.0.3",
     "js-cookie": "^3.0.1",
     "localforage": "1.10.0",
+    "mfm-js": "^0.22.1",
     "parse-link-header": "1.0.1",
     "phoenix": "1.6.2",
     "punycode.js": "2.1.0",
diff --git a/src/components/mfm_content/mfm_content.jsx b/src/components/mfm_content/mfm_content.jsx
new file mode 100644
index 00000000..e0ecda20
--- /dev/null
+++ b/src/components/mfm_content/mfm_content.jsx
@@ -0,0 +1,264 @@
+import { defineComponent, h } from 'vue'
+import * as mfm from 'mfm-js'
+import MentionLink from '../mention_link/mention_link.vue'
+import mention_link from '../mention_link/mention_link'
+
+function concat (xss) {
+  return ([]).concat(...xss)
+}
+
+export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle', 'rotate'];
+
+
+export default defineComponent({
+  props: {
+    status: {
+      type: Object,
+      required: true
+    }
+  },
+
+  render () {
+    if (!this.status) return null
+    const ast = mfm.parse(this.status.mfm_content, { fnNameList: MFM_TAGS })
+    const validTime = (t) => {
+      if (t == null) return null
+      return t.match(/^[0-9.]+s$/) ? t : null
+    }
+
+    const genEl = (ast) => concat(ast.map((token) => {
+      switch (token.type) {
+        case 'text': {
+          const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n')
+
+          const res = []
+          for (const t of text.split('\n')) {
+            res.push(h('br'))
+            res.push(t)
+          }
+          res.shift()
+          return res
+        }
+
+        case 'bold': {
+          return [h('b', genEl(token.children))]
+        }
+
+        case 'strike': {
+          return [h('del', genEl(token.children))]
+        }
+
+        case 'italic': {
+          return h('i', {
+            style: 'font-style: oblique;'
+          }, genEl(token.children))
+        }
+
+        case 'fn': {
+          // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
+          let style
+          switch (token.props.name) {
+            case 'tada': {
+              style = `font-size: 150%;` + 'animation: tada 1s linear infinite both;'
+              break
+            }
+            case 'jelly': {
+              const speed = validTime(token.props.args.speed) || '1s'
+              style = `animation: mfm-rubberBand ${speed} linear infinite both;`
+              break
+            }
+            case 'twitch': {
+              const speed = validTime(token.props.args.speed) || '0.5s'
+              style = `animation: mfm-twitch ${speed} ease infinite;`
+              break
+            }
+            case 'shake': {
+              const speed = validTime(token.props.args.speed) || '0.5s'
+              style = `animation: mfm-shake ${speed} ease infinite;`
+              break
+            }
+            case 'spin': {
+              const direction =
+								token.props.args.left ? 'reverse'
+								  : token.props.args.alternate ? 'alternate'
+								    : 'normal'
+              const anime =
+								token.props.args.x ? 'mfm-spinX'
+								  : token.props.args.y ? 'mfm-spinY'
+								    : 'mfm-spin'
+              const speed = validTime(token.props.args.speed) || '1.5s'
+              style = `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};`
+              break
+            }
+            case 'jump': {
+              style = 'animation: mfm-jump 0.75s linear infinite;'
+              break
+            }
+            case 'bounce': {
+              style = 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;'
+              break
+            }
+            case 'flip': {
+              const transform =
+								(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)'
+								  : token.props.args.v ? 'scaleY(-1)'
+								    : 'scaleX(-1)'
+              style = `transform: ${transform};`
+              break
+            }
+            case 'x2': {
+              style = `font-size: 200%;`
+              break
+            }
+            case 'x3': {
+              style = `font-size: 400%;`
+              break
+            }
+            case 'x4': {
+              style = `font-size: 600%;`
+              break
+            }
+            case 'font': {
+              const family =
+								token.props.args.serif ? 'serif'
+								  : token.props.args.monospace ? 'monospace'
+								    : token.props.args.cursive ? 'cursive'
+								      : token.props.args.fantasy ? 'fantasy'
+								        : token.props.args.emoji ? 'emoji'
+								          : token.props.args.math ? 'math'
+								            : null
+              if (family) style = `font-family: ${family};`
+              break
+            }
+            case 'blur': {
+              return h('span', {
+                class: '_mfm_blur_'
+              }, genEl(token.children))
+            }
+            case 'rainbow': {
+              style = 'animation: mfm-rainbow 1s linear infinite;'
+              break
+            }
+            case 'sparkle': {
+              return h(MkSparkle, {}, genEl(token.children))
+            }
+            case 'rotate': {
+              const degrees = parseInt(token.props.args.deg) || '90'
+              style = `transform: rotate(${degrees}deg); transform-origin: center center;`
+              break
+            }
+          }
+          if (style == null) {
+            return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']'])
+          } else {
+            return h('span', {
+              style: 'display: inline-block;' + style
+            }, genEl(token.children))
+          }
+        }
+
+        case 'small': {
+          return [h('small', {
+            style: 'opacity: 0.7;'
+          }, genEl(token.children))]
+        }
+
+        case 'center': {
+          return [h('div', {
+            style: 'text-align:center;'
+          }, genEl(token.children))]
+        }
+
+        case 'url': {
+          return [h('a', {
+            key: Math.random(),
+            href: token.props.url,
+            rel: 'nofollow noopener'
+          })]
+        }
+
+        case 'link': {
+          return [h('a', {
+            key: Math.random(),
+            href: token.props.url,
+            rel: 'nofollow noopener'
+          }, genEl(token.children))]
+        }
+
+        case 'mention': {
+          const user = this.status.attentions.find((mention) => `@${mention.screen_name}` === token.props.acct || mention.screen_name === token.props.username)
+          if (user) {
+            return [h(MentionLink, {
+              url: user.statusnet_profile_url,
+              content: token.props.acct,
+              userScreenName: token.props.acct
+            })]
+          }
+          return null
+        }
+
+        case 'hashtag': {
+          return [h('a', {
+            rel: 'noopener noreferrer',
+            target: '_blank',
+            key: token.props.hashtag,
+            href: this.status.tags.find((hash) => hash.name === token.props.hashtag).url
+          }, `#${token.props.hashtag}`)]
+        }
+
+        case 'blockCode': {
+          return [h('pre', {
+            key: Math.random(),
+            lang: token.props.lang
+          }, token.props.code)]
+        }
+
+        case 'inlineCode': {
+          return [h('pre', {
+            key: Math.random(),
+            code: token.props.code,
+            inline: true
+          })]
+        }
+
+        case 'quote': {
+          if (!this.nowrap) {
+            return [h('div', {
+              class: 'quote'
+            }, genEl(token.children))]
+          } else {
+            return [h('span', {
+              class: 'quote'
+            }, genEl(token.children))]
+          }
+        }
+
+        case 'emojiCode': {
+          return [h('div', {
+            class: 'still-image emoji img'
+          },
+          [h('img', {
+            key: Math.random(),
+            title: token.props.name,
+            alt: token.props.name,
+            src: this.status.emojis.find((emoji) => emoji.shortcode === token.props.name).static_url
+          })]
+          )]
+        }
+
+        case 'unicodeEmoji': {
+          return token.props.emoji
+        }
+
+        default: {
+          console.error('unrecognized ast type:', token.type)
+
+          return []
+        }
+      }
+    }))
+
+    // Parse ast to DOM
+    return h('span', genEl(ast))
+  }
+})
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 825ac06a..f2d332ce 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -99,6 +99,11 @@
             {{ $t('settings.sensitive_if_subject') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="renderMisskeyMarkdown">
+            {{ $t('settings.render_mfm') }}
+          </BooleanSetting>
+        </li>
         <li>
           <BooleanSetting
             path="alwaysShowNewPostButton"
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index b3ad3818..7c8a9e51 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -5,6 +5,7 @@
   white-space: normal;
   word-wrap: break-word;
   word-break: break-word;
+  overflow: hidden;
 
   &:hover {
     --_still-image-img-visibility: visible;
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index b8f6f9a0..1fe9eb7b 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -1,5 +1,6 @@
 import fileType from 'src/services/file_type/file_type.service'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
+import MFMContent from 'src/components/mfm_content/mfm_content.jsx'
 import { mapGetters } from 'vuex'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -35,9 +36,11 @@ const StatusContent = {
     'toggleShowingLongSubject'
   ],
   data () {
+    const { renderMisskeyMarkdown } = this.$store.getters.mergedConfig
     return {
       postLength: this.status.text.length,
-      parseReadyDone: false
+      parseReadyDone: false,
+      renderMisskeyMarkdown
     }
   },
   computed: {
@@ -81,7 +84,8 @@ const StatusContent = {
     ...mapGetters(['mergedConfig'])
   },
   components: {
-    RichContent
+    RichContent,
+    MFMContent
   },
   mounted () {
     this.status.attentions && this.status.attentions.forEach(attn => {
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 976fe98c..7add83aa 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -41,18 +41,26 @@
         >
           {{ $t("general.show_more") }}
         </button>
-        <RichContent
+        <div
           v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
-          :class="{ '-single-line': singleLine }"
-          class="text media-body"
-          :html="status.raw_html"
-          :emoji="status.emojis"
-          :handle-links="true"
-          :greentext="mergedConfig.greentext"
-          :attentions="status.attentions"
-          @parseReady="onParseReady"
-        />
-
+        >
+          <MFMContent
+            v-if="renderMisskeyMarkdown && status.mfm_content"
+            class="RichContent text media-body mfm-post-content"
+            :status="status"
+          />
+          <RichContent
+            v-else
+            :class="{ '-single-line': singleLine }"
+            class="text media-body"
+            :html="status.raw_html"
+            :emoji="status.emojis"
+            :handle-links="true"
+            :greentext="mergedConfig.greentext"
+            :attentions="status.attentions"
+            @parseReady="onParseReady"
+          />
+        </div>
         <button
           v-show="hideSubjectStatus"
           class="button-unstyled -link cw-status-hider"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 17977bdc..ec2882c5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -209,7 +209,8 @@
       "text/plain": "Plain text",
       "text/html": "HTML",
       "text/markdown": "Markdown",
-      "text/bbcode": "BBCode"
+      "text/bbcode": "BBCode",
+      "text/x.misskeymarkdown": "MFM"
     },
     "content_warning": "Subject (optional)",
     "default": "Just landed in L.A.",
@@ -507,6 +508,7 @@
     "post_status_content_type": "Post status content type",
     "sensitive_by_default": "Mark posts as sensitive by default",
     "sensitive_if_subject": "Automatically mark images as sensitive if a subject line is specified",
+    "render_mfm": "Render Misskey Markdown",
     "useStreamingApiWarning": "It's cool use it. If it breaks refresh I guess?",
     "stop_gifs": "Pause animated images until you hover on them",
     "streaming": "Automatically show new posts when scrolled to the top",
@@ -840,7 +842,7 @@
     "hide_repeats": "Hide repeats",
     "show_repeats": "Show repeats",
     "domain_muted": "Unblock domain",
-    "mute_domain": "Block domain", 
+    "mute_domain": "Block domain",
     "bot": "Bot",
     "admin_menu": {
       "moderation": "Moderation",
diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json
index 259b5480..cb52dffa 100644
--- a/src/i18n/ja_pedantic.json
+++ b/src/i18n/ja_pedantic.json
@@ -582,6 +582,7 @@
     "greentext": "引用を緑色で表示",
     "sensitive_by_default": "はじめから投稿をセンシティブとして設定",
     "sensitive_if_subject": "ステータスにサブジェクトをついたらNSFWにする",
+    "render_mfm": "Misskey Markdownを表示",
     "more_settings": "その他の設定",
     "reply_visibility_self_short": "自分宛のリプライを見る",
     "reply_visibility_following_short": "フォローしている人に宛てられたリプライを見る",
diff --git a/src/modules/config.js b/src/modules/config.js
index 4c860414..3afeeebe 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -95,6 +95,7 @@ export const defaultState = {
   virtualScrolling: undefined, // instance default
   sensitiveByDefault: undefined, // instance default
   sensitiveIfSubject: undefined,
+  renderMisskeyMarkdown: undefined,
   conversationDisplay: undefined, // instance default
   conversationTreeAdvanced: undefined, // instance default
   conversationOtherRepliesButton: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 50ede1bb..1da697a0 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -55,6 +55,7 @@ const defaultState = {
   virtualScrolling: true,
   sensitiveByDefault: false,
   sensitiveIfSubject: false,
+  renderMisskeyMarkdown: false,
   conversationDisplay: 'linear',
   conversationTreeAdvanced: false,
   conversationOtherRepliesButton: 'below',
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 9d0285b9..7924815e 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -280,6 +280,15 @@ export const parseStatus = (data) => {
       output.summary = data.spoiler_text
     }
 
+    if (data.akkoma) {
+      const { akkoma } = data
+      if (akkoma && akkoma.source && akkoma.source.mediaType === 'text/x.misskeymarkdown') {
+        output.mfm_content = akkoma.source.content
+      }
+    } else {
+      output.mfm_content = null
+    }
+
     output.in_reply_to_status_id = data.in_reply_to_id
     output.in_reply_to_user_id = data.in_reply_to_account_id
     output.replies_count = data.replies_count
diff --git a/static/mfm.css b/static/mfm.css
new file mode 100644
index 00000000..3be59f7e
--- /dev/null
+++ b/static/mfm.css
@@ -0,0 +1,143 @@
+.mfm-post-content {
+  overflow-y: hidden;
+};
+
+@keyframes tada {
+  from {
+    transform: scale3d(1, 1, 1);
+  }
+
+  10%,
+  20% {
+    transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
+  }
+
+  30%,
+  50%,
+  70%,
+  90% {
+    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+  }
+
+  40%,
+  60%,
+  80% {
+    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+  }
+
+  to {
+    transform: scale3d(1, 1, 1);
+  }
+}
+
+@keyframes bounce {
+  0% {
+    transform: scaleX(0.9) scaleY(0.9);
+  }
+
+  19% {
+    transform: scaleX(1.1) scaleY(1.1);
+  }
+
+  48% {
+    transform: scaleX(0.95) scaleY(0.95);
+  }
+
+  100% {
+    transform: scaleX(1) scaleY(1);
+  }
+}
+
+@keyframes mfm-spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+@keyframes mfm-spinX {
+  0% { transform: perspective(128px) rotateX(0deg); }
+  100% { transform: perspective(128px) rotateX(360deg); }
+}
+
+@keyframes mfm-spinY {
+  0% { transform: perspective(128px) rotateY(0deg); }
+  100% { transform: perspective(128px) rotateY(360deg); }
+}
+
+@keyframes mfm-jump {
+  0% { transform: translateY(0); }
+  25% { transform: translateY(-16px); }
+  50% { transform: translateY(0); }
+  75% { transform: translateY(-8px); }
+  100% { transform: translateY(0); }
+}
+
+@keyframes mfm-bounce {
+  0% { transform: translateY(0) scale(1, 1); }
+  25% { transform: translateY(-16px) scale(1, 1); }
+  50% { transform: translateY(0) scale(1, 1); }
+  75% { transform: translateY(0) scale(1.5, 0.75); }
+  100% { transform: translateY(0) scale(1, 1); }
+}
+
+@keyframes mfm-twitch {
+  0% { transform: translate(7px, -2px); }
+  5% { transform: translate(-3px, 1px); }
+  10% { transform: translate(-7px, -1px); }
+  15% { transform: translate(0, -1px); }
+  20% { transform: translate(-8px, 6px); }
+  25% { transform: translate(-4px, -3px); }
+  30% { transform: translate(-4px, -6px); }
+  35% { transform: translate(-8px, -8px); }
+  40% { transform: translate(4px, 6px); }
+  45% { transform: translate(-3px, 1px); }
+  50% { transform: translate(2px, -10px); }
+  55% { transform: translate(-7px, 0); }
+  60% { transform: translate(-2px, 4px); }
+  65% { transform: translate(3px, -8px); }
+  70% { transform: translate(6px, 7px); }
+  75% { transform: translate(-7px, -2px); }
+  80% { transform: translate(-7px, -8px); }
+  85% { transform: translate(9px, 3px); }
+  90% { transform: translate(-3px, -2px); }
+  95% { transform: translate(-10px, 2px); }
+  100% { transform: translate(-2px, -6px); }
+}
+
+@keyframes mfm-shake {
+  0% { transform: translate(-3px, -1px) rotate(-8deg); }
+  5% { transform: translate(0, -1px) rotate(-10deg); }
+  10% { transform: translate(1px, -3px) rotate(0deg); }
+  15% { transform: translate(1px, 1px) rotate(11deg); }
+  20% { transform: translate(-2px, 1px) rotate(1deg); }
+  25% { transform: translate(-1px, -2px) rotate(-2deg); }
+  30% { transform: translate(-1px, 2px) rotate(-3deg); }
+  35% { transform: translate(2px, 1px) rotate(6deg); }
+  40% { transform: translate(-2px, -3px) rotate(-9deg); }
+  45% { transform: translate(0, -1px) rotate(-12deg); }
+  50% { transform: translate(1px, 2px) rotate(10deg); }
+  55% { transform: translate(0, -3px) rotate(8deg); }
+  60% { transform: translate(1px, -1px) rotate(8deg); }
+  65% { transform: translate(0, -1px) rotate(-7deg); }
+  70% { transform: translate(-1px, -3px) rotate(6deg); }
+  75% { transform: translate(0, -2px) rotate(4deg); }
+  80% { transform: translate(-2px, -1px) rotate(3deg); }
+  85% { transform: translate(1px, -3px) rotate(-10deg); }
+  90% { transform: translate(1px, 0) rotate(3deg); }
+  95% { transform: translate(-2px, 0) rotate(-3deg); }
+  100% { transform: translate(2px, 1px) rotate(2deg); }
+}
+
+@keyframes mfm-rubberBand {
+  from { transform: scale3d(1, 1, 1); }
+  30% { transform: scale3d(1.25, 0.75, 1); }
+  40% { transform: scale3d(0.75, 1.25, 1); }
+  50% { transform: scale3d(1.15, 0.85, 1); }
+  65% { transform: scale3d(0.95, 1.05, 1); }
+  75% { transform: scale3d(1.05, 0.95, 1); }
+  to { transform: scale3d(1, 1, 1); }
+}
+
+@keyframes mfm-rainbow {
+  0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+  100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
diff --git a/yarn.lock b/yarn.lock
index e413b6d1..4c905d44 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6559,6 +6559,13 @@ methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
 
+mfm-js@^0.22.1:
+  version "0.22.1"
+  resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.22.1.tgz#ad5f0b95cc903ca5a5e414e2edf64ac4648dc8c2"
+  integrity sha512-UV5zvDKlWPpBFeABhyCzuOTJ3RwrNrmVpJ+zz/dFX6D/ntEywljgxkfsLamcy0ZSwUAr0O+WQxGHvAwyxUgsAQ==
+  dependencies:
+    twemoji-parser "14.0.x"
+
 micromatch@^3.1.10, micromatch@^3.1.4:
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@@ -9647,6 +9654,11 @@ tty-browserify@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
 
+twemoji-parser@14.0.x:
+  version "14.0.0"
+  resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-14.0.0.tgz#13dabcb6d3a261d9efbf58a1666b182033bf2b62"
+  integrity sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==
+
 type-check@~0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"