diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 0b4dfb6499283c05812f18ce5395134b449278a7..5d23a6b54e1fd424edca93d21bbc6064bd80a1ad 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,20 +1,26 @@
 <script>
-import $ from 'jquery';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import { sprintf, __ } from '~/locale';
 import Icon from '~/vue_shared/components/icon.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
 import IssueCardWeight from 'ee/boards/components/issue_card_weight.vue';
 import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
 import eventHub from '../eventhub';
-import tooltip from '../../vue_shared/directives/tooltip';
+import IssueDueDate from './issue_due_date.vue';
+import IssueTimeEstimate from './issue_time_estimate.vue';
 import boardsStore from '../stores/boards_store';
 
 export default {
   components: {
-    UserAvatarLink,
     Icon,
+    UserAvatarLink,
+    TooltipOnTruncate,
+    IssueDueDate,
     IssueCardWeight,
+    IssueTimeEstimate,
   },
   directives: {
-    tooltip,
+    GlTooltip: GlTooltipDirective,
   },
   props: {
     issue: {
@@ -47,8 +53,8 @@ export default {
   },
   data() {
     return {
-      limitBeforeCounter: 3,
-      maxRender: 4,
+      limitBeforeCounter: 2,
+      maxRender: 3,
       maxCounter: 99,
     };
   },
@@ -57,7 +63,9 @@ export default {
       return this.issue.assignees.length - this.limitBeforeCounter;
     },
     assigneeCounterTooltip() {
-      return `${this.assigneeCounterLabel} more`;
+      const { numberOverLimit, maxCounter } = this;
+      const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
+      return sprintf(__('%{count} more assignees'), { count });
     },
     assigneeCounterLabel() {
       if (this.numberOverLimit > this.maxCounter) {
@@ -82,6 +90,10 @@ export default {
     showLabelFooter() {
       return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
     },
+    issueReferencePath() {
+      const { referencePath, groupId } = this.issue;
+      return !groupId ? referencePath.split('#')[0] : null;
+    },
   },
   methods: {
     isIndexLessThanlimit(index) {
@@ -98,11 +110,9 @@ export default {
       return index < this.limitBeforeCounter;
     },
     assigneeUrl(assignee) {
+      if (!assignee) return '';
       return `${this.rootPath}${assignee.username}`;
     },
-    assigneeUrlTitle(assignee) {
-      return `Assigned to ${assignee.name}`;
-    },
     avatarUrlTitle(assignee) {
       return `Avatar for ${assignee.name}`;
     },
@@ -110,19 +120,29 @@ export default {
       if (!label.id) return false;
       return true;
     },
-    filterByLabel(label, e) {
+    filterByLabel(label) {
+      if (!this.updateFilters) return;
+      const labelTitle = encodeURIComponent(label.title);
+      const filter = `label_name[]=${labelTitle}`;
+
+      this.applyFilter(filter);
+    },
+    filterByWeight(weight) {
       if (!this.updateFilters) return;
 
+      const issueWeight = encodeURIComponent(weight);
+      const filter = `weight=${issueWeight}`;
+
+      this.applyFilter(filter);
+    },
+    applyFilter(filter) {
       const filterPath = boardsStore.filter.path.split('&');
-      const labelTitle = encodeURIComponent(label.title);
-      const param = `label_name[]=${labelTitle}`;
-      const labelIndex = filterPath.indexOf(param);
-      $(e.currentTarget).tooltip('hide');
+      const filterIndex = filterPath.indexOf(filter);
 
-      if (labelIndex === -1) {
-        filterPath.push(param);
+      if (filterIndex === -1) {
+        filterPath.push(filter);
       } else {
-        filterPath.splice(labelIndex, 1);
+        filterPath.splice(filterIndex, 1);
       }
 
       boardsStore.filter.path = filterPath.join('&');
@@ -143,28 +163,66 @@ export default {
 <template>
   <div>
     <div class="board-card-header">
-      <h4 class="board-card-title">
+      <h4 class="board-card-title append-bottom-0 prepend-top-0">
         <icon
           v-if="issue.confidential"
+          v-gl-tooltip
           name="eye-slash"
-          class="confidential-icon"
-        />
-        <a
+          :title="__('Confidential')"
+          class="confidential-icon append-right-4"
+          :aria-label="__('Confidential')"
+        /><a
           :href="issue.path"
           :title="issue.title"
           class="js-no-trigger"
           @mousemove.stop>{{ issue.title }}</a>
+      </h4>
+    </div>
+    <div
+      v-if="showLabelFooter"
+      class="board-card-labels prepend-top-4 d-flex flex-wrap"
+    >
+      <button
+        v-for="label in issue.labels"
+        v-if="showLabel(label)"
+        :key="label.id"
+        v-gl-tooltip
+        :style="labelStyle(label)"
+        :title="label.description"
+        class="badge color-label append-right-4 prepend-top-4"
+        type="button"
+        @click="filterByLabel(label)"
+      >
+        {{ label.title }}
+      </button>
+    </div>
+    <div class="board-card-footer d-flex justify-content-between align-items-end">
+      <div class="d-flex align-items-start flex-wrap-reverse board-card-number-container js-board-card-number-container">
         <span
-          v-if="issueId"
-          class="board-card-number append-right-5"
+          v-if="issue.referencePath"
+          class="board-card-number d-flex append-right-8 prepend-top-8"
         >
-          {{ issue.referencePath }}
+          <tooltip-on-truncate
+            v-if="issueReferencePath"
+            :title="issueReferencePath"
+            placement="bottom"
+            class="board-issue-path block-truncated bold"
+          >{{ issueReferencePath }}</tooltip-on-truncate>#{{ issue.iid }}
         </span>
-        <issue-card-weight
-          v-if="issue.weight"
-          :weight="issue.weight"
-        />
-      </h4>
+        <span class="board-info-items prepend-top-8 d-inline-block">
+          <issue-due-date
+            v-if="issue.dueDate"
+            :date="issue.dueDate"
+          /><issue-time-estimate
+            v-if="issue.timeEstimate"
+            :estimate="issue.timeEstimate"
+          /><issue-card-weight
+            v-if="issue.weight"
+            :weight="issue.weight"
+            @click="filterByWeight(issue.weight)"
+          />
+        </span>
+      </div>
       <div class="board-card-assignee">
         <user-avatar-link
           v-for="(assignee, index) in issue.assignees"
@@ -173,38 +231,26 @@ export default {
           :link-href="assigneeUrl(assignee)"
           :img-alt="avatarUrlTitle(assignee)"
           :img-src="assignee.avatar"
-          :tooltip-text="assigneeUrlTitle(assignee)"
+          :img-size="24"
           class="js-no-trigger"
           tooltip-placement="bottom"
-        />
+        >
+          <span class="js-assignee-tooltip">
+            <span class="bold d-block">Assignee</span>
+            {{ assignee.name }} 
+            <span class="text-white-50">@{{ assignee.username }}</span>
+          </span>
+        </user-avatar-link>
         <span
           v-if="shouldRenderCounter"
-          v-tooltip
+          v-gl-tooltip
           :title="assigneeCounterTooltip"
           class="avatar-counter"
+          data-placement="bottom"
         >
           {{ assigneeCounterLabel }}
         </span>
       </div>
     </div>
-    <div
-      v-if="showLabelFooter"
-      class="board-card-footer"
-    >
-      <button
-        v-for="label in issue.labels"
-        v-if="showLabel(label)"
-        :key="label.id"
-        v-tooltip
-        :style="labelStyle(label)"
-        :title="label.description"
-        class="badge color-label"
-        type="button"
-        data-container="body"
-        @click="filterByLabel(label, $event)"
-      >
-        {{ label.title }}
-      </button>
-    </div>
   </div>
 </template>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
new file mode 100644
index 0000000000000000000000000000000000000000..025ef7e974351440b0bfa405423de43db538ba81
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -0,0 +1,90 @@
+<script>
+import dateFormat from 'dateformat';
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
+
+export default {
+  components: {
+    Icon,
+    GlTooltip,
+  },
+  props: {
+    date: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    title() {
+      const timeago = getTimeago();
+      const { timeDifference, standardDateFormat } = this;
+      const formatedDate = standardDateFormat;
+
+      if (timeDifference >= -1 && timeDifference < 7) {
+        return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
+      }
+
+      return timeago.format(this.issueDueDate);
+    },
+    body() {
+      const { timeDifference, issueDueDate, standardDateFormat } = this;
+
+      if (timeDifference === 0) {
+        return __('Today');
+      } else if (timeDifference === 1) {
+        return __('Tomorrow');
+      } else if (timeDifference === -1) {
+        return __('Yesterday');
+      } else if (timeDifference > 0 && timeDifference < 7) {
+        return dateFormat(issueDueDate, 'dddd', true);
+      }
+
+      return standardDateFormat;
+    },
+    issueDueDate() {
+      return new Date(this.date);
+    },
+    timeDifference() {
+      const today = new Date();
+      return getDayDifference(today, this.issueDueDate);
+    },
+    isPastDue() {
+      if (this.timeDifference >= 0) return false;
+      return true;
+    },
+    standardDateFormat() {
+      const today = new Date();
+      const isDueInCurrentYear = today.getFullYear() === this.issueDueDate.getFullYear();
+
+      return dateInWords(this.issueDueDate, true, isDueInCurrentYear);
+    },
+  },
+};
+</script>
+
+<template>
+  <span>
+    <span
+      ref="issueDueDate"
+      class="board-card-info card-number"
+    >
+      <icon
+        :class="{'text-danger': isPastDue, 'board-card-info-icon': true}"
+        name="calendar"
+      /><time
+        :class="{'text-danger': isPastDue}"
+        datetime="date"
+        class="board-card-info-text">{{ body }}</time>
+    </span>
+    <gl-tooltip
+      :target="() => $refs.issueDueDate"
+      placement="bottom"
+    >
+      <span class="bold">{{ __('Due date') }}</span>
+      <br />
+      <span :class="{'text-danger-muted': isPastDue}">{{ title }}</span>
+    </gl-tooltip>
+  </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f70df3110f9404eae6e9ade084b7ebd38f3766d4
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+
+export default {
+  components: {
+    Icon,
+    GlTooltip,
+  },
+  props: {
+    estimate: {
+      type: Number,
+      required: true,
+    },
+  },
+  computed: {
+    title() {
+      return stringifyTime(parseSeconds(this.estimate), true);
+    },
+    timeEstimate() {
+      return stringifyTime(parseSeconds(this.estimate));
+    },
+  },
+};
+</script>
+
+<template>
+  <span>
+    <span
+      ref="issueTimeEstimate"
+      class="board-card-info card-number"
+    >
+      <icon
+        name="hourglass"
+        css-classes="board-card-info-icon"
+      /><time class="board-card-info-text">{{ timeEstimate }}</time>
+    </span>
+    <gl-tooltip
+      :target="() => $refs.issueTimeEstimate"
+      placement="bottom"
+      class="js-issue-time-estimate"
+    >
+      <span class="bold">{{ __('Time estimate') }}</span>
+      <br /> 
+      {{ title }}
+    </gl-tooltip>
+  </span>
+</template>
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 76dcffc8b2b1dd01d88c823dc0fec1483ac9afb5..e3b30af32f128cd5615b57e0a43659caee35c637 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -29,6 +29,7 @@ class ListIssue {
     this.path = obj.real_path;
     this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
     this.project_id = obj.project_id;
+    this.timeEstimate = obj.time_estimate;
     this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
 
     if (obj.project) {
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index fcdbd0a3aad3642c5e81ff87338135ec6d243d2b..59007d5950e0800a3d93bfce98f309129fa7d5e0 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -454,12 +454,20 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
 /**
  * Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
  * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+ * If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
  */
-export const stringifyTime = timeObject => {
+export const stringifyTime = (timeObject, fullNameFormat = false) => {
   const reducedTime = _.reduce(
     timeObject,
     (memo, unitValue, unitName) => {
       const isNonZero = !!unitValue;
+
+      if (fullNameFormat && isNonZero) {
+        // Remove traling 's' if unit value is singular
+        const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
+        return `${memo} ${unitValue} ${formatedUnitName}`;
+      }
+
       return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
     },
     '',
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 d6db86d541a2bf20a0d170a1156a46f8e4003b30..4cfb1ded0a92529855a1768d2dc0222b36b15f3d 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
@@ -15,14 +15,14 @@
 
 */
 
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
 import defaultAvatarUrl from 'images/no_avatar.png';
 import { placeholderImage } from '../../../lazy_loader';
-import tooltip from '../../directives/tooltip';
 
 export default {
   name: 'UserAvatarImage',
-  directives: {
-    tooltip,
+  components: {
+    GlTooltip,
   },
   props: {
     lazy: {
@@ -73,9 +73,6 @@ export default {
     resultantSrcAttribute() {
       return this.lazy ? placeholderImage : this.sanitizedSource;
     },
-    tooltipContainer() {
-      return this.tooltipText ? 'body' : null;
-    },
     avatarSizeClass() {
       return `s${this.size}`;
     },
@@ -84,21 +81,30 @@ export default {
 </script>
 
 <template>
-  <img
-    v-tooltip
-    :class="{
-      lazy: lazy,
-      [avatarSizeClass]: true,
-      [cssClasses]: true
-    }"
-    :src="resultantSrcAttribute"
-    :width="size"
-    :height="size"
-    :alt="imgAlt"
-    :data-src="sanitizedSource"
-    :data-container="tooltipContainer"
-    :data-placement="tooltipPlacement"
-    :title="tooltipText"
-    class="avatar"
-  />
+  <span>
+    <img
+      ref="userAvatarImage"
+      :class="{
+        lazy: lazy,
+        [avatarSizeClass]: true,
+        [cssClasses]: true
+      }"
+      :src="resultantSrcAttribute"
+      :width="size"
+      :height="size"
+      :alt="imgAlt"
+      :data-src="sanitizedSource"
+      class="avatar"
+    />
+    <gl-tooltip
+      :target="() => $refs.userAvatarImage"
+      :placement="tooltipPlacement"
+      boundary="window"
+      class="js-user-avatar-image-toolip"
+    >
+      <slot>
+        {{ tooltipText }}
+      </slot>
+    </gl-tooltip>
+  </span>
 </template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index dd6f96e2609b980316083408442974b896f69168..351a639c6e8ab0f91ff3180109b3bf395729e2f6 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -17,9 +17,8 @@
 
 */
 
-import { GlLink } from '@gitlab-org/gitlab-ui';
+import { GlLink, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
 import userAvatarImage from './user_avatar_image.vue';
-import tooltip from '../../directives/tooltip';
 
 export default {
   name: 'UserAvatarLink',
@@ -28,7 +27,7 @@ export default {
     userAvatarImage,
   },
   directives: {
-    tooltip,
+    GlTooltip: GlTooltipDirective,
   },
   props: {
     linkHref: {
@@ -94,11 +93,14 @@ export default {
       :size="imgSize"
       :tooltip-text="avatarTooltipText"
       :tooltip-placement="tooltipPlacement"
-    /><span
+    >
+      <slot></slot>
+    </user-avatar-image><span
       v-if="shouldShowUsername"
-      v-tooltip
+      v-gl-tooltip
       :title="tooltipText"
       :tooltip-placement="tooltipPlacement"
+      class="js-user-avatar-link-username"
     >{{ username }}</span><slot name="avatar-badge"></slot>
   </gl-link>
 </template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 627bc40e5440ea4814a31386d42143ec09309b82..de2d8d4bb2e3cbc355145acec6884c61ffa88f2b 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -33,6 +33,11 @@
   color: $brand-danger;
 }
 
+.text-danger-muted,
+.text-danger-muted:hover {
+  color: $red-300;
+}
+
 .text-warning,
 .text-warning:hover {
   color: $brand-warning;
@@ -345,6 +350,7 @@ img.emoji {
 /** COMMON CLASSES **/
 .prepend-top-0 { margin-top: 0; }
 .prepend-top-2 { margin-top: 2px; }
+.prepend-top-4 { margin-top: $gl-padding-4; }
 .prepend-top-5 { margin-top: 5px; }
 .prepend-top-8 { margin-top: $grid-size; }
 .prepend-top-10 { margin-top: 10px; }
@@ -367,6 +373,7 @@ img.emoji {
 .append-right-default { margin-right: $gl-padding; }
 .append-right-20 { margin-right: 20px; }
 .append-bottom-0 { margin-bottom: 0; }
+.append-bottom-4 { margin-bottom: $gl-padding-4; }
 .append-bottom-5 { margin-bottom: 5px; }
 .append-bottom-8 { margin-bottom: $grid-size; }
 .append-bottom-10 { margin-bottom: 10px; }
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 81adcab63525de8122532a2b4f205b1e27d9d298..82b3320042527c557ce02947f0f37999e99af90c 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -196,6 +196,7 @@ $well-light-text-color: #5b6169;
  * Text
  */
 $gl-font-size: 14px;
+$gl-font-size-xs: 11px;
 $gl-font-size-small: 12px;
 $gl-font-weight-normal: 400;
 $gl-font-weight-bold: 600;
@@ -446,7 +447,7 @@ $ci-skipped-color: #888;
 * Boards
 */
 $issue-boards-font-size: 14px;
-$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
+$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
 /*
  The following heights are used in boards.scss and are used for calculation of the board height.
  They probably should be derived in a smarter way.
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index b277c10bf9a4b5fe9dc1d7fcd815852dae288080..291649ad49751f21e43b1c7939df488e1a3ebc6e 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -127,20 +127,14 @@
   }
 
   .with-performance-bar & {
-    height: calc(
-      100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}
-    );
+    height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
 
     @include media-breakpoint-only(sm) {
-      height: calc(
-        100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}
-      );
+      height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
     }
 
     @include media-breakpoint-up(md) {
-      height: calc(
-        100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}
-      );
+      height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
     }
   }
 }
@@ -316,7 +310,7 @@
   height: 100%;
   width: 100%;
   margin-bottom: 0;
-  padding: 5px;
+  padding: $gl-padding-4;
   list-style: none;
   overflow-y: auto;
   overflow-x: hidden;
@@ -329,14 +323,16 @@
 
 .board-card {
   position: relative;
-  padding: 11px 10px 11px $gl-padding;
+  padding: $gl-padding;
   background: $white-light;
   border-radius: $border-radius-default;
+  border: 1px solid $theme-gray-200;
   box-shadow: 0 1px 2px $issue-boards-card-shadow;
   list-style: none;
+  line-height: $gl-padding;
 
   &:not(:last-child) {
-    margin-bottom: 5px;
+    margin-bottom: $gl-padding-8;
   }
 
   &.is-active,
@@ -347,113 +343,120 @@
   .badge {
     border: 0;
     outline: 0;
+
+    &:hover {
+      text-decoration: underline;
+    }
+
+    @include media-breakpoint-down(lg) {
+      font-size: $gl-font-size-xs;
+      padding-left: $gl-padding-4;
+      padding-right: $gl-padding-4;
+      font-weight: $gl-font-weight-bold;
+    }
+  }
+
+  svg {
+    vertical-align: top;
   }
 
   .confidential-icon {
-    vertical-align: text-top;
-    margin-right: 5px;
+    color: $orange-600;
+    cursor: help;
+  }
+
+  @include media-breakpoint-down(md) {
+    padding: $gl-padding-8;
   }
 }
 
 .board-card-title {
   @include overflow-break-word();
-  margin: 0 30px 0 0;
   font-size: 1em;
-  line-height: inherit;
 
   a {
     color: $gl-text-color;
-    margin-right: 2px;
+  }
+
+  @include media-breakpoint-down(md) {
+    font-size: $label-font-size;
   }
 }
 
 .board-card-header {
   display: flex;
-  min-height: 20px;
-
-  .board-card-assignee {
-    display: flex;
-    justify-content: flex-end;
-    position: absolute;
-    right: 15px;
-    height: 20px;
-    width: 20px;
+}
 
-    .avatar-counter {
-      display: none;
-      vertical-align: middle;
-      line-height: 18px;
-      height: 20px;
-      padding-left: 3px;
-      padding-right: 3px;
-      border-radius: 2em;
-    }
+.board-card-assignee {
+  display: flex;
+  margin-top: -$gl-padding-4;
+  margin-bottom: -$gl-padding-4;
+
+  .avatar-counter {
+    vertical-align: middle;
+    line-height: $gl-padding-24;
+    min-width: $gl-padding-24;
+    height: $gl-padding-24;
+    border-radius: $gl-padding-24;
+    background-color: $gl-text-color-tertiary;
+    font-size: $gl-font-size-xs;
+    cursor: help;
+    font-weight: $gl-font-weight-bold;
+    margin-left: -$gl-padding-4;
+    border: 0;
+    padding: 0 $gl-padding-4;
 
-    img {
-      vertical-align: top;
+    @include media-breakpoint-down(md) {
+      min-width: auto;
+      height: $gl-padding;
+      border-radius: $gl-padding;
+      line-height: $gl-padding;
     }
+  }
 
-    a {
-      position: relative;
-      margin-left: -15px;
-    }
+  img {
+    vertical-align: top;
+  }
 
-    a:nth-child(1) {
-      z-index: 3;
-    }
+  .user-avatar-link:not(:only-child) {
+    margin-left: -$gl-padding-4;
 
-    a:nth-child(2) {
+    &:nth-of-type(1) {
       z-index: 2;
     }
 
-    a:nth-child(3) {
+    &:nth-of-type(2) {
       z-index: 1;
     }
+  }
 
-    a:nth-child(4) {
-      display: none;
-    }
-
-    &:hover {
-      .avatar-counter {
-        display: inline-block;
-      }
-
-      a {
-        position: static;
-        background-color: $white-light;
-        transition: background-color 0s;
-        margin-left: auto;
-
-        &:nth-child(4) {
-          display: block;
-        }
+  .avatar {
+    margin: 0;
 
-        &:first-child:not(:only-child) {
-          box-shadow: -10px 0 10px 1px $white-light;
-        }
-      }
+    @include media-breakpoint-down(md) {
+      width: $gl-padding;
+      height: $gl-padding;
     }
   }
 
-  .avatar {
-    margin: 0;
+  @include media-breakpoint-down(md) {
+    margin-top: 0;
+    margin-bottom: 0;
   }
 }
 
-.board-card-footer {
-  margin: 0 0 5px;
+.board-card-number {
+  font-size: $gl-font-size-xs;
+  color: $gl-text-color-secondary;
+  overflow: hidden;
 
-  .badge {
-    margin-top: 5px;
-    margin-right: 6px;
+  @include media-breakpoint-up(md) {
+    font-size: $label-font-size;
   }
 }
 
-.board-card-number {
-  font-size: 12px;
-  color: $gl-text-color-secondary;
-  display: inline-block;
+.board-card-number-container {
+  overflow: hidden;
 }
 
 .issue-boards-search {
@@ -543,8 +546,7 @@
 .right-sidebar.right-sidebar-expanded {
   &.boards-sidebar-slide-enter-active,
   &.boards-sidebar-slide-leave-active {
-    transition: width $sidebar-transition-duration,
-      padding $sidebar-transition-duration;
+    transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
   }
 
   &.boards-sidebar-slide-enter,
@@ -762,3 +764,36 @@
     }
   }
 }
+
+.board-card-info {
+  color: $gl-text-color-secondary;
+  white-space: nowrap;
+  margin-right: $gl-padding-8;
+
+  &:not(.board-card-weight) {
+    cursor: help;
+  }
+
+  &.board-card-weight {
+    color: $gl-text-color;
+    cursor: pointer;
+
+    &:hover {
+      color: initial;
+      text-decoration: underline;
+    }
+  }
+
+  .board-card-info-icon {
+    color: $theme-gray-600;
+    margin-right: $gl-padding-4;
+  }
+
+  @include media-breakpoint-down(md) {
+    font-size: $label-font-size;
+  }
+}
+
+.board-issue-path.js-show-tooltip {
+  cursor: help;
+}
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 7f874687212b6056497eb27652ef670f225fc5d1..ffd9f6fc2b6bd6f1e9a3fbf502230a7ab021e38b 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -102,7 +102,7 @@ module Boards
 
     def serialize_as_json(resource)
       resource.as_json(
-        only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight],
+        only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight, :time_estimate],
         labels: true,
         issue_endpoints: true,
         include_full_project_path: board.group_board?,
diff --git a/ee/app/assets/javascripts/boards/components/issue_card_weight.vue b/ee/app/assets/javascripts/boards/components/issue_card_weight.vue
index 22ce0fcb94b6905989168050c733981b84000650..1c5f564574bf3ed86109875f403850652d953468 100644
--- a/ee/app/assets/javascripts/boards/components/issue_card_weight.vue
+++ b/ee/app/assets/javascripts/boards/components/issue_card_weight.vue
@@ -1,5 +1,5 @@
 <script>
-import tooltip from '~/vue_shared/directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
 import icon from '~/vue_shared/components/icon.vue';
 
 export default {
@@ -8,7 +8,7 @@ export default {
     icon,
   },
   directives: {
-    tooltip,
+    GlTooltip: GlTooltipDirective,
   },
   props: {
     weight: {
@@ -20,14 +20,18 @@ export default {
 </script>
 
 <template>
-  <span
-    v-tooltip
+  <a
+    v-gl-tooltip
     :title="__('Weight')"
-    class="board-card-weight card-number d-inline-block"
+    class="board-card-info card-number board-card-weight"
     data-container="body"
     data-placement="bottom"
+    tabindex="1"
+    v-on="$listeners"
   >
-    <icon name="scale" />
-    {{ weight }}
-  </span>
+    <icon
+      name="scale"
+      css-classes="board-card-info-icon"
+    /><span class="board-card-info-text">{{ weight }}</span>
+  </a>
 </template>
diff --git a/ee/app/assets/javascripts/boards/stores/boards_store_ee.js b/ee/app/assets/javascripts/boards/stores/boards_store_ee.js
index 3bcc2a8d3dda2fe68853afd695969ba6c4165c60..7c702c5aeb1526a8287d4a7c176e9542c93d64e5 100644
--- a/ee/app/assets/javascripts/boards/stores/boards_store_ee.js
+++ b/ee/app/assets/javascripts/boards/stores/boards_store_ee.js
@@ -81,7 +81,6 @@ class BoardsStoreEE {
         weight = 'No+Weight';
       }
       updateFilterPath('weight', weight);
-      this.store.cantEdit.push('weight');
     }
     updateFilterPath('assignee_username', this.store.boardConfig.assigneeUsername);
     if (this.store.boardConfig.assigneeUsername) {
diff --git a/ee/app/assets/stylesheets/pages/boards.scss b/ee/app/assets/stylesheets/pages/boards.scss
index 90e673376908e5ac9fc793e95649a77c284ea54a..e20d9e9acb6da00dee258eaa6d79fe4d992bc13b 100644
--- a/ee/app/assets/stylesheets/pages/boards.scss
+++ b/ee/app/assets/stylesheets/pages/boards.scss
@@ -1,10 +1,3 @@
-.board-card-weight {
-  svg {
-    position: relative;
-    top: 3px;
-  }
-}
-
 .dropdown.show .dropdown-menu.dropdown-menu-tabs {
   max-height: 400px;
   overflow-y: hidden;
diff --git a/ee/changelogs/unreleased/47008-issue-board-card-design.yml b/ee/changelogs/unreleased/47008-issue-board-card-design.yml
new file mode 100644
index 0000000000000000000000000000000000000000..39238687943ce59e874ce5764d5eb21de44bfe26
--- /dev/null
+++ b/ee/changelogs/unreleased/47008-issue-board-card-design.yml
@@ -0,0 +1,5 @@
+---
+title: Issue board card design
+merge_request: 21229
+author:
+type: changed
diff --git a/ee/spec/javascripts/epics/epic_show/components/epic_header_spec.js b/ee/spec/javascripts/epics/epic_show/components/epic_header_spec.js
index e3395f70565d15d37a58a25a443ea879a29cc2f1..eba610bf6c5d0a378601f2e8962a9df19488bd92 100644
--- a/ee/spec/javascripts/epics/epic_show/components/epic_header_spec.js
+++ b/ee/spec/javascripts/epics/epic_show/components/epic_header_spec.js
@@ -30,7 +30,7 @@ describe('epicHeader', () => {
   });
 
   it('should render username tooltip', () => {
-    expect(vm.$el.querySelector('.user-avatar-link span').dataset.originalTitle).toEqual(
+    expect(vm.$el.querySelector('.js-user-avatar-link-username').dataset.originalTitle).toEqual(
       author.username,
     );
   });
diff --git a/ee/spec/javascripts/vue_shared/security_reports/card_security_reports_app_spec.js b/ee/spec/javascripts/vue_shared/security_reports/card_security_reports_app_spec.js
index babf4a54bfe693fbedfe0d7dbe6f2be4205de3bf..7089e5034ed96c4c8bbd6e816b1550c1015d2e16 100644
--- a/ee/spec/javascripts/vue_shared/security_reports/card_security_reports_app_spec.js
+++ b/ee/spec/javascripts/vue_shared/security_reports/card_security_reports_app_spec.js
@@ -99,7 +99,7 @@ describe('Card security reports app', () => {
         `${TEST_HOST}/img?width=24`,
       );
 
-      expect(userAvatarLink.textContent).toBe('TestUser');
+      expect(userAvatarLink.textContent.trim()).toBe('TestUser');
     });
 
     it('branch and commit information', () => {
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 04dead10468067c2404105f5c3a51209b5af8c18..08c5dbc7a1e6bc176eab6d5023ec41db05371a15 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -125,6 +125,9 @@ msgstr ""
 msgid "%{count} %{alerts}"
 msgstr ""
 
+msgid "%{count} more assignees"
+msgstr ""
+
 msgid "%{count} participant"
 msgid_plural "%{count} participants"
 msgstr[0] ""
@@ -8187,6 +8190,9 @@ msgstr ""
 msgid "Time between merge request creation and merge/close"
 msgstr ""
 
+msgid "Time estimate"
+msgstr ""
+
 msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
 msgstr ""
 
@@ -8425,6 +8431,9 @@ msgstr ""
 msgid "To widen your search, change or remove filters."
 msgstr ""
 
+msgid "Today"
+msgstr ""
+
 msgid "Todo"
 msgstr ""
 
@@ -8458,6 +8467,9 @@ msgstr ""
 msgid "Token"
 msgstr ""
 
+msgid "Tomorrow"
+msgstr ""
+
 msgid "Too many changes to show."
 msgstr ""
 
@@ -9031,6 +9043,9 @@ msgstr ""
 msgid "Yes, let me map Google Code users to full names or GitLab users."
 msgstr ""
 
+msgid "Yesterday"
+msgstr ""
+
 msgid "You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution."
 msgstr ""
 
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index ddca89a0908240886c9fdd0d856174781e2b42cc..de26fde6e3d8e80c5b0abaf87dc38338d47ea736 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -168,7 +168,7 @@ describe 'Issue Boards add issue modal', :js do
 
       it 'changes button text with plural' do
         page.within('.add-issues-modal') do
-          all('.board-card .board-card-number').each do |el|
+          all('.board-card .js-board-card-number-container').each do |el|
             el.click
           end
 
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index ec0ca21450a285505b75374ab942a19d03916f8c..2177933655913903beac98eb01a56352e2206e56 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -78,7 +78,7 @@ describe 'Issue Boards', :js do
     end
 
     it 'moves from bottom to top' do
-      drag(from_index: 2, to_index: 0)
+      drag(from_index: 2, to_index: 0, duration: 1020)
 
       wait_for_requests
 
@@ -130,7 +130,7 @@ describe 'Issue Boards', :js do
     end
 
     it 'moves to bottom of another list' do
-      drag(list_from_index: 1, list_to_index: 2, to_index: 2)
+      drag(list_from_index: 1, list_to_index: 2, to_index: 2, duration: 1020)
 
       wait_for_requests
 
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index d3da8cc6752c8e7085f7f70bbef7146cde5acffa..b58c433bbfe42845f5edd5daa54d32be330e4ba3 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -89,16 +89,17 @@ describe 'Merge request > User sees avatars on diff notes', :js do
         page.within find_line(position.line_code(project.repository)) do
           find('.diff-notes-collapse').send_keys(:return)
 
-          expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
+          expect(page).to have_selector('.js-diff-comment-avatar img', count: 1)
         end
       end
 
       it 'shows comment on note avatar' do
         page.within find_line(position.line_code(project.repository)) do
           find('.diff-notes-collapse').send_keys(:return)
-
-          expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
+          first('.js-diff-comment-avatar img').hover
         end
+
+        expect(page).to have_content "#{note.author.name}: #{note.note.truncate(17)}"
       end
 
       it 'toggles comments when clicking avatar' do
@@ -109,7 +110,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
         expect(page).not_to have_selector('.notes_holder')
 
         page.within find_line(position.line_code(project.repository)) do
-          first('img.js-diff-comment-avatar').click
+          first('.js-diff-comment-avatar img').click
         end
 
         expect(page).to have_selector('.notes_holder')
@@ -125,7 +126,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
         wait_for_requests
 
         page.within find_line(position.line_code(project.repository)) do
-          expect(page).not_to have_selector('img.js-diff-comment-avatar')
+          expect(page).not_to have_selector('.js-diff-comment-avatar img')
         end
       end
 
@@ -143,7 +144,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
         page.within find_line(position.line_code(project.repository)) do
           find('.diff-notes-collapse').send_keys(:return)
 
-          expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
+          expect(page).to have_selector('.js-diff-comment-avatar img', count: 2)
         end
       end
 
@@ -162,7 +163,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
         page.within find_line(position.line_code(project.repository)) do
           find('.diff-notes-collapse').send_keys(:return)
 
-          expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
+          expect(page).to have_selector('.js-diff-comment-avatar img', count: 3)
           expect(find('.diff-comments-more-count')).to have_content '+1'
         end
       end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 00f9145a11bb2cae4f20d1ff42c955acf10925a0..d5d803614944b58bbdbcf46afffa6942c92b8479 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -14,6 +14,7 @@
     "confidential": { "type": "boolean" },
     "due_date": { "type": ["date", "null"] },
     "relative_position": { "type": "integer" },
+    "time_estimate": { "type": "integer" },
     "issue_sidebar_endpoint": { "type": "string" },
     "toggle_subscription_endpoint": { "type": "string" },
     "assignable_labels_endpoint": { "type": "string" },
diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e49330c052d0c40850031924ea14b72ef3513d1
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_due_date_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import dateFormat from 'dateformat';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Issue Due Date component', () => {
+  let vm;
+  let date;
+  const Component = Vue.extend(IssueDueDate);
+  const createComponent = (dueDate = new Date()) =>
+    mountComponent(Component, { date: dateFormat(dueDate, 'yyyy-mm-dd', true) });
+
+  beforeEach(() => {
+    date = new Date();
+    vm = createComponent();
+  });
+
+  afterEach(() => {
+    vm.$destroy();
+  });
+
+  it('should render "Today" if the due date is today', () => {
+    const timeContainer = vm.$el.querySelector('time');
+
+    expect(timeContainer.textContent.trim()).toEqual('Today');
+  });
+
+  it('should render "Yesterday" if the due date is yesterday', () => {
+    date.setDate(date.getDate() - 1);
+    vm = createComponent(date);
+
+    expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Yesterday');
+  });
+
+  it('should render "Tomorrow" if the due date is one day from now', () => {
+    date.setDate(date.getDate() + 1);
+    vm = createComponent(date);
+
+    expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Tomorrow');
+  });
+
+  it('should render day of the week if due date is one week away', () => {
+    date.setDate(date.getDate() + 5);
+    vm = createComponent(date);
+
+    expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true));
+  });
+
+  it('should render month and day for other dates', () => {
+    date.setDate(date.getDate() + 17);
+    vm = createComponent(date);
+
+    expect(vm.$el.querySelector('time').textContent.trim()).toEqual(
+      dateFormat(date, 'mmm d', true),
+    );
+  });
+
+  it('should contain the correct `.text-danger` css class for overdue issue', () => {
+    date.setDate(date.getDate() - 17);
+    vm = createComponent(date);
+
+    expect(vm.$el.querySelector('time').classList.contains('text-danger')).toEqual(true);
+  });
+});
diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ba65d3287daf00aaccbdc47413574412d40eb65f
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_time_estimate_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Issue Tine Estimate component', () => {
+  let vm;
+
+  beforeEach(() => {
+    const Component = Vue.extend(IssueTimeEstimate);
+    vm = mountComponent(Component, {
+      estimate: 374460,
+    });
+  });
+
+  afterEach(() => {
+    vm.$destroy();
+  });
+
+  it('renders the correct time estimate', () => {
+    expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
+  });
+
+  it('renders expanded time estimate in tooltip', () => {
+    expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
+      '2 weeks 3 days 1 minute',
+    );
+  });
+
+  it('prevents tooltip xss', done => {
+    const alertSpy = spyOn(window, 'alert');
+    vm.estimate = 'Foo <script>alert("XSS")</script>';
+
+    vm.$nextTick(() => {
+      expect(alertSpy).not.toHaveBeenCalled();
+      expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
+      expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
+      done();
+    });
+  });
+});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index e8a0d7c67a1475655b20fb7234a20bc426e5fef5..e33cbc308a9da89201e7e2415ac2c63969cd3c90 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -120,11 +120,9 @@ describe('Issue card component', () => {
       });
 
       it('sets title', () => {
-        expect(
-          component.$el
-            .querySelector('.board-card-assignee img')
-            .getAttribute('data-original-title'),
-        ).toContain(`Assigned to ${user.name}`);
+        expect(component.$el.querySelector('.js-assignee-tooltip').textContent).toContain(
+          `${user.name}`,
+        );
       });
 
       it('sets users path', () => {
@@ -157,7 +155,7 @@ describe('Issue card component', () => {
       it('displays defaults avatar if users avatar is null', () => {
         expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull();
         expect(component.$el.querySelector('.board-card-assignee img').getAttribute('src')).toBe(
-          'default_avatar?width=20',
+          'default_avatar?width=24',
         );
       });
     });
@@ -166,7 +164,6 @@ describe('Issue card component', () => {
   describe('multiple assignees', () => {
     beforeEach(done => {
       component.issue.assignees = [
-        user,
         new ListAssignee({
           id: 2,
           name: 'user2',
@@ -190,11 +187,11 @@ describe('Issue card component', () => {
       Vue.nextTick(() => done());
     });
 
-    it('renders all four assignees', () => {
-      expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4);
+    it('renders all three assignees', () => {
+      expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
     });
 
-    describe('more than four assignees', () => {
+    describe('more than three assignees', () => {
       beforeEach(done => {
         component.issue.assignees.push(
           new ListAssignee({
@@ -210,12 +207,12 @@ describe('Issue card component', () => {
 
       it('renders more avatar counter', () => {
         expect(
-          component.$el.querySelector('.board-card-assignee .avatar-counter').innerText,
+          component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
         ).toEqual('+2');
       });
 
-      it('renders three assignees', () => {
-        expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
+      it('renders two assignees', () => {
+        expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(2);
       });
 
       it('renders 99+ avatar counter', done => {
@@ -231,7 +228,7 @@ describe('Issue card component', () => {
 
         Vue.nextTick(() => {
           expect(
-            component.$el.querySelector('.board-card-assignee .avatar-counter').innerText,
+            component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
           ).toEqual('99+');
           done();
         });
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index f8ca43fc150b5146e172d932d6d35a15bd2008f9..aaae6b2979deb4a6eb33e1322befd48ce0d6b1ae 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -101,7 +101,7 @@ describe('Job App ', () => {
                 .querySelector('.header-main-content')
                 .textContent.replace(/\s+/g, ' ')
                 .trim(),
-            ).toEqual('passed Job #4757 triggered 1 year ago by Root');
+            ).toContain('passed Job #4757 triggered 1 year ago by Root');
             done();
           }, 0);
         });
@@ -127,7 +127,7 @@ describe('Job App ', () => {
                 .querySelector('.header-main-content')
                 .textContent.replace(/\s+/g, ' ')
                 .trim(),
-            ).toEqual('passed Job #4757 created 3 weeks ago by Root');
+            ).toContain('passed Job #4757 created 3 weeks ago by Root');
             done();
           }, 0);
         });
diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js
index d699e66b8ca951d671cc212282fda5e51f51eea8..bebe76f76c5b7ec32a89d5cb7d0da211d92d840f 100644
--- a/spec/javascripts/lib/utils/datetime_utility_spec.js
+++ b/spec/javascripts/lib/utils/datetime_utility_spec.js
@@ -336,6 +336,12 @@ describe('prettyTime methods', () => {
 
       expect(timeString).toBe('0m');
     });
+
+    it('should return non-condensed representation of time object', () => {
+      const timeObject = { weeks: 1, days: 0, hours: 1, minutes: 0 };
+
+      expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour');
+    });
   });
 
   describe('abbreviateTime', () => {
diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js
index 473a062fc4070070c90a88d9013341f70af6b244..556a0976b293be3a724ec3207751af762ef51b00 100644
--- a/spec/javascripts/pipelines/header_component_spec.js
+++ b/spec/javascripts/pipelines/header_component_spec.js
@@ -51,7 +51,7 @@ describe('Pipeline details header', () => {
         .querySelector('.header-main-content')
         .textContent.replace(/\s+/g, ' ')
         .trim(),
-    ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo');
+    ).toContain('failed Pipeline #123 triggered 3 weeks ago by Foo');
   });
 
   describe('action buttons', () => {
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index c9011b403b7e1f58f3e125102a251b5340da0e87..d6c44f4c976d5ac3e1e34b4241e86b5b495bfabc 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -63,12 +63,15 @@ describe('Pipeline Url Component', () => {
     }).$mount();
 
     const image = component.$el.querySelector('.js-pipeline-url-user img');
+    const tooltip = component.$el.querySelector(
+      '.js-pipeline-url-user .js-user-avatar-image-toolip',
+    );
 
     expect(component.$el.querySelector('.js-pipeline-url-user').getAttribute('href')).toEqual(
       mockData.pipeline.user.web_url,
     );
 
-    expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name);
+    expect(tooltip.textContent.trim()).toEqual(mockData.pipeline.user.name);
     expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`);
   });
 
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 506d01f5ec177908d5ff2aadb88e041b9952f537..4c575536f0eb90323685dac0b6344dcd499af436 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -86,8 +86,8 @@ describe('Pipelines Table Row', () => {
 
         expect(
           component.$el
-            .querySelector('.table-section:nth-child(2) img')
-            .getAttribute('data-original-title'),
+            .querySelector('.table-section:nth-child(2) .js-user-avatar-image-toolip')
+            .textContent.trim(),
         ).toEqual(pipeline.user.name);
       });
     });
@@ -112,8 +112,8 @@ describe('Pipelines Table Row', () => {
 
       const commitAuthorLink = commitAuthorElement.getAttribute('href');
       const commitAuthorName = commitAuthorElement
-        .querySelector('img.avatar')
-        .getAttribute('data-original-title');
+        .querySelector('.js-user-avatar-image-toolip')
+        .textContent.trim();
 
       return { commitAuthorElement, commitAuthorLink, commitAuthorName };
     };
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 97dacec1fce0c73d95d85506a4e7fd9da6898e6b..18fcdf7ede11ef4696a7473b7f352529a789f40d 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -98,8 +98,8 @@ describe('Commit component', () => {
       it('Should render the author avatar with title and alt attributes', () => {
         expect(
           component.$el
-            .querySelector('.commit-title .avatar-image-container img')
-            .getAttribute('data-original-title'),
+            .querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip')
+            .textContent.trim(),
         ).toContain(props.author.username);
 
         expect(
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index 3bf497bc00bd18326452547957bae581e584ce40..7a741bdc0672f89d288f8340be685924ab908ec6 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -73,7 +73,7 @@ describe('Header CI Component', () => {
     });
 
     it('should render user icon and name', () => {
-      expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
+      expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
     });
 
     it('should render provided actions', () => {
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index dc7652c77f7b4023af4e8ef82d1d3338d76fbebb..5c4aa7cf844a51facff737ca531bb10e3eb7f867 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -1,7 +1,7 @@
 import Vue from 'vue';
 import { placeholderImage } from '~/lazy_loader';
 import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
 
 const DEFAULT_PROPS = {
   size: 99,
@@ -32,18 +32,12 @@ describe('User Avatar Image Component', function() {
     });
 
     it('should have <img> as a child element', function() {
-      expect(vm.$el.tagName).toBe('IMG');
-      expect(vm.$el.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
-      expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
-      expect(vm.$el.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
-    });
-
-    it('should properly compute tooltipContainer', function() {
-      expect(vm.tooltipContainer).toBe('body');
-    });
+      const imageElement = vm.$el.querySelector('img');
 
-    it('should properly render tooltipContainer', function() {
-      expect(vm.$el.getAttribute('data-container')).toBe('body');
+      expect(imageElement).not.toBe(null);
+      expect(imageElement.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+      expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+      expect(imageElement.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
     });
 
     it('should properly compute avatarSizeClass', function() {
@@ -51,7 +45,7 @@ describe('User Avatar Image Component', function() {
     });
 
     it('should properly render img css', function() {
-      const { classList } = vm.$el;
+      const { classList } = vm.$el.querySelector('img');
       const containsAvatar = classList.contains('avatar');
       const containsSizeClass = classList.contains('s99');
       const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
@@ -73,12 +67,41 @@ describe('User Avatar Image Component', function() {
     });
 
     it('should add lazy attributes', function() {
-      const { classList } = vm.$el;
-      const lazyClass = classList.contains('lazy');
+      const imageElement = vm.$el.querySelector('img');
+      const lazyClass = imageElement.classList.contains('lazy');
 
       expect(lazyClass).toBe(true);
-      expect(vm.$el.getAttribute('src')).toBe(placeholderImage);
-      expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+      expect(imageElement.getAttribute('src')).toBe(placeholderImage);
+      expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+    });
+  });
+
+  describe('dynamic tooltip content', () => {
+    const props = DEFAULT_PROPS;
+    const slots = {
+      default: ['Action!'],
+    };
+
+    beforeEach(() => {
+      vm = mountComponentWithSlots(UserAvatarImage, { props, slots }).$mount();
+    });
+
+    it('renders the tooltip slot', () => {
+      expect(vm.$el.querySelector('.js-user-avatar-image-toolip')).not.toBe(null);
+    });
+
+    it('renders the tooltip content', () => {
+      expect(vm.$el.querySelector('.js-user-avatar-image-toolip').textContent).toContain(
+        slots.default[0],
+      );
+    });
+
+    it('does not render tooltip data attributes for on avatar image', () => {
+      const avatarImg = vm.$el.querySelector('img');
+
+      expect(avatarImg.dataset.originalTitle).not.toBeDefined();
+      expect(avatarImg.dataset.placement).not.toBeDefined();
+      expect(avatarImg.dataset.container).not.toBeDefined();
     });
   });
 });
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index e022245d3ea5403bf8dc98ded93643b0919301f7..0151ad23ba24daa4ea4304070d1be6d0d4fd63ca 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -60,39 +60,43 @@ describe('User Avatar Link Component', function() {
     it('should only render image tag in link', function() {
       const childElements = this.userAvatarLink.$el.childNodes;
 
-      expect(childElements[0].tagName).toBe('IMG');
+      expect(this.userAvatarLink.$el.querySelector('img')).not.toBe('null');
 
       // Vue will render the hidden component as <!---->
       expect(childElements[1].tagName).toBeUndefined();
     });
 
     it('should render avatar image tooltip', function() {
-      expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(
-        this.propsData.tooltipText,
-      );
+      expect(this.userAvatarLink.shouldShowUsername).toBe(false);
+      expect(this.userAvatarLink.avatarTooltipText).toEqual(this.propsData.tooltipText);
     });
   });
 
   describe('username', function() {
     it('should not render avatar image tooltip', function() {
-      expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual('');
+      expect(
+        this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(),
+      ).toEqual('');
     });
 
     it('should render username prop in <span>', function() {
-      expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(
-        this.propsData.username,
-      );
+      expect(
+        this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').innerText.trim(),
+      ).toEqual(this.propsData.username);
     });
 
     it('should render text tooltip for <span>', function() {
-      expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(
-        this.propsData.tooltipText,
-      );
+      expect(
+        this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').dataset
+          .originalTitle,
+      ).toEqual(this.propsData.tooltipText);
     });
 
     it('should render text tooltip placement for <span>', function() {
       expect(
-        this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement'),
+        this.userAvatarLink.$el
+          .querySelector('.js-user-avatar-link-username')
+          .getAttribute('tooltip-placement'),
       ).toEqual(this.propsData.tooltipPlacement);
     });
   });