diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4faf08387fb4fa95c9fd0d67a035f9db917a2102
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -0,0 +1,76 @@
+<script>
+  export default {
+    props: {
+      illustrationPath: {
+        type: String,
+        required: true,
+      },
+      illustrationSizeClass: {
+        type: String,
+        required: true,
+      },
+      title: {
+        type: String,
+        required: true,
+      },
+      content: {
+        type: String,
+        required: false,
+        default: null,
+      },
+      action: {
+        type: Object,
+        required: false,
+        default: null,
+        validator(value) {
+          return (
+            value === null ||
+            (Object.prototype.hasOwnProperty.call(value, 'link') &&
+              Object.prototype.hasOwnProperty.call(value, 'method') &&
+              Object.prototype.hasOwnProperty.call(value, 'title'))
+          );
+        },
+      },
+    },
+  };
+</script>
+<template>
+  <div class="row empty-state">
+    <div class="col-12">
+      <div
+        :class="illustrationSizeClass"
+        class="svg-content"
+      >
+        <img :src="illustrationPath" />
+      </div>
+    </div>
+
+    <div class="col-12">
+      <div class="text-content">
+        <h4 class="js-job-empty-state-title text-center">
+          {{ title }}
+        </h4>
+
+        <p
+          v-if="content"
+          class="js-job-empty-state-content"
+        >
+          {{ content }}
+        </p>
+
+        <div
+          v-if="action"
+          class="text-center"
+        >
+          <a
+            :href="action.link"
+            :data-method="action.method"
+            class="js-job-empty-state-action btn btn-primary"
+          >
+            {{ action.title }}
+          </a>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/changelogs/unreleased/50101-empty-state-component.yml b/changelogs/unreleased/50101-empty-state-component.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ee99b65d9642ec6e8e69c17d05031f345cdbef13
--- /dev/null
+++ b/changelogs/unreleased/50101-empty-state-component.yml
@@ -0,0 +1,5 @@
+---
+title: Creates empty state vue component for job view
+merge_request:
+author:
+type: other
diff --git a/spec/javascripts/jobs/empty_state_spec.js b/spec/javascripts/jobs/empty_state_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f8feb069fe01c655a6871b4f0bf4812e65ab5d00
--- /dev/null
+++ b/spec/javascripts/jobs/empty_state_spec.js
@@ -0,0 +1,90 @@
+import Vue from 'vue';
+import component from '~/jobs/components/empty_state.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Empty State', () => {
+  const Component = Vue.extend(component);
+  let vm;
+
+  const props = {
+    illustrationPath: 'illustrations/pending_job_empty.svg',
+    illustrationSizeClass: 'svg-430',
+    title: 'This job has not started yet',
+  };
+
+  const content = 'This job is in pending state and is waiting to be picked by a runner';
+
+  afterEach(() => {
+    vm.$destroy();
+  });
+
+  describe('renders image and title', () => {
+    beforeEach(() => {
+      vm = mountComponent(Component, {
+        ...props,
+        content,
+      });
+    });
+
+    it('renders img with provided path and size', () => {
+      expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath);
+      expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass);
+    });
+
+    it('renders provided title', () => {
+      expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual(
+        props.title,
+      );
+    });
+  });
+
+  describe('with content', () => {
+    it('renders content', () => {
+      vm = mountComponent(Component, {
+        ...props,
+        content,
+      });
+
+      expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual(
+        content,
+      );
+    });
+  });
+
+  describe('without content', () => {
+    it('does not render content', () => {
+      vm = mountComponent(Component, {
+        ...props,
+      });
+      expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull();
+    });
+  });
+
+  describe('with action', () => {
+    it('renders action', () => {
+      vm = mountComponent(Component, {
+        ...props,
+        content,
+        action: {
+          link: 'runner',
+          title: 'Check runner',
+          method: 'post',
+        },
+      });
+
+      expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual(
+        'runner',
+      );
+    });
+  });
+
+  describe('without action', () => {
+    it('does not render action', () => {
+      vm = mountComponent(Component, {
+        ...props,
+        content,
+      });
+      expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull();
+    });
+  });
+});