diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index a2448520a5f6695b24944cd40466f98a776c7159..e7495677e7c948286dc4b5d5101127019fba2694 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -2,6 +2,7 @@
 import playIconSvg from 'icons/_icon_play.svg';
 import eventHub from '../event_hub';
 import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
 
 export default {
   props: {
@@ -12,6 +13,10 @@ export default {
     },
   },
 
+  directives: {
+    tooltip,
+  },
+
   components: {
     loadingIcon,
   },
@@ -33,8 +38,6 @@ export default {
     onClickAction(endpoint) {
       this.isLoading = true;
 
-      $(this.$refs.tooltip).tooltip('destroy');
-
       eventHub.$emit('postAction', endpoint);
     },
 
@@ -53,11 +56,11 @@ export default {
     class="btn-group"
     role="group">
     <button
+      v-tooltip
       type="button"
-      class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
+      class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
       data-container="body"
       data-toggle="dropdown"
-      ref="tooltip"
       :title="title"
       :aria-label="title"
       :disabled="isLoading">
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index eaeec2bc53c60f043b193f15853345fa159a8fbe..6b749814ea42a1134b4782c0c79894a77f5d4dda 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,4 +1,6 @@
 <script>
+import tooltip from '../../vue_shared/directives/tooltip';
+
 /**
  * Renders the external url link in environments table.
  */
@@ -10,6 +12,10 @@ export default {
     },
   },
 
+  directives: {
+    tooltip,
+  },
+
   computed: {
     title() {
       return 'Open';
@@ -19,7 +25,8 @@ export default {
 </script>
 <template>
   <a
-    class="btn external-url has-tooltip"
+    v-tooltip
+    class="btn external-url"
     data-container="body"
     target="_blank"
     rel="noopener noreferrer nofollow"
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 07cf92281a0f9649c9990e2ba0e281f0267b9d69..1655561cdd31b8394134c3d74c240345b387e08e 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,6 +2,8 @@
 /**
  * Renders the Monitoring (Metrics) link in environments table.
  */
+import tooltip from '../../vue_shared/directives/tooltip';
+
 export default {
   props: {
     monitoringUrl: {
@@ -10,6 +12,10 @@ export default {
     },
   },
 
+  directives: {
+    tooltip,
+  },
+
   computed: {
     title() {
       return 'Monitoring';
@@ -19,7 +25,8 @@ export default {
 </script>
 <template>
   <a
-    class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
+    v-tooltip
+    class="btn monitoring-url hidden-xs hidden-sm"
     data-container="body"
     rel="noopener noreferrer nofollow"
     :href="monitoringUrl"
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index 091c543860b081b0033744694296caaac36cfca6..85f11d2071b9fd2f0558b39c8f6aaefb2577bf89 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -5,6 +5,7 @@
  */
 import eventHub from '../event_hub';
 import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
 
 export default {
   props: {
@@ -14,6 +15,10 @@ export default {
     },
   },
 
+  directives: {
+    tooltip,
+  },
+
   data() {
     return {
       isLoading: false,
@@ -46,8 +51,9 @@ export default {
 </script>
 <template>
   <button
+    v-tooltip
     type="button"
-    class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
+    class="btn stop-env-link hidden-xs hidden-sm"
     data-container="body"
     @click="onClick"
     :disabled="isLoading"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 1ca65a799515f158a50bb92118ed846cac9e7c8a..2037bf618e38acf6676e745b2cee9da1c0143a8e 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -4,6 +4,7 @@
  * Used in environments table.
  */
 import terminalIconSvg from 'icons/_icon_terminal.svg';
+import tooltip from '../../vue_shared/directives/tooltip';
 
 export default {
   props: {
@@ -14,6 +15,10 @@ export default {
     },
   },
 
+  directives: {
+    tooltip,
+  },
+
   data() {
     return {
       terminalIconSvg,
@@ -29,7 +34,8 @@ export default {
 </script>
 <template>
   <a
-    class="btn terminal-button has-tooltip hidden-xs hidden-sm"
+    v-tooltip
+    class="btn terminal-button hidden-xs hidden-sm"
     data-container="body"
     :title="title"
     :aria-label="title"
diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue
index f811fb0de24e2d9e0ace87463c4f01b63ca20e17..7bf2be8b28a64a368adc30a2e179ce4799930a5b 100644
--- a/app/assets/javascripts/issue_show/components/fields/project_move.vue
+++ b/app/assets/javascripts/issue_show/components/fields/project_move.vue
@@ -1,10 +1,10 @@
 <script>
-  import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+  import tooltip from '../../../vue_shared/directives/tooltip';
 
   export default {
-    mixins: [
-      tooltipMixin,
-    ],
+    directives: {
+      tooltip,
+    },
     props: {
       formState: {
         type: Object,
@@ -71,9 +71,9 @@
         data-placeholder="Move to a different project" />
     </div>
     <span
+      v-tooltip
       data-placement="auto top"
-      title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
-      ref="tooltip">
+      title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
       <i
         class="fa fa-question-circle"
         aria-hidden="true">
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
index abcd0c4ecea44328e6156a841a692abe218a7269..16cc0761fc17186d6611e34194dee1c1458ebe2d 100644
--- a/app/assets/javascripts/pipelines/components/async_button.vue
+++ b/app/assets/javascripts/pipelines/components/async_button.vue
@@ -3,7 +3,7 @@
 
 import eventHub from '../event_hub';
 import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import tooltipMixin from '../../vue_shared/mixins/tooltip';
+import tooltip from '../../vue_shared/directives/tooltip';
 
 export default {
   props: {
@@ -28,12 +28,12 @@ export default {
       required: false,
     },
   },
+  directives: {
+    tooltip,
+  },
   components: {
     loadingIcon,
   },
-  mixins: [
-    tooltipMixin,
-  ],
   data() {
     return {
       isLoading: false,
@@ -58,7 +58,6 @@ export default {
     makeRequest() {
       this.isLoading = true;
 
-      $(this.$refs.tooltip).tooltip('destroy');
       eventHub.$emit('postAction', this.endpoint);
     },
   },
@@ -67,6 +66,7 @@ export default {
 
 <template>
   <button
+    v-tooltip
     type="button"
     @click="onClick"
     :class="buttonClass"
@@ -74,7 +74,6 @@ export default {
     :aria-label="title"
     data-container="body"
     data-placement="top"
-    ref="tooltip"
     :disabled="isLoading">
     <i
       :class="iconClass"
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 1f9e3d3977938ede3ac250cd41718c16b0432f03..54227425d2a0652fbff67c6336edd6919ce18603 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,6 +1,6 @@
 <script>
   import getActionIcon from '../../../vue_shared/ci_action_icons';
-  import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+  import tooltip from '../../../vue_shared/directives/tooltip';
 
   /**
    * Renders either a cancel, retry or play icon pointing to the given path.
@@ -29,9 +29,9 @@
       },
     },
 
-    mixins: [
-      tooltipMixin,
-    ],
+    directives: {
+      tooltip,
+    },
 
     computed: {
       actionIconSvg() {
@@ -46,12 +46,11 @@
 </script>
 <template>
   <a
+    v-tooltip
     :data-method="actionMethod"
     :title="tooltipText"
     :href="link"
-    ref="tooltip"
     class="ci-action-icon-container"
-    data-toggle="tooltip"
     data-container="body">
 
     <i
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
index 19cafff4e1c6e098f6d0ac6be9eb9a68625d9d5c..18fe1847eef0b52aab6e6f7cb82b9ca512742940 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
@@ -1,6 +1,6 @@
 <script>
   import getActionIcon from '../../../vue_shared/ci_action_icons';
-  import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+  import tooltip from '../../../vue_shared/directives/tooltip';
 
   /**
    * Renders either a cancel, retry or play icon pointing to the given path.
@@ -29,9 +29,9 @@
       },
     },
 
-    mixins: [
-      tooltipMixin,
-    ],
+    directives: {
+      tooltip,
+    },
 
     computed: {
       actionIconSvg() {
@@ -42,13 +42,12 @@
 </script>
 <template>
   <a
+    v-tooltip
     :data-method="actionMethod"
     :title="tooltipText"
     :href="link"
-    ref="tooltip"
     rel="nofollow"
     class="ci-action-icon-wrapper js-ci-status-icon"
-    data-toggle="tooltip"
     data-container="body"
     v-html="actionIconSvg"
     aria-label="Job's action">
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index d597af8dfb5f8f4ad9e03b7635a29c7ba97ec5ab..2944689a5a723f46a0e3a0a62cfe0f6d7cee0954 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -1,7 +1,7 @@
 <script>
   import jobNameComponent from './job_name_component.vue';
   import jobComponent from './job_component.vue';
-  import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+  import tooltip from '../../../vue_shared/directives/tooltip';
 
   /**
    * Renders the dropdown for the pipeline graph.
@@ -34,9 +34,9 @@
       },
     },
 
-    mixins: [
-      tooltipMixin,
-    ],
+    directives: {
+      tooltip,
+    },
 
     components: {
       jobComponent,
@@ -53,12 +53,12 @@
 <template>
   <div>
     <button
+      v-tooltip
       type="button"
       data-toggle="dropdown"
       data-container="body"
       class="dropdown-menu-toggle build-content"
-      :title="tooltipText"
-      ref="tooltip">
+      :title="tooltipText">
 
       <job-name-component
         :name="job.name"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index b39c936101e3f989de65a8e78a808b1c573bcc06..1f5ed3f1074d8a6c11238257c86af464e63cfd65 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -2,7 +2,7 @@
   import actionComponent from './action_component.vue';
   import dropdownActionComponent from './dropdown_action_component.vue';
   import jobNameComponent from './job_name_component.vue';
-  import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+  import tooltip from '../../../vue_shared/directives/tooltip';
 
   /**
    * Renders the badge for the pipeline graph and the job's dropdown.
@@ -54,9 +54,9 @@
       jobNameComponent,
     },
 
-    mixins: [
-      tooltipMixin,
-    ],
+    directives: {
+      tooltip,
+    },
 
     computed: {
       tooltipText() {
@@ -77,12 +77,11 @@
 <template>
   <div>
     <a
+      v-tooltip
       v-if="job.status.details_path"
       :href="job.status.details_path"
       :title="tooltipText"
       :class="cssClassJobName"
-      ref="tooltip"
-      data-toggle="tooltip"
       data-container="body">
 
       <job-name-component
@@ -93,10 +92,9 @@
 
     <div
       v-else
+      v-tooltip
       :title="tooltipText"
       :class="cssClassJobName"
-      ref="tooltip"
-      data-toggle="tooltip"
       data-container="body">
 
       <job-name-component
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 8333ec0fbc3563bc12418d94d0ec0e30e29a9edb..2ca5ac2912f544a3600dba358513db1f5097b074 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -1,6 +1,6 @@
 <script>
 import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import tooltipMixin from '../../vue_shared/mixins/tooltip';
+import tooltip from '../../vue_shared/directives/tooltip';
 
 export default {
   props: {
@@ -12,9 +12,9 @@ export default {
   components: {
     userAvatarLink,
   },
-  mixins: [
-    tooltipMixin,
-  ],
+  directives: {
+    tooltip,
+  },
   computed: {
     user() {
       return this.pipeline.user;
@@ -45,16 +45,16 @@ export default {
     <div class="label-container">
       <span
         v-if="pipeline.flags.latest"
+        v-tooltip
         class="js-pipeline-url-latest label label-success"
-        title="Latest pipeline for this branch"
-        ref="tooltip">
+        title="Latest pipeline for this branch">
         latest
       </span>
       <span
         v-if="pipeline.flags.yaml_errors"
+        v-tooltip
         class="js-pipeline-url-yaml label label-danger"
-        :title="pipeline.yaml_errors"
-        ref="tooltip">
+        :title="pipeline.yaml_errors">
         yaml invalid
       </span>
       <span
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index a6fc4f042373ce58df43dd485aeee2c2902874d0..01dfe51cc17de4af1cf6bd296410def04bc44396 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -4,6 +4,7 @@
   import playIconSvg from 'icons/_icon_play.svg';
   import eventHub from '../event_hub';
   import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+  import tooltip from '../../vue_shared/directives/tooltip';
 
   export default {
     props: {
@@ -12,6 +13,9 @@
         required: true,
       },
     },
+    directives: {
+      tooltip,
+    },
     components: {
       loadingIcon,
     },
@@ -25,8 +29,6 @@
       onClickAction(endpoint) {
         this.isLoading = true;
 
-        $(this.$refs.tooltip).tooltip('destroy');
-
         eventHub.$emit('postAction', endpoint);
       },
 
@@ -43,13 +45,13 @@
 <template>
   <div class="btn-group">
     <button
+      v-tooltip
       type="button"
-      class="dropdown-new btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+      class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
       title="Manual job"
       data-toggle="dropdown"
       data-placement="top"
       aria-label="Manual job"
-      ref="tooltip"
       :disabled="isLoading">
       <span v-html="playIconSvg"></span>
       <i
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index b4520481cdc8d42ee9d872ae9e8eaa78107cf25b..b19bd509a00d6248ce70b1ada4555a8ab9d30f22 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -1,5 +1,5 @@
 <script>
-  import tooltipMixin from '../../vue_shared/mixins/tooltip';
+  import tooltip from '../../vue_shared/directives/tooltip';
 
   export default {
     props: {
@@ -8,9 +8,9 @@
         required: true,
       },
     },
-    mixins: [
-      tooltipMixin,
-    ],
+    directives: {
+      tooltip,
+    },
   };
 </script>
 <template>
@@ -18,12 +18,12 @@
     class="btn-group"
     role="group">
     <button
+      v-tooltip
       class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
       title="Artifacts"
       data-placement="top"
       data-toggle="dropdown"
-      aria-label="Artifacts"
-      ref="tooltip">
+      aria-label="Artifacts">
       <i
         class="fa fa-download"
         aria-hidden="true">
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index c05c76c9a644e0fcb93b4d549ebf216dd80d6bdc..e98f35bb58c759c59af7fa88121efc745443dd8c 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -16,7 +16,7 @@
 /* global Flash */
 import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
 import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import tooltipMixin from '../../vue_shared/mixins/tooltip';
+import tooltip from '../../vue_shared/directives/tooltip';
 
 export default {
   props: {
@@ -32,9 +32,9 @@ export default {
     },
   },
 
-  mixins: [
-    tooltipMixin,
-  ],
+  directives: {
+    tooltip,
+  },
 
   data() {
     return {
@@ -132,7 +132,7 @@ export default {
 <template>
   <div class="dropdown">
     <button
-      ref="tooltip"
+      v-tooltip
       :class="triggerButtonClass"
       @click="onClickStage"
       class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue
index be3f32afa09eca71daee06456e0e6ae7a9dbe298..037684b4e7223a5e2641707d8283cb9afb889a6f 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/time_ago.vue
@@ -1,7 +1,7 @@
 <script>
   import iconTimerSvg from 'icons/_icon_timer.svg';
   import '../../lib/utils/datetime_utility';
-  import tooltipMixin from '../../vue_shared/mixins/tooltip';
+  import tooltip from '../../vue_shared/directives/tooltip';
   import timeagoMixin from '../../vue_shared/mixins/timeago';
 
   export default {
@@ -16,9 +16,11 @@
       },
     },
     mixins: [
-      tooltipMixin,
       timeagoMixin,
     ],
+    directives: {
+      tooltip,
+    },
     data() {
       return {
         iconTimerSvg,
@@ -81,7 +83,7 @@
         </i>
 
         <time
-          ref="tooltip"
+          v-tooltip
           data-placement="top"
           data-container="body"
           :title="tooltipTitle(finishedTime)">
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 1d4d90f75b688ca9e061beceacaea23d1f285d55..bdc059f4a03c9d0b44d04ad6ed5fc2fd74396b83 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -2,7 +2,7 @@
 import ciIconBadge from './ci_badge_link.vue';
 import loadingIcon from './loading_icon.vue';
 import timeagoTooltip from './time_ago_tooltip.vue';
-import tooltipMixin from '../mixins/tooltip';
+import tooltip from '../directives/tooltip';
 import userAvatarImage from './user_avatar/user_avatar_image.vue';
 
 /**
@@ -47,9 +47,9 @@ export default {
     },
   },
 
-  mixins: [
-    tooltipMixin,
-  ],
+  directives: {
+    tooltip,
+  },
 
   components: {
     ciIconBadge,
@@ -90,10 +90,10 @@ export default {
 
       <template v-if="user">
         <a
+          v-tooltip
           :href="user.path"
           :title="user.email"
-          class="js-user-link commit-committer-link"
-          ref="tooltip">
+          class="js-user-link commit-committer-link">
 
           <user-avatar-image
             :img-src="user.avatar_url"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 1a11f493b7fe8c5c8bacec6084249b3af0aa64ae..5bf2a90cc3ba985f095baff3d7abf145bb18ca0c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,17 +1,17 @@
 <script>
-  import tooltipMixin from '../../mixins/tooltip';
+  import tooltip from '../../directives/tooltip';
   import toolbarButton from './toolbar_button.vue';
 
   export default {
-    mixins: [
-      tooltipMixin,
-    ],
     props: {
       previewMarkdown: {
         type: Boolean,
         required: true,
       },
     },
+    directives: {
+      tooltip,
+    },
     components: {
       toolbarButton,
     },
@@ -94,13 +94,13 @@
         </div>
         <div class="toolbar-group">
           <button
+            v-tooltip
             aria-label="Go full screen"
             class="toolbar-btn js-zen-enter"
             data-container="body"
             tabindex="-1"
             title="Go full screen"
-            type="button"
-            ref="tooltip">
+            type="button">
             <i
               aria-hidden="true"
               class="fa fa-arrows-alt fa-fw">
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 096be50762572c1c265bdc8fb727126a96e06872..f7da7ebfcfe07737901d16add34dbe4377565a87 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,10 +1,7 @@
 <script>
-  import tooltipMixin from '../../mixins/tooltip';
+  import tooltip from '../../directives/tooltip';
 
   export default {
-    mixins: [
-      tooltipMixin,
-    ],
     props: {
       buttonTitle: {
         type: String,
@@ -29,6 +26,9 @@
         default: false,
       },
     },
+    directives: {
+      tooltip,
+    },
     computed: {
       iconClass() {
         return `fa-${this.icon}`;
@@ -39,10 +39,10 @@
 
 <template>
   <button
+    v-tooltip
     type="button"
     class="toolbar-btn js-md hidden-xs"
     tabindex="-1"
-    ref="tooltip"
     data-container="body"
     :data-md-tag="tag"
     :data-md-block="tagBlock"
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index 1c6ef071a6d2d3bc0a5df213e97708fd4c2e7248..3ff7f6e2c4e63740e38f9a1992982243c858892c 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -1,5 +1,5 @@
 <script>
-import tooltipMixin from '../mixins/tooltip';
+import tooltip from '../directives/tooltip';
 import timeagoMixin from '../mixins/timeago';
 import '../../lib/utils/datetime_utility';
 
@@ -28,19 +28,21 @@ export default {
   },
 
   mixins: [
-    tooltipMixin,
     timeagoMixin,
   ],
+
+  directives: {
+    tooltip,
+  },
 };
 </script>
 <template>
   <time
+    v-tooltip
     :class="cssClass"
-    class="js-vue-timeago"
     :title="tooltipTitle(time)"
     :data-placement="tooltipPlacement"
-    data-container="body"
-    ref="tooltip">
+    data-container="body">
     {{timeFormated(time)}}
   </time>
 </template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index cd6f8c7aee46148df0a6f479ba76b43dde10fb41..dd9a2ebb184f1904e6e1164fce4133ff6688b5f7 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -16,11 +16,10 @@
 */
 
 import defaultAvatarUrl from 'images/no_avatar.png';
-import TooltipMixin from '../../mixins/tooltip';
+import tooltip from '../../directives/tooltip';
 
 export default {
   name: 'UserAvatarImage',
-  mixins: [TooltipMixin],
   props: {
     imgSrc: {
       type: String,
@@ -53,6 +52,9 @@ export default {
       default: 'top',
     },
   },
+  directives: {
+    tooltip,
+  },
   computed: {
     tooltipContainer() {
       return this.tooltipText ? 'body' : null;
@@ -72,6 +74,7 @@ export default {
 
 <template>
   <img
+    v-tooltip
     class="avatar"
     :class="[avatarSizeClass, cssClasses]"
     :src="imageSource"
@@ -81,6 +84,5 @@ export default {
     :data-container="tooltipContainer"
     :data-placement="tooltipPlacement"
     :title="tooltipText"
-    ref="tooltip"
   />
 </template>
diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc896cf5c7ddff2dfff43e08e7d20c9ac80274df
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -0,0 +1,13 @@
+export default {
+  bind(el) {
+    $(el).tooltip();
+  },
+
+  componentUpdated(el) {
+    $(el).tooltip('fixTitle');
+  },
+
+  unbind(el) {
+    $(el).tooltip('destroy');
+  },
+};
diff --git a/app/assets/javascripts/vue_shared/mixins/tooltip.js b/app/assets/javascripts/vue_shared/mixins/tooltip.js
deleted file mode 100644
index 995c0c985051c15128c7835eb5de25315d0c2ca0..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_shared/mixins/tooltip.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default {
-  mounted() {
-    $(this.$refs.tooltip).tooltip();
-  },
-
-  updated() {
-    $(this.$refs.tooltip).tooltip('fixTitle');
-  },
-
-  beforeDestroy() {
-    $(this.$refs.tooltip).tooltip('destroy');
-  },
-};
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index d2d895172410fa41d926e4e3bc7cdb4a3601fb0c..ae844fa105161d4b1cc90ec8579cbc917d77f086 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -463,20 +463,24 @@ A forEach will cause side effects, it will be mutating the array being iterated.
   1. `destroyed`
 
 #### Vue and Boostrap
-1. Tooltips: Do not rely on `has-tooltip` class name for vue components
+1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
   ```javascript
     // bad
-    <span class="has-tooltip">
+    <span
+      class="has-tooltip"
+      title="Some tooltip text">
       Text
     </span>
 
     // good
-    <span data-toggle="tooltip">
+    <span
+      v-tooltip
+      title="Some tooltip text">
       Text
     </span>
   ```
 
-1. Tooltips: When using a tooltip, include the tooltip mixin
+1. Tooltips: When using a tooltip, include the tooltip directive, `./app/assets/javascripts/vue_shared/directives/tooltip.js`
 
 1. Don't change `data-original-title`.
   ```javascript
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
index 596d812c724f7e22916d7da411d116246ad3fcdf..ea40a1fcd4be43b0e858ea49257f330a894ead7c 100644
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -32,9 +32,16 @@ describe('Actions Component', () => {
     }).$mount();
   });
 
+  describe('computed', () => {
+    it('title', () => {
+      expect(component.title).toEqual('Deploy to...');
+    });
+  });
+
   it('should render a dropdown button with icon and title attribute', () => {
     expect(component.$el.querySelector('.fa-caret-down')).toBeDefined();
-    expect(component.$el.querySelector('.dropdown-new').getAttribute('title')).toEqual('Deploy to...');
+    expect(component.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual('Deploy to...');
+    expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual('Deploy to...');
   });
 
   it('should render a dropdown with the provided list of actions', () => {
diff --git a/spec/javascripts/environments/environment_monitoring_spec.js b/spec/javascripts/environments/environment_monitoring_spec.js
index 0f3dba662303a8b2cc801e8a0fe972fd24f6b455..f8d8223967ae82f7be2b8d28c3143cacc92bc6c0 100644
--- a/spec/javascripts/environments/environment_monitoring_spec.js
+++ b/spec/javascripts/environments/environment_monitoring_spec.js
@@ -3,21 +3,30 @@ import monitoringComp from '~/environments/components/environment_monitoring.vue
 
 describe('Monitoring Component', () => {
   let MonitoringComponent;
+  let component;
+
+  const monitoringUrl = 'https://gitlab.com';
 
   beforeEach(() => {
     MonitoringComponent = Vue.extend(monitoringComp);
-  });
 
-  it('should render a link to environment monitoring page', () => {
-    const monitoringUrl = 'https://gitlab.com';
-    const component = new MonitoringComponent({
+    component = new MonitoringComponent({
       propsData: {
         monitoringUrl,
       },
     }).$mount();
+  });
 
+  describe('computed', () => {
+    it('title', () => {
+      expect(component.title).toEqual('Monitoring');
+    });
+  });
+
+  it('should render a link to environment monitoring page', () => {
     expect(component.$el.getAttribute('href')).toEqual(monitoringUrl);
     expect(component.$el.querySelector('.fa-area-chart')).toBeDefined();
-    expect(component.$el.getAttribute('title')).toEqual('Monitoring');
+    expect(component.$el.getAttribute('data-original-title')).toEqual('Monitoring');
+    expect(component.$el.getAttribute('aria-label')).toEqual('Monitoring');
   });
 });
diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js
index 8131f1e5b116198603dbe43ac259a0df2ff514c2..3f95faf466abe66dfadcdb29b0b4954ad27dcc5b 100644
--- a/spec/javascripts/environments/environment_stop_spec.js
+++ b/spec/javascripts/environments/environment_stop_spec.js
@@ -17,8 +17,15 @@ describe('Stop Component', () => {
     }).$mount();
   });
 
+  describe('computed', () => {
+    it('title', () => {
+      expect(component.title).toEqual('Stop');
+    });
+  });
+
   it('should render a button to stop the environment', () => {
     expect(component.$el.tagName).toEqual('BUTTON');
-    expect(component.$el.getAttribute('title')).toEqual('Stop');
+    expect(component.$el.getAttribute('data-original-title')).toEqual('Stop');
+    expect(component.$el.getAttribute('aria-label')).toEqual('Stop');
   });
 });
diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js
index 858472af4b615dbcddda82a94acd31ae59d49b8a..f1576b19d1bf59341267a70d689acdff204394e1 100644
--- a/spec/javascripts/environments/environment_terminal_button_spec.js
+++ b/spec/javascripts/environments/environment_terminal_button_spec.js
@@ -16,9 +16,16 @@ describe('Stop Component', () => {
     }).$mount();
   });
 
+  describe('computed', () => {
+    it('title', () => {
+      expect(component.title).toEqual('Terminal');
+    });
+  });
+
   it('should render a link to open a web terminal with the provided path', () => {
     expect(component.$el.tagName).toEqual('A');
-    expect(component.$el.getAttribute('title')).toEqual('Terminal');
+    expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal');
+    expect(component.$el.getAttribute('aria-label')).toEqual('Terminal');
     expect(component.$el.getAttribute('href')).toEqual(terminalPath);
   });
 });
diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
index f3b4adc0b702f79e6465fcdcd783c5e4ce715a32..b4c1f70ed1e77bd7627c1663c884a9471ebf93d0 100644
--- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
@@ -22,7 +22,6 @@ describe('Time ago with tooltip component', () => {
     }).$mount();
 
     expect(vm.$el.tagName).toEqual('TIME');
-    expect(vm.$el.classList.contains('js-vue-timeago')).toEqual(true);
     expect(
       vm.$el.getAttribute('data-original-title'),
     ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z'));
diff --git a/spec/javascripts/vue_shared/directives/tooltip_spec.js b/spec/javascripts/vue_shared/directives/tooltip_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b1b3071527b2240268543df8c16c7e06b8b54da2
--- /dev/null
+++ b/spec/javascripts/vue_shared/directives/tooltip_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+describe('Tooltip directive', () => {
+  let vm;
+
+  afterEach(() => {
+    if (vm) {
+      vm.$destroy();
+    }
+  });
+
+  describe('with a single tooltip', () => {
+    beforeEach(() => {
+      const SomeComponent = Vue.extend({
+        directives: {
+          tooltip,
+        },
+        template: `
+          <div
+            v-tooltip
+            title="foo">
+          </div>
+        `,
+      });
+
+      vm = new SomeComponent().$mount();
+    });
+
+    it('should have tooltip plugin applied', () => {
+      expect($(vm.$el).data('bs.tooltip')).toBeDefined();
+    });
+  });
+
+  describe('with multiple tooltips', () => {
+    beforeEach(() => {
+      const SomeComponent = Vue.extend({
+        directives: {
+          tooltip,
+        },
+        template: `
+          <div>
+            <div
+              v-tooltip
+              class="js-look-for-tooltip"
+              title="foo">
+            </div>
+            <div
+              v-tooltip
+              title="bar">
+            </div>
+          </div>
+        `,
+      });
+
+      vm = new SomeComponent().$mount();
+    });
+
+    it('should have tooltip plugin applied to all instances', () => {
+      expect($(vm.$el).find('.js-look-for-tooltip').data('bs.tooltip')).toBeDefined();
+    });
+  });
+});