diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue
index 99ec5c78830d9a76047d4bb8f8725e40ebc1b410..b6e3cdbb7a3db6347371f330cf1ea1932a856cda 100644
--- a/app/assets/javascripts/repository/components/commit_info.vue
+++ b/app/assets/javascripts/repository/components/commit_info.vue
@@ -53,7 +53,7 @@ export default {
 </script>
 
 <template>
-  <div class="well-segment commit gl-min-h-8 gl-p-5 gl-w-full gl-display-flex">
+  <div class="well-segment commit gl-min-h-8 gl-p-2 gl-w-full gl-display-flex">
     <user-avatar-link
       v-if="commit.author"
       :link-href="commit.author.webPath"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
new file mode 100644
index 0000000000000000000000000000000000000000..9bce9402afa77d977540727375f0835b1cf06ae9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import CommitInfo from '~/repository/components/commit_info.vue';
+import { calculateBlameOffset, toggleBlameClasses } from '../utils';
+
+export default {
+  name: 'BlameInfo',
+  components: {
+    CommitInfo,
+  },
+  directives: {
+    GlTooltip: GlTooltipDirective,
+    SafeHtml,
+  },
+  props: {
+    blameData: {
+      type: Array,
+      required: true,
+    },
+  },
+  computed: {
+    blameInfo() {
+      return this.blameData.map((blame, index) => ({
+        ...blame,
+        blameOffset: calculateBlameOffset(blame.lineno, index),
+      }));
+    },
+  },
+  mounted() {
+    toggleBlameClasses(this.blameData, true);
+  },
+  destroyed() {
+    toggleBlameClasses(this.blameData, false);
+  },
+};
+</script>
+<template>
+  <div class="blame gl-bg-gray-10">
+    <div class="blame-commit gl-border-none!">
+      <commit-info
+        v-for="(blame, index) in blameInfo"
+        :key="index"
+        :class="{ 'gl-border-t': index !== 0 }"
+        class="gl-display-flex gl-absolute gl-px-3"
+        :style="{ top: blame.blameOffset }"
+        :commit="blame.commit"
+      />
+    </div>
+  </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..af01653fc0d96d80e31d39e46a1b2c2b773d5aff
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
@@ -0,0 +1,37 @@
+const BLAME_INFO_CLASSLIST = ['gl-border-t', 'gl-border-gray-500', 'gl-pt-3!'];
+const PADDING_BOTTOM_LARGE = 'gl-pb-6!';
+const PADDING_BOTTOM_SMALL = 'gl-pb-3!';
+
+const findLineNumberElement = (lineNumber) => document.getElementById(`L${lineNumber}`);
+
+const findLineContentElement = (lineNumber) => document.getElementById(`LC${lineNumber}`);
+
+export const calculateBlameOffset = (lineNumber) => {
+  if (lineNumber === 1) return '0px';
+  const lineContentOffset = findLineContentElement(lineNumber)?.offsetTop;
+  return `${lineContentOffset}px`;
+};
+
+export const toggleBlameClasses = (blameData, isVisible) => {
+  /**
+   * Adds/removes classes to line number/content elements to match the line with the blame info
+   * */
+  const method = isVisible ? 'add' : 'remove';
+  blameData.forEach(({ lineno, span }) => {
+    const lineNumberEl = findLineNumberElement(lineno)?.parentElement;
+    const lineContentEl = findLineContentElement(lineno);
+    const lineNumberSpanEl = findLineNumberElement(lineno + span - 1)?.parentElement;
+    const lineContentSpanEl = findLineContentElement(lineno + span - 1);
+
+    lineNumberEl?.classList[method](...BLAME_INFO_CLASSLIST);
+    lineContentEl?.classList[method](...BLAME_INFO_CLASSLIST);
+
+    if (span === 1) {
+      lineNumberSpanEl?.classList[method](PADDING_BOTTOM_LARGE);
+      lineContentSpanEl?.classList[method](PADDING_BOTTOM_LARGE);
+    } else {
+      lineNumberSpanEl?.classList[method](PADDING_BOTTOM_SMALL);
+      lineContentSpanEl?.classList[method](PADDING_BOTTOM_SMALL);
+    }
+  });
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/__snapshots__/utils_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..e75b07dcf71c15607877e0ccb221b87b8361d09d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/__snapshots__/utils_spec.js.snap
@@ -0,0 +1,88 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SourceViewer utils toggleBlameClasses adds classes 1`] = `
+<div
+  class="content"
+>
+  <div
+    class="gl-border-gray-500 gl-border-t gl-pt-3!"
+  >
+    <div
+      id="reference-0"
+    >
+      1
+    </div>
+    <div
+      id="reference-1"
+    >
+      2
+    </div>
+    <div
+      id="reference-2"
+    >
+      3
+    </div>
+  </div>
+  <div>
+    <div
+      class="gl-border-gray-500 gl-border-t gl-pt-3!"
+      id="reference-3"
+    >
+      Content 1
+    </div>
+    <div
+      class="gl-border-gray-500 gl-border-t gl-pt-3!"
+      id="reference-4"
+    >
+      Content 2
+    </div>
+    <div
+      class="gl-border-gray-500 gl-border-t gl-pt-3!"
+      id="reference-5"
+    >
+      Content 3
+    </div>
+  </div>
+</div>
+`;
+
+exports[`SourceViewer utils toggleBlameClasses removes classes 1`] = `
+<div
+  class="content"
+>
+  <div>
+    <div
+      id="reference-0"
+    >
+      1
+    </div>
+    <div
+      id="reference-1"
+    >
+      2
+    </div>
+    <div
+      id="reference-2"
+    >
+      3
+    </div>
+  </div>
+  <div>
+    <div
+      id="reference-3"
+    >
+      Content 1
+    </div>
+    <div
+      id="reference-4"
+    >
+      Content 2
+    </div>
+    <div
+      id="reference-5"
+    >
+      Content 3
+    </div>
+  </div>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ff8b2be96340fb40e108b8c8afc2f2a877c6d060
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
@@ -0,0 +1,63 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture } from 'helpers/fixtures';
+import CommitInfo from '~/repository/components/commit_info.vue';
+import BlameInfo from '~/vue_shared/components/source_viewer/components/blame_info.vue';
+import * as utils from '~/vue_shared/components/source_viewer/utils';
+import { SOURCE_CODE_CONTENT_MOCK, BLAME_DATA_MOCK } from '../mock_data';
+
+describe('BlameInfo component', () => {
+  let wrapper;
+
+  const createComponent = () => {
+    wrapper = shallowMountExtended(BlameInfo, {
+      propsData: { blameData: BLAME_DATA_MOCK },
+    });
+  };
+
+  beforeEach(() => {
+    setHTMLFixture(SOURCE_CODE_CONTENT_MOCK);
+    jest.spyOn(utils, 'toggleBlameClasses');
+    createComponent();
+  });
+
+  const findCommitInfoComponents = () => wrapper.findAllComponents(CommitInfo);
+
+  it('adds the necessary classes to the DOM', () => {
+    expect(utils.toggleBlameClasses).toHaveBeenCalledWith(BLAME_DATA_MOCK, true);
+  });
+
+  it('renders a CommitInfo component for each blame entry', () => {
+    expect(findCommitInfoComponents().length).toBe(BLAME_DATA_MOCK.length);
+  });
+
+  it.each(BLAME_DATA_MOCK)(
+    'sets the correct data and positioning for the commitInfo',
+    ({ lineno, commit, index }) => {
+      const commitInfoComponent = findCommitInfoComponents().at(index);
+
+      expect(commitInfoComponent.props('commit')).toEqual(commit);
+      expect(commitInfoComponent.element.style.top).toBe(utils.calculateBlameOffset(lineno));
+    },
+  );
+
+  describe('commitInfo component styling', () => {
+    const borderTopClassName = 'gl-border-t';
+
+    it('does not add a top border for the first entry', () => {
+      expect(findCommitInfoComponents().at(0).element.classList).not.toContain(borderTopClassName);
+    });
+
+    it('add a top border for the rest of the entries', () => {
+      expect(findCommitInfoComponents().at(1).element.classList).toContain(borderTopClassName);
+      expect(findCommitInfoComponents().at(2).element.classList).toContain(borderTopClassName);
+    });
+  });
+
+  describe('when component is destroyed', () => {
+    beforeEach(() => wrapper.destroy());
+
+    it('resets the DOM to its original state', () => {
+      expect(utils.toggleBlameClasses).toHaveBeenCalledWith(BLAME_DATA_MOCK, false);
+    });
+  });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
index f35e9607d5c535629e75a501bed228893c1f24bc..b3516f7ed72148ea7196ecde6d8a40a23c5ede8a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -22,3 +22,24 @@ export const CHUNK_2 = {
   startingFrom: 70,
   blamePath,
 };
+
+export const SOURCE_CODE_CONTENT_MOCK = `    
+<div class="content">
+  <div>
+    <div id="L1">1</div>
+    <div id="L2">2</div>
+    <div id="L3">3</div>
+  </div>
+
+  <div>
+    <div id="LC1">Content 1</div>
+    <div id="LC2">Content 2</div>
+    <div id="LC3">Content 3</div>
+  </div>
+</div>`;
+
+export const BLAME_DATA_MOCK = [
+  { lineno: 1, commit: { author: 'Peter' }, index: 0 },
+  { lineno: 2, commit: { author: 'Sarah' }, index: 1 },
+  { lineno: 3, commit: { author: 'Peter' }, index: 2 },
+];
diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0ac72aa9afb3aa8d4fa59592b30039f5612e7d98
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
@@ -0,0 +1,35 @@
+import { setHTMLFixture } from 'helpers/fixtures';
+import {
+  calculateBlameOffset,
+  toggleBlameClasses,
+} from '~/vue_shared/components/source_viewer/utils';
+import { SOURCE_CODE_CONTENT_MOCK, BLAME_DATA_MOCK } from './mock_data';
+
+describe('SourceViewer utils', () => {
+  beforeEach(() => setHTMLFixture(SOURCE_CODE_CONTENT_MOCK));
+
+  const findContent = () => document.querySelector('.content');
+
+  describe('calculateBlameOffset', () => {
+    it('returns an offset of zero if line number === 1', () => {
+      expect(calculateBlameOffset(1)).toBe('0px');
+    });
+
+    it('calculates an offset for the blame component', () => {
+      const { offsetTop } = document.querySelector('#LC3');
+      expect(calculateBlameOffset(3)).toBe(`${offsetTop}px`);
+    });
+  });
+
+  describe('toggleBlameClasses', () => {
+    it('adds classes', () => {
+      toggleBlameClasses(BLAME_DATA_MOCK, true);
+      expect(findContent()).toMatchSnapshot();
+    });
+
+    it('removes classes', () => {
+      toggleBlameClasses(BLAME_DATA_MOCK, false);
+      expect(findContent()).toMatchSnapshot();
+    });
+  });
+});