diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index b3a76fbb43ebf4f9cdad4556177a57101b2a7e71..3843539a3b87dcc1b6727b22e77cc8e730509e04 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -142,7 +142,8 @@ window.DropzoneInput = (function() {
       $(child).val(beforeSelection + formattedText + afterSelection);
       textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
       textarea.style.height = `${textarea.scrollHeight}px`;
-      return form_textarea.trigger("input");
+      form_textarea.trigger("input");
+      form_textarea.get(0).dispatchEvent(new Event('input'));
     };
     getFilename = function(e) {
       var value;
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index a4d517dddedc62c5d92c32abb67939b8174b82e9..87757b1a35dacb3f7791f8baea9ad44186039b52 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -45,6 +45,14 @@ export default {
       type: Boolean,
       required: true,
     },
+    markdownPreviewUrl: {
+      type: String,
+      required: true,
+    },
+    markdownDocs: {
+      type: String,
+      required: true,
+    },
   },
   data() {
     const store = new Store({
@@ -75,6 +83,7 @@ export default {
       this.store.formState = {
         title: this.state.titleText,
         confidential: this.isConfidential,
+        description: this.state.descriptionText,
       };
     },
     closeForm() {
@@ -155,7 +164,9 @@ export default {
     <form-component
       v-if="canUpdate && showForm"
       :form-state="formState"
-      :can-destroy="canDestroy" />
+      :can-destroy="canDestroy"
+      :markdown-docs="markdownDocs"
+      :markdown-preview-url="markdownPreviewUrl" />
     <div v-else>
       <title-component
         :issuable-ref="issuableRef"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 4ad3eb7dfd750f00760911e3b7f3abcf25e0f8e7..3281ec6b1726b5b4f352677d29a2ee381f4df5ec 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -18,11 +18,13 @@
       },
       updatedAt: {
         type: String,
-        required: true,
+        required: false,
+        default: '',
       },
       taskStatus: {
         type: String,
-        required: true,
+        required: false,
+        default: '',
       },
     },
     data() {
@@ -83,6 +85,7 @@
 
 <template>
   <div
+    v-if="descriptionHtml"
     class="description"
     :class="{
       'js-task-list-container': canUpdate
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b4c31811a0b983d67549cde7498ad8896760e0b5
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -0,0 +1,47 @@
+<script>
+  /* global Flash */
+  import markdownField from '../../../vue_shared/components/markdown/field.vue';
+
+  export default {
+    props: {
+      formState: {
+        type: Object,
+        required: true,
+      },
+      markdownPreviewUrl: {
+        type: String,
+        required: true,
+      },
+      markdownDocs: {
+        type: String,
+        required: true,
+      },
+    },
+    components: {
+      markdownField,
+    },
+  };
+</script>
+
+<template>
+  <div class="common-note-form">
+    <label
+      class="sr-only"
+      for="issue-description">
+      Description
+    </label>
+    <markdown-field
+      :markdown-preview-url="markdownPreviewUrl"
+      :markdown-docs="markdownDocs">
+      <textarea
+        id="issue-description"
+        class="note-textarea js-gfm-input js-autosize markdown-area"
+        data-supports-slash-commands="false"
+        aria-label="Description"
+        v-model="formState.description"
+        ref="textatea"
+        slot="textarea">
+      </textarea>
+    </markdown-field>
+  </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 862558562e504b20a6bbeb907528940d8f580fae..a653609c78e438367ed29f6615d18176b235e253 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -1,5 +1,6 @@
 <script>
   import titleField from './fields/title.vue';
+  import descriptionField from './fields/description.vue';
   import editActions from './edit_actions.vue';
   import confidentialCheckbox from './fields/confidential_checkbox.vue';
 
@@ -13,9 +14,18 @@
         type: Object,
         required: true,
       },
+      markdownPreviewUrl: {
+        type: String,
+        required: true,
+      },
+      markdownDocs: {
+        type: String,
+        required: true,
+      },
     },
     components: {
       titleField,
+      descriptionField,
       editActions,
       confidentialCheckbox,
     },
@@ -28,6 +38,10 @@
       :form-state="formState" />
     <confidential-checkbox
       :form-state="formState" />
+    <description-field
+      :form-state="formState"
+      :markdown-preview-url="markdownPreviewUrl"
+      :markdown-docs="markdownDocs" />
     <edit-actions
       :can-destroy="canDestroy" />
   </form>
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index b1e8f46797994bc8a68161daf188f3d57b629a6f..3b69be05cf355978a5038eaf0ee933f6e506272c 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -26,6 +26,8 @@ document.addEventListener('DOMContentLoaded', () => {
         endpoint,
         issuableRef,
         isConfidential,
+        markdownPreviewUrl,
+        markdownDocs,
       } = issuableElement.dataset;
 
       return {
@@ -37,6 +39,8 @@ document.addEventListener('DOMContentLoaded', () => {
         initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
         initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
         isConfidential: gl.utils.convertPermissionToBoolean(isConfidential),
+        markdownPreviewUrl,
+        markdownDocs,
       };
     },
     render(createElement) {
@@ -50,6 +54,8 @@ document.addEventListener('DOMContentLoaded', () => {
           initialDescriptionHtml: this.initialDescriptionHtml,
           initialDescriptionText: this.initialDescriptionText,
           isConfidential: this.isConfidential,
+          markdownPreviewUrl: this.markdownPreviewUrl,
+          markdownDocs: this.markdownDocs,
         },
       });
     },
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 5af63369211e0e2f7afe422afcc37e25c7a9e4f9..d90716bef80db9152e0eed7a1027ecb9216162bd 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -15,6 +15,7 @@ export default class Store {
     this.formState = {
       title: '',
       confidential: false,
+      description: '',
     };
   }
 
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
new file mode 100644
index 0000000000000000000000000000000000000000..68bbd263f02fb37e021c84a5046e3cfd159f274d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -0,0 +1,107 @@
+<script>
+  /* global Flash */
+  import markdownHeader from './header.vue';
+  import markdownToolbar from './toolbar.vue';
+
+  export default {
+    props: {
+      markdownPreviewUrl: {
+        type: String,
+        required: false,
+        default: '',
+      },
+      markdownDocs: {
+        type: String,
+        required: true,
+      },
+    },
+    data() {
+      return {
+        markdownPreview: '',
+        markdownPreviewLoading: false,
+        previewMarkdown: false,
+      };
+    },
+    components: {
+      markdownHeader,
+      markdownToolbar,
+    },
+    methods: {
+      toggleMarkdownPreview() {
+        this.previewMarkdown = !this.previewMarkdown;
+
+        if (!this.previewMarkdown) {
+          this.markdownPreview = '';
+        } else {
+          this.markdownPreviewLoading = true;
+          this.$http.post(
+            this.markdownPreviewUrl,
+            {
+              /*
+                Can't use `$refs` as the component is technically in the parent component
+                so we access the VNode & then get the element
+              */
+              text: this.$slots.textarea[0].elm.value,
+            },
+          )
+          .then((res) => {
+            const data = res.json();
+
+            this.markdownPreviewLoading = false;
+            this.markdownPreview = data.body;
+
+            this.$nextTick(() => {
+              $(this.$refs['markdown-preview']).renderGFM();
+            });
+          })
+          .catch(() => new Flash('Error loading markdown preview'));
+        }
+      },
+    },
+    mounted() {
+      /*
+        GLForm class handles all the toolbar buttons
+      */
+      return new gl.GLForm($(this.$refs['gl-form']));
+    },
+  };
+</script>
+
+<template>
+  <div
+    class="md-area prepend-top-default append-bottom-default"
+    ref="gl-form">
+    <markdown-header
+      :preview-markdown="previewMarkdown"
+      @toggle-markdown="toggleMarkdownPreview" />
+    <div
+      class="md-write-holder"
+      v-show="!previewMarkdown">
+      <div class="zen-backdrop">
+        <slot name="textarea"></slot>
+        <a
+          class="zen-control zen-control-leave js-zen-leave"
+          href="#"
+          aria-label="Enter zen mode">
+          <i
+            class="fa fa-compress"
+            aria-hidden="true">
+          </i>
+        </a>
+        <markdown-toolbar
+          :markdown-docs="markdownDocs" />
+      </div>
+    </div>
+    <div
+      class="md md-preview-holder md-preview"
+      v-show="previewMarkdown">
+      <div
+        ref="markdown-preview"
+        v-html="markdownPreview">
+      </div>
+      <span v-if="markdownPreviewLoading">
+        Loading...
+      </span>
+    </div>
+  </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7884b25c5efa5e68200634eb34a22372140f9166
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -0,0 +1,101 @@
+<script>
+  import tooltipMixin from '../../mixins/tooltip';
+  import toolbarButton from './toolbar_button.vue';
+
+  export default {
+    mixins: [
+      tooltipMixin,
+    ],
+    props: {
+      previewMarkdown: {
+        type: Boolean,
+        required: true,
+      },
+    },
+    components: {
+      toolbarButton,
+    },
+    methods: {
+      toggleMarkdownPreview(e) {
+        e.target.blur();
+
+        this.$emit('toggle-markdown');
+      },
+    },
+  };
+</script>
+
+<template>
+  <div class="md-header">
+    <ul class="nav-links clearfix">
+      <li :class="{ active: !previewMarkdown }">
+        <a
+          href="#md-write-holder"
+          tabindex="-1"
+          @click.prevent="toggleMarkdownPreview($event)">
+          Write
+        </a>
+      </li>
+      <li :class="{ active: previewMarkdown }">
+        <a
+          href="#md-preview-holder"
+          tabindex="-1"
+          @click.prevent="toggleMarkdownPreview($event)">
+          Preview
+        </a>
+      </li>
+      <li class="pull-right">
+        <div class="toolbar-group">
+          <toolbar-button
+            tag="**"
+            button-title="Add bold text"
+            icon="bold" />
+          <toolbar-button
+            tag="*"
+            button-title="Add italic text"
+            icon="italic" />
+          <toolbar-button
+            tag="> "
+            :prepend="true"
+            button-title="Insert a quote"
+            icon="quote-right" />
+          <toolbar-button
+            tag="`"
+            tag-block="```"
+            button-title="Insert code"
+            icon="code" />
+          <toolbar-button
+            tag="* "
+            :prepend="true"
+            button-title="Add a bullet list"
+            icon="list-ul" />
+          <toolbar-button
+            tag="1. "
+            :prepend="true"
+            button-title="Add a numbered list"
+            icon="list-ol" />
+          <toolbar-button
+            tag="* [ ] "
+            :prepend="true"
+            button-title="Add a task list"
+            icon="check-square-o" />
+        </div>
+        <div class="toolbar-group">
+          <button
+            aria-label="Go full screen"
+            class="toolbar-btn js-zen-enter"
+            data-container="body"
+            tabindex="-1"
+            title="Go full screen"
+            type="button"
+            ref="tooltip">
+            <i
+              aria-hidden="true"
+              class="fa fa-arrows-alt fa-fw">
+            </i>
+          </button>
+        </div>
+      </li>
+    </ul>
+  </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
new file mode 100644
index 0000000000000000000000000000000000000000..93252293ba68a7cd4a60f19bc7638394b689fa76
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -0,0 +1,33 @@
+<script>
+  export default {
+    props: {
+      markdownDocs: {
+        type: String,
+        required: true,
+      },
+    },
+  };
+</script>
+
+<template>
+  <div class="comment-toolbar clearfix">
+    <div class="toolbar-text">
+      <a
+        :href="markdownDocs"
+        target="_blank"
+        tabindex="-1">
+        Markdown is supported
+      </a>
+    </div>
+    <button
+      class="toolbar-button markdown-selector"
+      type="button"
+      tabindex="-1">
+      <i
+        class="fa fa-file-image-o toolbar-button-icon"
+        aria-hidden="true">
+      </i>
+      Attach a file
+    </button>
+  </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..096be50762572c1c265bdc8fb727126a96e06872
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -0,0 +1,58 @@
+<script>
+  import tooltipMixin from '../../mixins/tooltip';
+
+  export default {
+    mixins: [
+      tooltipMixin,
+    ],
+    props: {
+      buttonTitle: {
+        type: String,
+        required: true,
+      },
+      icon: {
+        type: String,
+        required: true,
+      },
+      tag: {
+        type: String,
+        required: true,
+      },
+      tagBlock: {
+        type: String,
+        required: false,
+        default: '',
+      },
+      prepend: {
+        type: Boolean,
+        required: false,
+        default: false,
+      },
+    },
+    computed: {
+      iconClass() {
+        return `fa-${this.icon}`;
+      },
+    },
+  };
+</script>
+
+<template>
+  <button
+    type="button"
+    class="toolbar-btn js-md hidden-xs"
+    tabindex="-1"
+    ref="tooltip"
+    data-container="body"
+    :data-md-tag="tag"
+    :data-md-block="tagBlock"
+    :data-md-prepend="prepend"
+    :title="buttonTitle"
+    :aria-label="buttonTitle">
+    <i
+      aria-hidden="true"
+      class="fa fa-fw"
+      :class="iconClass">
+    </i>
+  </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/tooltip.js b/app/assets/javascripts/vue_shared/mixins/tooltip.js
index 9bb948bff66b26f88c1065b28056e2c787863306..2e3b716a36cb2ee02e06b45d5ffe90bf6afbe769 100644
--- a/app/assets/javascripts/vue_shared/mixins/tooltip.js
+++ b/app/assets/javascripts/vue_shared/mixins/tooltip.js
@@ -1,9 +1,17 @@
 export default {
   mounted() {
-    $(this.$refs.tooltip).tooltip();
+    this.$nextTick(() => {
+      $(this.$refs.tooltip).tooltip();
+    });
   },
 
   updated() {
-    $(this.$refs.tooltip).tooltip('fixTitle');
+    this.$nextTick(() => {
+      $(this.$refs.tooltip).tooltip('fixTitle');
+    });
+  },
+
+  beforeDestroy() {
+    $(this.$refs.tooltip).tooltip('destroy');
   },
 };
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index c21cea259a1d6633514b9f34b654424b5145d5eb..9afffdba354a37fba830f67fcde2f86648da646c 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -56,6 +56,8 @@
       "can-destroy" => can?(current_user, :destroy_issue, @issue).to_s,
       "issuable-ref" => @issue.to_reference,
       "is-confidential" => @issue.confidential.to_s,
+      "markdown-preview-url" => preview_markdown_path(@project),
+      "markdown-docs" => help_page_path('user/markdown'),
     } }
       %h2.title= markdown_field(@issue, :title)
       - if @issue.description.present?