diff --git a/app/assets/javascripts/milestones/components/delete_milestone_modal.vue b/app/assets/javascripts/milestones/components/delete_milestone_modal.vue
index c13bf50eba735e526e961da09c30aaa17495d88f..1dca961b9045658444b19bb44d441c90076120d2 100644
--- a/app/assets/javascripts/milestones/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/milestones/components/delete_milestone_modal.vue
@@ -13,6 +13,11 @@ export default {
     GlSprintf,
   },
   props: {
+    visible: {
+      type: Boolean,
+      default: false,
+      required: false,
+    },
     issueCount: {
       type: Number,
       required: true,
@@ -98,8 +103,14 @@ Once deleted, it cannot be undone or recovered.`),
             });
           }
           throw error;
+        })
+        .finally(() => {
+          this.onClose();
         });
     },
+    onClose() {
+      this.$emit('deleteModalVisible', false);
+    },
   },
   primaryProps: {
     text: s__('Milestones|Delete milestone'),
@@ -113,11 +124,13 @@ Once deleted, it cannot be undone or recovered.`),
 
 <template>
   <gl-modal
+    :visible="visible"
     modal-id="delete-milestone-modal"
     :title="title"
     :action-primary="$options.primaryProps"
     :action-cancel="$options.cancelProps"
     @primary="onSubmit"
+    @hide="onClose"
   >
     <gl-sprintf :message="text">
       <template #milestoneTitle>
diff --git a/app/assets/javascripts/milestones/components/more_actions_dropdown.vue b/app/assets/javascripts/milestones/components/more_actions_dropdown.vue
new file mode 100644
index 0000000000000000000000000000000000000000..41000048fa230533cf4167e640bd6bb87cdfe261
--- /dev/null
+++ b/app/assets/javascripts/milestones/components/more_actions_dropdown.vue
@@ -0,0 +1,237 @@
+<script>
+import {
+  GlButton,
+  GlIcon,
+  GlDisclosureDropdownItem,
+  GlDisclosureDropdownGroup,
+  GlDisclosureDropdown,
+  GlTooltipDirective,
+} from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
+import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
+import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
+
+export default {
+  components: {
+    GlButton,
+    GlIcon,
+    GlDisclosureDropdownItem,
+    GlDisclosureDropdownGroup,
+    GlDisclosureDropdown,
+    PromoteMilestoneModal,
+    DeleteMilestoneModal,
+  },
+  directives: {
+    GlTooltip: GlTooltipDirective,
+  },
+  inject: [
+    'id',
+    'title',
+    'isActive',
+    'showDelete',
+    'isDetailPage',
+    'canReadMilestone',
+    'milestoneUrl',
+    'editUrl',
+    'closeUrl',
+    'reopenUrl',
+    'promoteUrl',
+    'groupName',
+    'issueCount',
+    'mergeRequestCount',
+  ],
+  data() {
+    return {
+      isDropdownVisible: false,
+      isPromotionModalVisible: false,
+      isDeleteModalVisible: false,
+      isPromoteModalVisible: false,
+    };
+  },
+  computed: {
+    hasUrl() {
+      return this.editUrl || this.closeUrl || this.reopenUrl || this.promoteUrl;
+    },
+    copiedToClipboard() {
+      return this.$options.i18n.copiedToClipboard;
+    },
+    editItem() {
+      return {
+        text: this.$options.i18n.edit,
+        href: this.editUrl,
+        extraAttrs: {
+          'data-testid': 'milestone-edit-item',
+        },
+      };
+    },
+    promoteItem() {
+      return {
+        text: this.$options.i18n.promote,
+        extraAttrs: {
+          'data-testid': 'milestone-promote-item',
+        },
+      };
+    },
+    closeItem() {
+      return {
+        text: this.$options.i18n.close,
+        href: this.closeUrl,
+        extraAttrs: {
+          class: { 'gl-sm-display-none!': this.isDetailPage },
+          'data-testid': 'milestone-close-item',
+          'data-method': 'put',
+          rel: 'nofollow',
+        },
+      };
+    },
+    reopenItem() {
+      return {
+        text: this.$options.i18n.reopen,
+        href: this.reopenUrl,
+        extraAttrs: {
+          class: { 'gl-sm-display-none!': this.isDetailPage },
+          'data-testid': 'milestone-reopen-item',
+          'data-method': 'put',
+          rel: 'nofollow',
+        },
+      };
+    },
+    deleteItem() {
+      return {
+        text: this.$options.i18n.delete,
+        extraAttrs: {
+          class: 'gl-text-red-500!',
+          'data-testid': 'milestone-delete-item',
+        },
+      };
+    },
+    copyIdItem() {
+      return {
+        text: sprintf(this.$options.i18n.copyTitle, { id: this.id }),
+        action: () => {
+          this.$toast.show(this.copiedToClipboard);
+        },
+        extraAttrs: {
+          'data-testid': 'copy-milestone-id',
+          itemprop: 'identifier',
+        },
+      };
+    },
+    showDropdownTooltip() {
+      return !this.isDropdownVisible ? this.$options.i18n.actionsLabel : '';
+    },
+    showTestIdIfNotDetailPage() {
+      return !this.isDetailPage ? 'milestone-more-actions-dropdown-toggle' : false;
+    },
+  },
+  methods: {
+    showDropdown() {
+      this.isDropdownVisible = true;
+    },
+    hideDropdown() {
+      this.isDropdownVisible = false;
+    },
+    setDeleteModalVisibility(visibility = false) {
+      this.isDeleteModalVisible = visibility;
+    },
+    setPromoteModalVisibility(visibility = false) {
+      this.isPromoteModalVisible = visibility;
+    },
+  },
+  primaryAction: {
+    text: s__('Milestones|Promote Milestone'),
+    attributes: { variant: 'confirm' },
+  },
+  cancelAction: {
+    text: __('Cancel'),
+    attributes: {},
+  },
+  i18n: {
+    actionsLabel: s__('Milestone|Milestone actions'),
+    close: __('Close'),
+    delete: __('Delete'),
+    edit: __('Edit'),
+    promote: __('Promote'),
+    reopen: __('Reopen'),
+    copyTitle: s__('Milestone|Copy milestone ID: %{id}'),
+    copiedToClipboard: s__('Milestone|Milestone ID copied to clipboard.'),
+  },
+};
+</script>
+
+<template>
+  <gl-disclosure-dropdown
+    v-gl-tooltip="showDropdownTooltip"
+    category="tertiary"
+    icon="ellipsis_v"
+    placement="bottom-end"
+    block
+    no-caret
+    :toggle-text="$options.i18n.actionsLabel"
+    text-sr-only
+    class="gl-relative gl-w-full gl-sm-w-auto gl-min-w-7"
+    :data-testid="showTestIdIfNotDetailPage"
+    @shown="showDropdown"
+    @hidden="hideDropdown"
+  >
+    <template v-if="isDetailPage" #toggle>
+      <div class="gl-min-h-7">
+        <gl-button
+          class="gl-md-display-none! gl-new-dropdown-toggle gl-absolute gl-top-0 gl-left-0 gl-w-full gl-sm-w-auto"
+          button-text-classes="gl-w-full"
+          category="secondary"
+          :aria-label="$options.i18n.actionsLabel"
+          :title="$options.i18n.actionsLabel"
+        >
+          <span class="gl-new-dropdown-button-text">{{ $options.i18n.actionsLabel }}</span>
+          <gl-icon class="dropdown-chevron" name="chevron-down" />
+        </gl-button>
+        <gl-button
+          class="gl-display-none gl-md-display-flex! gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
+          category="tertiary"
+          icon="ellipsis_v"
+          :aria-label="$options.i18n.actionsLabel"
+          :title="$options.i18n.actionsLabel"
+          data-testid="milestone-more-actions-dropdown-toggle"
+        />
+      </div>
+    </template>
+
+    <gl-disclosure-dropdown-item v-if="isActive" :item="closeItem" />
+    <gl-disclosure-dropdown-item v-else :item="reopenItem" />
+
+    <gl-disclosure-dropdown-item v-if="editUrl" :item="editItem" />
+
+    <gl-disclosure-dropdown-item
+      v-if="promoteUrl"
+      :item="promoteItem"
+      @action="setPromoteModalVisibility(true)"
+    />
+
+    <gl-disclosure-dropdown-group v-if="canReadMilestone" bordered class="gl-border-t-gray-200!">
+      <gl-disclosure-dropdown-item :item="copyIdItem" :data-clipboard-text="id" />
+    </gl-disclosure-dropdown-group>
+
+    <gl-disclosure-dropdown-group v-if="showDelete" bordered class="gl-border-t-gray-200!">
+      <gl-disclosure-dropdown-item :item="deleteItem" @action="setDeleteModalVisibility(true)" />
+    </gl-disclosure-dropdown-group>
+
+    <promote-milestone-modal
+      :visible="isPromoteModalVisible"
+      :milestone-title="title"
+      :promote-url="promoteUrl"
+      :group-name="groupName"
+      @promotionModalVisible="setPromoteModalVisibility"
+    />
+
+    <delete-milestone-modal
+      :visible="isDeleteModalVisible"
+      :issue-count="issueCount"
+      :merge-request-count="mergeRequestCount"
+      :milestone-id="id"
+      :milestone-title="title"
+      :milestone-url="milestoneUrl"
+      @deleteModalVisible="setDeleteModalVisibility"
+    />
+  </gl-disclosure-dropdown>
+</template>
diff --git a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
index 63791dcd011bc9e6361cceab582b72ef35a073f5..3ab63307fe209717d21f48ce3db2587f4b386fd7 100644
--- a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
@@ -9,14 +9,24 @@ export default {
   components: {
     GlModal,
   },
-  data() {
-    return {
-      milestoneTitle: '',
-      url: '',
-      groupName: '',
-      currentButton: null,
-      visible: false,
-    };
+  props: {
+    visible: {
+      type: Boolean,
+      default: false,
+      required: false,
+    },
+    milestoneTitle: {
+      type: String,
+      required: true,
+    },
+    promoteUrl: {
+      type: String,
+      required: true,
+    },
+    groupName: {
+      type: String,
+      required: true,
+    },
   },
   computed: {
     title() {
@@ -32,33 +42,10 @@ export default {
       );
     },
   },
-  mounted() {
-    this.getButtons().forEach((button) => {
-      button.addEventListener('click', this.onPromoteButtonClick);
-      button.removeAttribute('disabled');
-    });
-  },
-  beforeDestroy() {
-    this.getButtons().forEach((button) => {
-      button.removeEventListener('click', this.onPromoteButtonClick);
-    });
-  },
   methods: {
-    onPromoteButtonClick({ currentTarget }) {
-      const { milestoneTitle, url, groupName } = currentTarget.dataset;
-      currentTarget.setAttribute('disabled', '');
-      this.visible = true;
-      this.milestoneTitle = milestoneTitle;
-      this.url = url;
-      this.groupName = groupName;
-      this.currentButton = currentTarget;
-    },
-    getButtons() {
-      return document.querySelectorAll('.js-promote-project-milestone-button');
-    },
     onSubmit() {
       return axios
-        .post(this.url, { params: { format: 'json' } })
+        .post(this.promoteUrl, { params: { format: 'json' } })
         .then((response) => {
           visitUrl(response.data.url);
         })
@@ -68,14 +55,11 @@ export default {
           });
         })
         .finally(() => {
-          this.visible = false;
+          this.onClose();
         });
     },
     onClose() {
-      this.visible = false;
-      if (this.currentButton) {
-        this.currentButton.removeAttribute('disabled');
-      }
+      this.$emit('promotionModalVisible', false);
     },
   },
   primaryAction: {
@@ -92,9 +76,9 @@ export default {
   <gl-modal
     :visible="visible"
     modal-id="promote-milestone-modal"
+    :title="title"
     :action-primary="$options.primaryAction"
     :action-cancel="$options.cancelAction"
-    :title="title"
     @primary="onSubmit"
     @hide="onClose"
   >
diff --git a/app/assets/javascripts/milestones/index.js b/app/assets/javascripts/milestones/index.js
index 403db0865f0fe09b67d324b48990a0751513c2e1..af5b42af710e7d2c4e835598752860a7730c1aaa 100644
--- a/app/assets/javascripts/milestones/index.js
+++ b/app/assets/javascripts/milestones/index.js
@@ -1,20 +1,14 @@
-import Vue from 'vue';
 import initDatePicker from '~/behaviors/date_picker';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
 import Milestone from '~/milestones/milestone';
 import { renderGFM } from '~/behaviors/markdown/render_gfm';
 import { mountMarkdownEditor } from '~/vue_shared/components/markdown/mount_markdown_editor';
 import Sidebar from '~/right_sidebar';
 import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
-import Translate from '~/vue_shared/translate';
 import ZenMode from '~/zen_mode';
 import TaskList from '~/task_list';
 import { TYPE_MILESTONE } from '~/issues/constants';
 import { createAlert } from '~/alert';
 import { __ } from '~/locale';
-import DeleteMilestoneModal from './components/delete_milestone_modal.vue';
-import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
-import eventHub from './event_hub';
 
 // See app/views/shared/milestones/_description.html.haml
 export const MILESTONE_DESCRIPTION_ELEMENT = '.milestone-detail .description';
@@ -54,88 +48,3 @@ export function initShow() {
     },
   });
 }
-
-export function initPromoteMilestoneModal() {
-  Vue.use(Translate);
-
-  const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
-  if (!promoteMilestoneModal) {
-    return null;
-  }
-
-  return new Vue({
-    el: promoteMilestoneModal,
-    name: 'PromoteMilestoneModalRoot',
-    render(createElement) {
-      return createElement(PromoteMilestoneModal);
-    },
-  });
-}
-
-export function initDeleteMilestoneModal() {
-  Vue.use(Translate);
-
-  const onRequestFinished = ({ milestoneUrl, successful }) => {
-    const button = document.querySelector(
-      `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
-    );
-
-    if (!successful) {
-      button.removeAttribute('disabled');
-    }
-  };
-
-  const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
-
-  const onRequestStarted = (milestoneUrl) => {
-    const button = document.querySelector(
-      `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
-    );
-    button.setAttribute('disabled', '');
-    eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
-  };
-
-  return new Vue({
-    el: '#js-delete-milestone-modal',
-    name: 'DeleteMilestoneModalRoot',
-    data() {
-      return {
-        modalProps: {
-          milestoneId: -1,
-          milestoneTitle: '',
-          milestoneUrl: '',
-          issueCount: -1,
-          mergeRequestCount: -1,
-        },
-      };
-    },
-    mounted() {
-      eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
-      deleteMilestoneButtons.forEach((button) => {
-        button.removeAttribute('disabled');
-        button.addEventListener('click', () => {
-          this.$root.$emit(BV_SHOW_MODAL, 'delete-milestone-modal');
-          eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
-
-          this.setModalProps({
-            milestoneId: parseInt(button.dataset.milestoneId, 10),
-            milestoneTitle: button.dataset.milestoneTitle,
-            milestoneUrl: button.dataset.milestoneUrl,
-            issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
-            mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
-          });
-        });
-      });
-    },
-    methods: {
-      setModalProps(modalProps) {
-        this.modalProps = modalProps;
-      },
-    },
-    render(createElement) {
-      return createElement(DeleteMilestoneModal, {
-        props: this.modalProps,
-      });
-    },
-  });
-}
diff --git a/app/assets/javascripts/milestones/init_more_actions_dropdown.js b/app/assets/javascripts/milestones/init_more_actions_dropdown.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce3ac6544f83b1afb62a14ba0618392fabe76c90
--- /dev/null
+++ b/app/assets/javascripts/milestones/init_more_actions_dropdown.js
@@ -0,0 +1,52 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import MoreActionsDropdown from '~/milestones/components/more_actions_dropdown.vue';
+
+export default function InitMoreActionsDropdown() {
+  const containers = document.querySelectorAll('.js-vue-milestone-actions');
+
+  if (!containers.length) {
+    return false;
+  }
+
+  return containers.forEach((el) => {
+    const {
+      id,
+      title,
+      isActive,
+      showDelete,
+      isDetailPage,
+      canReadMilestone,
+      milestoneUrl,
+      editUrl,
+      closeUrl,
+      reopenUrl,
+      promoteUrl,
+      groupName,
+      issueCount,
+      mergeRequestCount,
+    } = el.dataset;
+
+    return new Vue({
+      el,
+      name: 'MoreActionsDropdownRoot',
+      provide: {
+        id: Number(id),
+        title,
+        isActive: parseBoolean(isActive),
+        showDelete: parseBoolean(showDelete),
+        isDetailPage: parseBoolean(isDetailPage),
+        canReadMilestone: parseBoolean(canReadMilestone),
+        milestoneUrl,
+        editUrl,
+        closeUrl,
+        reopenUrl,
+        promoteUrl,
+        groupName,
+        issueCount: Number(issueCount),
+        mergeRequestCount: Number(mergeRequestCount),
+      },
+      render: (createElement) => createElement(MoreActionsDropdown),
+    });
+  });
+}
diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js
index 88061d9ca22229b4a314f985fe8bd7a513f53901..730febb38de6c730d0108f959a295cff94e2e193 100644
--- a/app/assets/javascripts/pages/dashboard/milestones/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js
@@ -1,6 +1,7 @@
 import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown';
 import { RESOURCE_TYPE_MILESTONE } from '~/vue_shared/components/new_resource_dropdown/constants';
 import searchUserGroupsAndProjects from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql';
+import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
 
 initNewResourceDropdown({
   resourceType: RESOURCE_TYPE_MILESTONE,
@@ -10,3 +11,4 @@ initNewResourceDropdown({
     ...(data?.projects?.nodes ?? []),
   ],
 });
+InitMoreActionsDropdown();
diff --git a/app/assets/javascripts/pages/groups/milestones/index/index.js b/app/assets/javascripts/pages/groups/milestones/index/index.js
index 01cf830b782448a36462ea9e14079ec485345487..b3b3d7922056e296f0390d7fc8b54e93a908441e 100644
--- a/app/assets/javascripts/pages/groups/milestones/index/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/index/index.js
@@ -1,3 +1,3 @@
-import { initDeleteMilestoneModal } from '~/milestones';
+import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
 
-initDeleteMilestoneModal();
+InitMoreActionsDropdown();
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index f2ab5d7837479ba31a4591f63e179177f7740216..502155dc758acd21a3c3952db714dd8998a6cca4 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,4 +1,5 @@
-import { initDeleteMilestoneModal, initShow } from '~/milestones';
+import { initShow } from '~/milestones';
+import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
 
 initShow();
-initDeleteMilestoneModal();
+InitMoreActionsDropdown();
diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js
index ef1c9ab83dbe153c6bd01ae858bee808ac3b8e63..b3b3d7922056e296f0390d7fc8b54e93a908441e 100644
--- a/app/assets/javascripts/pages/projects/milestones/index/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/index/index.js
@@ -1,4 +1,3 @@
-import { initDeleteMilestoneModal, initPromoteMilestoneModal } from '~/milestones';
+import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
 
-initDeleteMilestoneModal();
-initPromoteMilestoneModal();
+InitMoreActionsDropdown();
diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js
index 16aac7748da82b83017bf47fef561456c8b52118..502155dc758acd21a3c3952db714dd8998a6cca4 100644
--- a/app/assets/javascripts/pages/projects/milestones/show/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/show/index.js
@@ -1,5 +1,5 @@
-import { initDeleteMilestoneModal, initPromoteMilestoneModal, initShow } from '~/milestones';
+import { initShow } from '~/milestones';
+import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
 
 initShow();
-initDeleteMilestoneModal();
-initPromoteMilestoneModal();
+InitMoreActionsDropdown();
diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml
deleted file mode 100644
index 65920a5453f0e01e0fec37589a157d419e510416..0000000000000000000000000000000000000000
--- a/app/views/shared/milestones/_delete_button.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- milestone_url = Gitlab::UrlBuilder.build(milestone, only_path: true)
-
-= render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: 'menu-item js-delete-milestone-button', data: { milestone_id: milestone.id, milestone_title: markdown_field(milestone, :title), milestone_url: milestone_url, milestone_issue_count: milestone.total_issues_count, milestone_merge_request_count: milestone.total_merge_requests_count }, disabled: true }) do
-  .gl-dropdown-item-text-wrapper.gl-text-red-500
-    = _('Delete')
-#js-delete-milestone-modal
diff --git a/app/views/shared/milestones/_description.html.haml b/app/views/shared/milestones/_description.html.haml
index ff1224f89d0cd129a0f23b8496e8db433a560719..e1180d37f74a2257fb11c583e1dd932a95e2e23f 100644
--- a/app/views/shared/milestones/_description.html.haml
+++ b/app/views/shared/milestones/_description.html.haml
@@ -1,11 +1,6 @@
 .detail-page-description.milestone-detail.gl-py-4
   %h2.gl-m-0{ data: { testid: "milestone-title-content" } }
     = markdown_field(milestone, :title)
-    .gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ itemprop: 'identifier' }
-      - if can?(current_user, :read_milestone, @milestone)
-        %span.gl-display-inline-block.gl-vertical-align-middle
-          = s_('MilestonePage|Milestone ID: %{milestone_id}') % { milestone_id: @milestone.id }
-          = clipboard_button(title: s_('MilestonePage|Copy milestone ID'), text: @milestone.id)
 
   - if milestone.try(:description).present?
     %div{ data: { testid: "milestone-description-content" } }
diff --git a/app/views/shared/milestones/_header.html.haml b/app/views/shared/milestones/_header.html.haml
index 3eae1bcd08079c190e9ad9728730fa225c1b0fab..856e9f10d99047595a8ae81301eee74b6d5f12cf 100644
--- a/app/views/shared/milestones/_header.html.haml
+++ b/app/views/shared/milestones/_header.html.haml
@@ -11,7 +11,10 @@
       = render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left', button_options: { 'aria-label' => _('Toggle sidebar'), class: 'btn-grouped !gl-float-right gl-sm-display-none js-sidebar-toggle' })
 
   - if can?(current_user, :admin_milestone, @group || @project)
-    .milestone-buttons.detail-page-header-actions.gl-display-flex.gl-align-self-start
+    - can_promote = @project && can_admin_group_milestones? && milestone.project
+    - can_read_milestone = can?(current_user, :read_milestone, @milestone)
+
+    .milestone-buttons.detail-page-header-actions.gl-display-flex.gl-align-self-start.gl-gap-3
       - if milestone.active?
         = render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :close }), method: :put, button_options: { class: 'btn-close gl-display-none gl-md-display-inline-block' }) do
           = _('Close milestone')
@@ -19,38 +22,18 @@
         = render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :activate }), method: :put, button_options: { class: 'gl-display-none gl-md-display-inline-block' }) do
           = _('Reopen milestone')
 
-      .gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full
-        = render Pajamas::ButtonComponent.new(category: :tertiary, icon: 'ellipsis_v', button_options: { class: 'has-tooltip gl-display-none! gl-md-display-inline-flex!', 'aria-label': _('Milestone actions'), data: { toggle: 'dropdown', title: _('Milestone actions'), testid: 'milestone-actions' } })
-        = render Pajamas::ButtonComponent.new(button_options: { class: 'btn-block gl-md-display-none!', data: { toggle: 'dropdown' } }) do
-          = _('Milestone actions')
-          = sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon"
-        .dropdown-menu.dropdown-menu-right
-          .gl-dropdown-inner
-            .gl-dropdown-contents
-              %ul
-                %li.gl-dropdown-item
-                  = link_to edit_milestone_path(milestone), class: 'menu-item' do
-                    .gl-dropdown-item-text-wrapper
-                      = _('Edit')
-                - if milestone.project_milestone? && milestone.project.group
-                  %li.gl-dropdown-item
-                    %button.js-promote-project-milestone-button{ data: { milestone_title: milestone.title,
-                      group_name: milestone.project.group.name,
-                      url: promote_project_milestone_path(milestone.project, milestone)},
-                      disabled: true,
-                      type: 'button' }
-                      .gl-dropdown-item-text-wrapper
-                        = _('Promote')
-                    #promote-milestone-modal
-                - if milestone.active?
-                  %li.gl-dropdown-item{ class: "gl-md-display-none!" }
-                    = link_to update_milestone_path(milestone, { state_event: :close }), method: :put, class: 'menu-item' do
-                      .gl-dropdown-item-text-wrapper
-                        = _('Close milestone')
-                - else
-                  %li.gl-dropdown-item{ class: "gl-md-display-none!" }
-                    = link_to update_milestone_path(milestone, { state_event: :activate }), method: :put, class: 'menu-item' do
-                      .gl-dropdown-item-text-wrapper
-                        = _('Reopen milestone')
-                %li.gl-dropdown-item
-                  = render 'shared/milestones/delete_button', milestone: @milestone
+      .js-vue-milestone-actions{ data: { id: @milestone.id,
+        title: milestone.title,
+        is_active: milestone.active?.to_s,
+        show_delete: 'true',
+        is_detail_page: 'true',
+        can_read_milestone: can_read_milestone.to_s,
+        milestone_url: Gitlab::UrlBuilder.build(milestone, only_path: true),
+        edit_url: edit_milestone_path(milestone),
+        close_url: update_milestone_path(milestone, { state_event: :close }),
+        reopen_url: update_milestone_path(milestone, { state_event: :activate }),
+        promote_url: can_promote ? promote_project_milestone_path(milestone.project, milestone) : '',
+        group_name: can_promote && milestone.project_milestone? && milestone.project.group ? milestone.project.group.name : '',
+        issue_count: @milestone.issues.count,
+        merge_request_count: @milestone.merge_requests.count
+      } }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index a25efadee5c6643aa7fedffdca48e3b8d03eabd8..c059b49b6c77dd6b7a52b57dfd39cbe8e34dbc97 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -48,39 +48,19 @@
       .float-lg-right.light
         = format(s_('Milestone|%{percentage}%{percent} complete'), percentage: milestone.percent_complete, percent: '%')
     - if can_admin_milestone
+      - show_delete = @project.present? || @group.present?
       .col-1.order-2.order-md-3
-        .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
-          = render Pajamas::ButtonComponent.new(category: :tertiary,
-            size: :small,
-            icon: 'ellipsis_v',
-            button_options: { class: 'gl-ml-3 has-tooltip', 'aria_label': _('Milestone actions'), title: _('Milestone actions'), data: { toggle: 'dropdown' } })
-          .dropdown-menu.dropdown-menu-right
-            .gl-dropdown-inner
-              .gl-dropdown-contents
-                %ul
-                  %li.gl-dropdown-item
-                    - if milestone.closed?
-                      = render Pajamas::ButtonComponent.new(category: :tertiary,
-                        href: milestone_path(milestone, milestone: { state_event: :activate }),
-                        method: :put,
-                        variant: :link) do
-                        = s_('Milestones|Reopen')
-                    - else
-                      = render Pajamas::ButtonComponent.new(category: :tertiary,
-                        href: milestone_path(milestone, milestone: { state_event: :close }),
-                        method: :put,
-                        variant: :link) do
-                        = s_('Milestones|Close')
-                  %li.gl-dropdown-item
-                    = render Pajamas::ButtonComponent.new(category: :tertiary,
-                      href: edit_milestone_path(milestone),
-                      variant: :link) do
-                      = _('Edit')
-                  - if can_promote
-                    %li.gl-dropdown-item
-                      = render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
-                        button_options: { class: 'js-promote-project-milestone-button', disabled: true, data: { toggle: 'tooltip', container: 'body', url: promote_project_milestone_path(milestone.project, milestone), milestone_title: milestone.title, group_name: @project.group.name } }) do
-                        = s_('Promote')
-                  - if @project || @group
-                    %li.gl-dropdown-item
-                      = render 'shared/milestones/delete_button', milestone: milestone
+        .gl-display-flex.gl-justify-content-end
+          .js-vue-milestone-actions{ data: { id: milestone.id,
+            title: milestone.title,
+            is_active: milestone.active?.to_s,
+            show_delete: show_delete.to_s,
+            milestone_url: Gitlab::UrlBuilder.build(milestone, only_path: true),
+            edit_url: edit_milestone_path(milestone),
+            close_url: milestone_path(milestone, milestone: { state_event: :close }),
+            reopen_url: milestone_path(milestone, milestone: { state_event: :activate }),
+            promote_url: can_promote ? promote_project_milestone_path(milestone.project, milestone) : '',
+            group_name: can_promote ? @project.group.name : '',
+            issue_count: milestone.issues.count,
+            merge_request_count: milestone.merge_requests.count
+          } }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d1f4bf53cdd5086b96151911d01eab3cd1ad6eb9..a26c309460e6a1f464b8317caed4591839f3cc4e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -32416,9 +32416,6 @@ msgid_plural "Milestones"
 msgstr[0] ""
 msgstr[1] ""
 
-msgid "Milestone actions"
-msgstr ""
-
 msgid "Milestone due date"
 msgstr ""
 
@@ -32443,12 +32440,6 @@ msgstr ""
 msgid "MilestoneCombobox|Select milestone"
 msgstr ""
 
-msgid "MilestonePage|Copy milestone ID"
-msgstr ""
-
-msgid "MilestonePage|Milestone ID: %{milestone_id}"
-msgstr ""
-
 msgid "MilestoneSidebar|Closed:"
 msgstr ""
 
@@ -32515,9 +32506,6 @@ msgstr ""
 msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle}. This milestone is not currently used in any issues or merge requests."
 msgstr ""
 
-msgid "Milestones|Close"
-msgstr ""
-
 msgid "Milestones|Completed Issues (closed)"
 msgstr ""
 
@@ -32557,9 +32545,6 @@ msgstr ""
 msgid "Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged."
 msgstr ""
 
-msgid "Milestones|Reopen"
-msgstr ""
-
 msgid "Milestones|There are no closed milestones"
 msgstr ""
 
@@ -32578,6 +32563,15 @@ msgstr ""
 msgid "Milestone|%{percentage}%{percent} complete"
 msgstr ""
 
+msgid "Milestone|Copy milestone ID: %{id}"
+msgstr ""
+
+msgid "Milestone|Milestone ID copied to clipboard."
+msgstr ""
+
+msgid "Milestone|Milestone actions"
+msgstr ""
+
 msgid "Min Value"
 msgstr ""
 
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 3f03f62604d4584e6c0082685ccbf0c00d78671f..6dcb5a55e687e0eef8f1a3748f80ef7d122538d8 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -70,7 +70,7 @@
       end
     end
 
-    context 'when milestones exists' do
+    context 'when milestones exists', :js do
       let_it_be(:other_project) { create(:project_empty_repo, group: group) }
 
       let_it_be(:active_project_milestone1) do
@@ -116,12 +116,42 @@
         end
 
         page.within('.detail-page-header') do
+          find_by_testid('milestone-more-actions-dropdown-toggle').click
           click_link('Edit')
         end
 
         expect(page).to have_selector('.milestone-form')
       end
 
+      it 'shows milestone id' do
+        page.within(".milestones #milestone_#{active_group_milestone.id}") do
+          click_link(active_group_milestone.title)
+        end
+
+        page.within('.detail-page-header') do
+          find_by_testid('milestone-more-actions-dropdown-toggle').click
+        end
+
+        expect(page).to have_selector('[data-testid="copy-milestone-id"]')
+        expect(page).to have_content("Copy milestone ID: #{active_group_milestone.id}")
+      end
+
+      it 'delete a milestone' do
+        page.within(".milestones #milestone_#{active_group_milestone.id}") do
+          click_link(active_group_milestone.title)
+        end
+
+        page.within('.detail-page-header') do
+          find_by_testid('milestone-more-actions-dropdown-toggle').click
+          click_button('Delete')
+        end
+
+        click_button('Delete milestone')
+
+        expect(page).to have_selector('.milestones')
+        expect(page).not_to have_selector(".milestones #milestone_#{active_group_milestone.id}")
+      end
+
       it 'renders milestones' do
         expect(page).to have_content('v1.0')
         expect(page).to have_content('v1.1')
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index b9f46f9831e28bdafc8a625138a50dd6c592025a..620e966938ed1fd619b9b2af87e85314e64ff65d 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -105,18 +105,18 @@
     end
   end
 
-  describe 'Deleting a milestone' do
+  describe 'Deleting a milestone', :js do
     it "the delete milestone button does not show for unauthorized users" do
       create(:milestone, project: project, title: 8.7)
       sign_out(user)
 
       visit group_milestones_path(group)
 
-      expect(page).to have_selector('.js-delete-milestone-button', count: 0)
+      expect(page).to have_selector('[data-testid="milestone-delete-item"]', count: 0)
     end
   end
 
-  describe 'reopen closed milestones' do
+  describe 'reopen closed milestones', :js do
     before do
       create(:milestone, :closed, project: project)
     end
@@ -125,6 +125,7 @@
       it 'reopens the milestone' do
         visit group_milestones_path(group, { state: 'closed' })
 
+        find_by_testid('milestone-more-actions-dropdown-toggle').click
         click_link 'Reopen'
 
         expect(page).not_to have_selector('.badge-danger')
@@ -132,10 +133,11 @@
       end
     end
 
-    describe 'project milestones page' do
+    describe 'project milestones page', :js do
       it 'reopens the milestone' do
         visit project_milestones_path(project, { state: 'closed' })
 
+        find_by_testid('milestone-more-actions-dropdown-toggle').click
         click_link 'Reopen'
 
         expect(page).not_to have_selector('.badge-danger')
diff --git a/spec/features/milestones/user_promotes_milestone_spec.rb b/spec/features/milestones/user_promotes_milestone_spec.rb
index 0eacf36cdde39b8ef226aa42fcfe6392aa30d12b..a0a5a90b11a8f51256f9baeb26cbbaaa3d056883 100644
--- a/spec/features/milestones/user_promotes_milestone_spec.rb
+++ b/spec/features/milestones/user_promotes_milestone_spec.rb
@@ -15,8 +15,10 @@
       visit(project_milestones_path(project))
     end
 
-    it "shows milestone promote button" do
-      expect(page).to have_selector('.js-promote-project-milestone-button')
+    it "shows milestone promote button", :js do
+      find_by_testid('milestone-more-actions-dropdown-toggle').click
+
+      expect(page).to have_selector('[data-testid="milestone-promote-item"]')
     end
   end
 
@@ -27,8 +29,10 @@
       visit(project_milestones_path(project))
     end
 
-    it "does not show milestone promote button" do
-      expect(page).not_to have_selector('.js-promote-project-milestone-button')
+    it "does not show milestone promote button", :js do
+      find_by_testid('milestone-more-actions-dropdown-toggle').click
+
+      expect(page).not_to have_selector('[data-testid="milestone-promote-item"]')
     end
   end
 end
diff --git a/spec/frontend/milestones/components/more_actions_dropdown_spec.js b/spec/frontend/milestones/components/more_actions_dropdown_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..592955126f1e5948723871716fa300da8836e42c
--- /dev/null
+++ b/spec/frontend/milestones/components/more_actions_dropdown_spec.js
@@ -0,0 +1,293 @@
+import { GlDisclosureDropdownItem, GlDisclosureDropdown } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import moreActionsDropdown from '~/milestones/components/more_actions_dropdown.vue';
+import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
+import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
+
+describe('moreActionsDropdown', () => {
+  let wrapper;
+  const defaultProvide = {
+    id: 1,
+    title: 'Milestone 1',
+    isActive: true,
+    showDelete: true,
+    canReadMilestone: true,
+    milestoneUrl: '/milestone-url',
+    editUrl: '/edit-url',
+    closeUrl: '/close-url',
+    reopenUrl: '/reopen-url',
+    promoteUrl: '/promote-url',
+    groupName: 'test-group',
+    issueCount: 1,
+    mergeRequestCount: 2,
+    isDetailPage: false,
+  };
+
+  const createComponent = ({ provideData = {}, propsData = {} } = {}) => {
+    wrapper = shallowMountExtended(moreActionsDropdown, {
+      directives: {
+        GlTooltip: createMockDirective('gl-tooltip'),
+      },
+      provide: {
+        ...defaultProvide,
+        ...provideData,
+      },
+      propsData,
+      stubs: {
+        GlDisclosureDropdownItem,
+        DeleteMilestoneModal,
+        PromoteMilestoneModal,
+      },
+    });
+  };
+
+  const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+  const showDropdown = () => {
+    findDropdown().vm.$emit('show');
+  };
+  const findDropdownTooltip = () => getBinding(findDropdown().element, 'gl-tooltip');
+  const findEditItem = () => wrapper.findByTestId('milestone-edit-item');
+  const findPromoteItem = () => wrapper.findByTestId('milestone-promote-item');
+  const findPromoteMilestoneModal = () => wrapper.findComponent(PromoteMilestoneModal);
+  const findCloseItem = () => wrapper.findByTestId('milestone-close-item');
+  const findReopenItem = () => wrapper.findByTestId('milestone-reopen-item');
+  const findDeleteItem = () => wrapper.findByTestId('milestone-delete-item');
+  const findMilestoneIdItem = () => wrapper.findByTestId('copy-milestone-id');
+  const findDeleteMilestoneModal = () => wrapper.findComponent(DeleteMilestoneModal);
+
+  describe('dropdown group', () => {
+    it('renders tooltip', () => {
+      createComponent();
+
+      expect(findDropdownTooltip().value).toBe('Milestone actions');
+    });
+  });
+
+  describe('edit item', () => {
+    it('renders with correct value if `editUrl` is set', () => {
+      const provideData = {
+        editUrl: '/my-edit-url',
+      };
+
+      createComponent({
+        provideData,
+      });
+
+      expect(findEditItem().attributes('href')).toBe(provideData.editUrl);
+    });
+
+    it('does not render if `editUrl` is false', () => {
+      createComponent({
+        provideData: {
+          editUrl: '',
+        },
+      });
+
+      expect(findEditItem().exists()).toBe(false);
+    });
+  });
+
+  describe('promote item', () => {
+    const provideData = {
+      promoteUrl: '/my-promote-url',
+      groupName: 'promote-group',
+      title: 'Milestone to promote',
+    };
+
+    it('renders with correct values if `promoteUrl` is set', () => {
+      createComponent({
+        provideData,
+      });
+
+      expect(findPromoteItem().exists()).toBe(true);
+      expect(findPromoteMilestoneModal().props()).toMatchObject({
+        visible: false,
+        milestoneTitle: provideData.title,
+        promoteUrl: provideData.promoteUrl,
+        groupName: provideData.groupName,
+      });
+    });
+
+    it('click on promote opens confirm modal with correct props', async () => {
+      createComponent({
+        provideData,
+      });
+
+      expect(findPromoteMilestoneModal().props('visible')).toBe(false);
+
+      findPromoteItem().trigger('click');
+      await nextTick();
+
+      expect(findPromoteMilestoneModal().props()).toMatchObject({
+        visible: true,
+        milestoneTitle: provideData.title,
+        promoteUrl: provideData.promoteUrl,
+        groupName: provideData.groupName,
+      });
+    });
+
+    it('does not render if `promoteUrl` is false', () => {
+      createComponent({
+        provideData: {
+          promoteUrl: '',
+        },
+      });
+
+      expect(findPromoteItem().exists()).toBe(false);
+    });
+  });
+
+  describe('close item', () => {
+    it('renders with correct values if `isActive` is set', () => {
+      const provideData = {
+        isActive: true,
+        closeUrl: '/my-close-url',
+      };
+
+      createComponent({
+        provideData,
+      });
+
+      expect(findCloseItem().exists()).toBe(true);
+      expect(findReopenItem().exists()).toBe(false);
+      expect(findCloseItem().attributes('href')).toBe(provideData.closeUrl);
+    });
+
+    it('does not render if `isActive` is false', () => {
+      createComponent({
+        provideData: {
+          isActive: false,
+        },
+      });
+
+      expect(findCloseItem().exists()).toBe(false);
+      expect(findReopenItem().exists()).toBe(true);
+    });
+
+    it('has correct class if `isDetailPage` is true', () => {
+      createComponent({
+        provideData: {
+          isDetailPage: true,
+        },
+      });
+
+      expect(findCloseItem().attributes('class')).toContain('gl-sm-display-none!');
+    });
+  });
+
+  describe('reopen item', () => {
+    it('renders with correct values if `isActive` is set', () => {
+      const provideData = {
+        isActive: false,
+        reopenUrl: '/my-reopen-url',
+      };
+
+      createComponent({
+        provideData,
+      });
+
+      expect(findReopenItem().exists()).toBe(true);
+      expect(findCloseItem().exists()).toBe(false);
+      expect(findReopenItem().attributes('href')).toBe(provideData.reopenUrl);
+    });
+
+    it('does not render if `isActive` is false', () => {
+      createComponent({
+        provideData: {
+          isActive: true,
+        },
+      });
+
+      expect(findReopenItem().exists()).toBe(false);
+      expect(findCloseItem().exists()).toBe(true);
+    });
+
+    it('has correct class if `isDetailPage` is true', () => {
+      createComponent({
+        provideData: {
+          isActive: false,
+          isDetailPage: true,
+        },
+      });
+
+      expect(findReopenItem().attributes('class')).toContain('gl-sm-display-none!');
+    });
+  });
+
+  describe('delete item', () => {
+    const provideData = {
+      issueCount: 1,
+      mergeRequestCount: 2,
+      milestoneId: 1,
+      milestoneTitle: 'Milestone 1',
+      milestoneUrl: '/milestone-url',
+    };
+
+    it('renders with correct values', () => {
+      createComponent();
+
+      expect(findDeleteItem().exists()).toBe(true);
+      expect(findDeleteMilestoneModal().props()).toMatchObject({
+        visible: false,
+        issueCount: provideData.issueCount,
+        mergeRequestCount: provideData.mergeRequestCount,
+        milestoneId: provideData.milestoneId,
+        milestoneTitle: provideData.milestoneTitle,
+        milestoneUrl: provideData.milestoneUrl,
+      });
+    });
+
+    it('click on delete opens confirm modal with correct props', async () => {
+      createComponent();
+
+      expect(findDeleteMilestoneModal().props('visible')).toBe(false);
+
+      findDeleteItem().trigger('click');
+      await nextTick();
+
+      expect(findDeleteMilestoneModal().props()).toMatchObject({
+        visible: true,
+        issueCount: provideData.issueCount,
+        mergeRequestCount: provideData.mergeRequestCount,
+        milestoneId: provideData.milestoneId,
+        milestoneTitle: provideData.milestoneTitle,
+        milestoneUrl: provideData.milestoneUrl,
+      });
+    });
+  });
+
+  describe('copy milestone id item', () => {
+    it('renders copy milestone id with correct id', () => {
+      createComponent({
+        provideData: {
+          id: 22,
+        },
+      });
+
+      showDropdown();
+
+      expect(findMilestoneIdItem().text()).toBe('Copy milestone ID: 22');
+    });
+
+    it('renders if `canReadMilestone` is true', () => {
+      createComponent({
+        provideData: {
+          canReadMilestone: true,
+        },
+      });
+      expect(findMilestoneIdItem().exists()).toBe(true);
+    });
+
+    it('does not render if `canReadMilestone` is false', () => {
+      createComponent({
+        provideData: {
+          canReadMilestone: false,
+        },
+      });
+
+      expect(findMilestoneIdItem().exists()).toBe(false);
+    });
+  });
+});
diff --git a/spec/frontend/milestones/components/promote_milestone_modal_spec.js b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
index e91e792afe827ec1b4bcf8ab3784d392922c55f3..f9130293d51afccad4f6d00720bde7c92c8bbaa7 100644
--- a/spec/frontend/milestones/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
@@ -1,6 +1,5 @@
 import { GlModal } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
-import { setHTMLFixture } from 'helpers/fixtures';
 import { TEST_HOST } from 'helpers/test_constants';
 import waitForPromises from 'helpers/wait_for_promises';
 import { createAlert } from '~/alert';
@@ -16,44 +15,29 @@ describe('Promote milestone modal', () => {
   let wrapper;
   const milestoneMockData = {
     milestoneTitle: 'v1.0',
-    url: `${TEST_HOST}/dummy/promote/milestones`,
+    promoteUrl: `${TEST_HOST}/dummy/promote/milestones`,
     groupName: 'group',
   };
 
-  const promoteButton = () => document.querySelector('.js-promote-project-milestone-button');
-
-  beforeEach(() => {
-    setHTMLFixture(`<button
-      class="js-promote-project-milestone-button"
-      data-group-name="${milestoneMockData.groupName}"
-      data-milestone-title="${milestoneMockData.milestoneTitle}"
-      data-url="${milestoneMockData.url}">
-      Promote
-      </button>`);
-    wrapper = shallowMount(PromoteMilestoneModal);
-  });
-
-  describe('Modal opener button', () => {
-    it('button gets disabled when the modal opens', () => {
-      expect(promoteButton().disabled).toBe(false);
-
-      promoteButton().click();
-
-      expect(promoteButton().disabled).toBe(true);
-    });
-
-    it('button gets enabled when the modal closes', () => {
-      promoteButton().click();
-
-      wrapper.findComponent(GlModal).vm.$emit('hide');
-
-      expect(promoteButton().disabled).toBe(false);
+  const createComponent = ({ propsData = {} } = {}) => {
+    wrapper = shallowMount(PromoteMilestoneModal, {
+      propsData,
+      stubs: {
+        PromoteMilestoneModal,
+      },
     });
-  });
+  };
 
   describe('Modal title and description', () => {
     beforeEach(() => {
-      promoteButton().click();
+      createComponent({
+        propsData: {
+          visible: true,
+          milestoneTitle: milestoneMockData.milestoneTitle,
+          promoteUrl: milestoneMockData.promoteUrl,
+          groupName: milestoneMockData.groupName,
+        },
+      });
     });
 
     it('contains the proper description', () => {
@@ -63,19 +47,28 @@ describe('Promote milestone modal', () => {
     });
 
     it('contains the correct title', () => {
-      expect(wrapper.vm.title).toBe('Promote v1.0 to group milestone?');
+      expect(wrapper.vm.title).toBe(
+        `Promote ${milestoneMockData.milestoneTitle} to group milestone?`,
+      );
     });
   });
 
   describe('When requesting a milestone promotion', () => {
     beforeEach(() => {
-      promoteButton().click();
+      createComponent({
+        propsData: {
+          visible: true,
+          milestoneTitle: milestoneMockData.milestoneTitle,
+          promoteUrl: milestoneMockData.promoteUrl,
+          groupName: milestoneMockData.groupName,
+        },
+      });
     });
 
     it('redirects when a milestone is promoted', async () => {
       const responseURL = `${TEST_HOST}/dummy/endpoint`;
       jest.spyOn(axios, 'post').mockImplementation((url) => {
-        expect(url).toBe(milestoneMockData.url);
+        expect(url).toBe(milestoneMockData.promoteUrl);
         return Promise.resolve({
           data: {
             url: responseURL,
@@ -93,7 +86,7 @@ describe('Promote milestone modal', () => {
       const dummyError = new Error('promoting milestone failed');
       dummyError.response = { status: HTTP_STATUS_INTERNAL_SERVER_ERROR };
       jest.spyOn(axios, 'post').mockImplementation((url) => {
-        expect(url).toBe(milestoneMockData.url);
+        expect(url).toBe(milestoneMockData.promoteUrl);
         return Promise.reject(dummyError);
       });