diff --git a/app/assets/javascripts/blob/notebook/notebook_viewer.vue b/app/assets/javascripts/blob/notebook/notebook_viewer.vue
index d2a841c88f139dbca8547d64cc2b6b90dbaba89d..79b8d2738838770c55cf56c6dadff2bb366d1170 100644
--- a/app/assets/javascripts/blob/notebook/notebook_viewer.vue
+++ b/app/assets/javascripts/blob/notebook/notebook_viewer.vue
@@ -66,7 +66,7 @@ export default {
     <div v-if="loading && !error" class="text-center loading">
       <gl-loading-icon class="mt-5" size="lg" />
     </div>
-    <notebook-lab v-if="!loading && !error" :notebook="json" code-css-class="code white" />
+    <notebook-lab v-if="!loading && !error" :notebook="json" />
     <p v-if="error" class="text-center">
       <span v-if="loadError" ref="loadErrorMessage">{{
         __('An error occurred while loading the file. Please try again later.')
diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
index f5a6f3a98174597c7b26237db5b92aefaa475b61..bc1bab625533f849c91823f64fa52fb4776c142d 100644
--- a/app/assets/javascripts/notebook/cells/code.vue
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -13,11 +13,6 @@ export default {
       type: Object,
       required: true,
     },
-    codeCssClass: {
-      type: String,
-      required: false,
-      default: '',
-    },
   },
   computed: {
     rawInputCode() {
@@ -39,18 +34,12 @@ export default {
 
 <template>
   <div class="cell">
-    <code-output
-      :raw-code="rawInputCode"
-      :count="cell.execution_count"
-      :code-css-class="codeCssClass"
-      type="input"
-    />
+    <code-output :raw-code="rawInputCode" :count="cell.execution_count" type="input" />
     <output-cell
       v-if="hasOutput"
       :count="cell.execution_count"
       :outputs="outputs"
       :metadata="cell.metadata"
-      :code-css-class="codeCssClass"
     />
   </div>
 </template>
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
index e1ef9aa6d7968c7378de6e78605334dfbe7734cc..64e801a751667bf546309ebb6c30545a56888226 100644
--- a/app/assets/javascripts/notebook/cells/code/index.vue
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -1,10 +1,11 @@
 <script>
-import Prism from '../../lib/highlight';
+import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue';
 import Prompt from '../prompt.vue';
 
 export default {
   name: 'CodeOutput',
   components: {
+    CodeBlockHighlighted,
     Prompt,
   },
   props: {
@@ -13,11 +14,6 @@ export default {
       required: false,
       default: 0,
     },
-    codeCssClass: {
-      type: String,
-      required: false,
-      default: '',
-    },
     type: {
       type: String,
       required: true,
@@ -41,22 +37,21 @@ export default {
 
       return type.charAt(0).toUpperCase() + type.slice(1);
     },
-    cellCssClass() {
-      return {
-        [this.codeCssClass]: true,
-        'jupyter-notebook-scrolled': this.metadata.scrolled,
-      };
+    maxHeight() {
+      return this.metadata.scrolled ? '20rem' : 'initial';
     },
   },
-  mounted() {
-    Prism.highlightElement(this.$refs.code);
-  },
 };
 </script>
 
 <template>
   <div :class="type">
     <prompt :type="promptType" :count="count" />
-    <pre ref="code" :class="cellCssClass" class="language-python" v-text="code"></pre>
+    <code-block-highlighted
+      language="python"
+      :code="code"
+      :max-height="maxHeight"
+      class="gl-border"
+    />
   </div>
 </template>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 5f7ef4a4377718ebdfd19dac1c5722fddb034967..88d01ffa6599fe5ff381a1cc6c712aaf8d55cf23 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -6,11 +6,6 @@ import LatexOutput from './latex.vue';
 
 export default {
   props: {
-    codeCssClass: {
-      type: String,
-      required: false,
-      default: '',
-    },
     count: {
       type: Number,
       required: false,
@@ -96,7 +91,6 @@ export default {
       :index="index"
       :raw-code="rawCode(output)"
       :metadata="metadata"
-      :code-css-class="codeCssClass"
     />
   </div>
 </template>
diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue
index 44dc1856e49b3f0a9176b377358cfdfddd949b1a..df9694b7cd82581c1bcd6ff1abef5892f84716f6 100644
--- a/app/assets/javascripts/notebook/index.vue
+++ b/app/assets/javascripts/notebook/index.vue
@@ -11,11 +11,6 @@ export default {
       type: Object,
       required: true,
     },
-    codeCssClass: {
-      type: String,
-      required: false,
-      default: '',
-    },
   },
   computed: {
     cells() {
@@ -52,7 +47,6 @@ export default {
       v-for="(cell, index) in cells"
       :key="index"
       :cell="cell"
-      :code-css-class="codeCssClass"
     />
   </div>
 </template>
diff --git a/app/assets/javascripts/notebook/lib/highlight.js b/app/assets/javascripts/notebook/lib/highlight.js
deleted file mode 100644
index 313aeecbd51e23d1b07e01d2a0fbecbc7b79e46b..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/notebook/lib/highlight.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import Prism from 'prismjs';
-import 'prismjs/components/prism-python';
-import 'prismjs/themes/prism.css';
-
-export default Prism;
diff --git a/app/assets/javascripts/vue_shared/components/code_block.stories.js b/app/assets/javascripts/vue_shared/components/code_block.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..ad53afe3676882a5a6ee139df9248861ed003eb2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_block.stories.js
@@ -0,0 +1,18 @@
+import CodeBlock from './code_block.vue';
+
+export default {
+  component: CodeBlock,
+  title: 'vue_shared/components/code_block',
+};
+
+const Template = (args, { argTypes }) => ({
+  components: { CodeBlock },
+  props: Object.keys(argTypes),
+  template: '<code-block v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+  // eslint-disable-next-line @gitlab/require-i18n-strings
+  code: `git commit -a "Message"\ngit push`,
+};
diff --git a/app/assets/javascripts/vue_shared/components/code_block.vue b/app/assets/javascripts/vue_shared/components/code_block.vue
index 9856f35c7f618e5102b09f91ef2c226b0d289688..4a69845d3a42979aa17ccab85ff4ed4a84f51ed7 100644
--- a/app/assets/javascripts/vue_shared/components/code_block.vue
+++ b/app/assets/javascripts/vue_shared/components/code_block.vue
@@ -4,7 +4,8 @@ export default {
   props: {
     code: {
       type: String,
-      required: true,
+      required: false,
+      default: '',
     },
     maxHeight: {
       type: String,
@@ -32,5 +33,5 @@ export default {
     class="code-block rounded code"
     :class="$options.userColorScheme"
     :style="styleObject"
-  ><code class="d-block">{{ code }}</code></pre>
+  ><slot><code class="d-block">{{ code }}</code></slot></pre>
 </template>
diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js b/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..1939575ae4055f3362d57436c1e3bbbe4174805f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js
@@ -0,0 +1,18 @@
+import CodeBlockHighlighted from './code_block_highlighted.vue';
+
+export default {
+  component: CodeBlockHighlighted,
+  title: 'vue_shared/components/code_block_highlighted',
+};
+
+const Template = (args, { argTypes }) => ({
+  components: { CodeBlockHighlighted },
+  props: Object.keys(argTypes),
+  template: '<code-block-highlighted v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+  code: `const foo = 1;\nconsole.log(foo + ' yay')`,
+  language: 'javascript',
+};
diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
new file mode 100644
index 0000000000000000000000000000000000000000..65b08b608e85dd76ed3c2d05225e0c32d452ccca
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
@@ -0,0 +1,72 @@
+<script>
+import { GlSafeHtmlDirective } from '@gitlab/ui';
+
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import CodeBlock from './code_block.vue';
+
+export default {
+  name: 'CodeBlockHighlighted',
+  directives: {
+    SafeHtml: GlSafeHtmlDirective,
+  },
+  components: {
+    CodeBlock,
+  },
+  props: {
+    code: {
+      type: String,
+      required: true,
+    },
+    language: {
+      type: String,
+      required: true,
+    },
+    maxHeight: {
+      type: String,
+      required: false,
+      default: 'initial',
+    },
+  },
+  data() {
+    return {
+      hljs: null,
+      languageLoaded: false,
+    };
+  },
+  computed: {
+    highlighted() {
+      if (this.hljs && this.languageLoaded) {
+        return this.hljs.highlight(this.code, { language: this.language }).value;
+      }
+
+      return this.code;
+    },
+  },
+  async mounted() {
+    this.hljs = await this.loadHighlightJS();
+    if (this.language) {
+      await this.loadLanguage();
+    }
+  },
+  methods: {
+    async loadLanguage() {
+      try {
+        const { default: languageDefinition } = await languageLoader[this.language]();
+
+        this.hljs.registerLanguage(this.language, languageDefinition);
+        this.languageLoaded = true;
+      } catch (e) {
+        this.$emit('error', e);
+      }
+    },
+    loadHighlightJS() {
+      return import('highlight.js/lib/core');
+    },
+  },
+};
+</script>
+<template>
+  <code-block :max-height="maxHeight" class="highlight">
+    <span v-safe-html="highlighted"></span>
+  </code-block>
+</template>
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index b980d7fdaa7cba9b8b35a38af84ed1e15acf1d61..cba8f48071bb1a73c354c82adf0032d849fbc6a6 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -471,11 +471,6 @@ span.idiff {
   }
 }
 
-.jupyter-notebook-scrolled {
-  overflow-y: auto;
-  max-height: 20rem;
-}
-
 #js-openapi-viewer {
   pre.version,
   code {
diff --git a/package.json b/package.json
index 580174d472098adac39eb277067caf9b03427c57..ed78775ed3c76a0368208dd3e0b688c04315e2c9 100644
--- a/package.json
+++ b/package.json
@@ -150,7 +150,6 @@
     "popper.js": "^1.16.1",
     "portal-vue": "^2.1.7",
     "postcss": "8.4.14",
-    "prismjs": "^1.21.0",
     "prosemirror-markdown": "1.9.1",
     "prosemirror-model": "^1.18.1",
     "prosemirror-state": "^1.4.1",
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
index 475c41a72f62ddcea7645dcafdb3e7c2d61f970d..b79000a3505b72a1f6fc146c32d998d98416d5ff 100644
--- a/spec/frontend/notebook/index_spec.js
+++ b/spec/frontend/notebook/index_spec.js
@@ -11,7 +11,7 @@ describe('Notebook component', () => {
 
   function buildComponent(notebook) {
     return mount(Component, {
-      propsData: { notebook, codeCssClass: 'js-code-class' },
+      propsData: { notebook },
       provide: { relativeRawPath: '' },
     }).vm;
   }
@@ -46,10 +46,6 @@ describe('Notebook component', () => {
     it('renders code cell', () => {
       expect(vm.$el.querySelector('pre')).not.toBeNull();
     });
-
-    it('add code class to code blocks', () => {
-      expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
-    });
   });
 
   describe('with worksheets', () => {
@@ -72,9 +68,5 @@ describe('Notebook component', () => {
     it('renders code cell', () => {
       expect(vm.$el.querySelector('pre')).not.toBeNull();
     });
-
-    it('add code class to code blocks', () => {
-      expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
-    });
   });
 });
diff --git a/spec/frontend/notebook/lib/highlight_spec.js b/spec/frontend/notebook/lib/highlight_spec.js
deleted file mode 100644
index 944ccd6aa9f085fa811f943e2486de6f8066141e..0000000000000000000000000000000000000000
--- a/spec/frontend/notebook/lib/highlight_spec.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Prism from '~/notebook/lib/highlight';
-
-describe('Highlight library', () => {
-  it('imports python language', () => {
-    expect(Prism.languages.python).toBeDefined();
-  });
-
-  it('uses custom CSS classes', () => {
-    const el = document.createElement('div');
-    el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript);
-
-    expect(el.querySelector('.string')).not.toBeNull();
-    expect(el.querySelector('.function')).not.toBeNull();
-  });
-});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
deleted file mode 100644
index 7f655d67ae81a3afe42388703d5437082ef6075d..0000000000000000000000000000000000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
+++ /dev/null
@@ -1,26 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Code Block with default props renders correctly 1`] = `
-<pre
-  class="code-block rounded code"
->
-  <code
-    class="d-block"
-  >
-    test-code
-  </code>
-</pre>
-`;
-
-exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = `
-<pre
-  class="code-block rounded code"
-  style="max-height: 200px; overflow-y: auto;"
->
-  <code
-    class="d-block"
-  >
-    test-code
-  </code>
-</pre>
-`;
diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..181692e61b5b92758841764bbcd90750f927fa9b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
@@ -0,0 +1,65 @@
+import { shallowMount } from '@vue/test-utils';
+import CodeBlock from '~/vue_shared/components/code_block_highlighted.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('Code Block Highlighted', () => {
+  let wrapper;
+
+  const code = 'const foo = 1;';
+
+  const createComponent = (propsData = {}) => {
+    wrapper = shallowMount(CodeBlock, { propsData });
+  };
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  it('renders highlighted code if language is supported', async () => {
+    createComponent({ code, language: 'javascript' });
+
+    await waitForPromises();
+
+    expect(wrapper.element).toMatchInlineSnapshot(`
+      <code-block-stub
+        class="highlight"
+        code=""
+        maxheight="initial"
+      >
+        <span>
+          <span
+            class="hljs-keyword"
+          >
+            const
+          </span>
+           foo = 
+          <span
+            class="hljs-number"
+          >
+            1
+          </span>
+          ;
+        </span>
+      </code-block-stub>
+    `);
+  });
+
+  it("renders plain text if language isn't supported", async () => {
+    createComponent({ code, language: 'foobar' });
+    await waitForPromises();
+
+    expect(wrapper.emitted('error')).toEqual([[expect.any(TypeError)]]);
+
+    expect(wrapper.element).toMatchInlineSnapshot(`
+      <code-block-stub
+        class="highlight"
+        code=""
+        maxheight="initial"
+      >
+        <span>
+          const foo = 1;
+        </span>
+      </code-block-stub>
+    `);
+  });
+});
diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js
index 60b0b0b566b90e7ab95b85ac843d8e1a89b48f38..9a4dbcc47ff77d87b421aaaedb83ec7960c176a3 100644
--- a/spec/frontend/vue_shared/components/code_block_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_spec.js
@@ -4,41 +4,77 @@ import CodeBlock from '~/vue_shared/components/code_block.vue';
 describe('Code Block', () => {
   let wrapper;
 
-  const defaultProps = {
-    code: 'test-code',
-  };
+  const code = 'test-code';
 
-  const createComponent = (props = {}) => {
+  const createComponent = (propsData, slots = {}) => {
     wrapper = shallowMount(CodeBlock, {
-      propsData: {
-        ...defaultProps,
-        ...props,
-      },
+      slots,
+      propsData,
     });
   };
 
   afterEach(() => {
     wrapper.destroy();
-    wrapper = null;
   });
 
-  describe('with default props', () => {
-    beforeEach(() => {
-      createComponent();
-    });
+  it('overwrites the default slot', () => {
+    createComponent({}, { default: 'DEFAULT SLOT' });
 
-    it('renders correctly', () => {
-      expect(wrapper.element).toMatchSnapshot();
-    });
+    expect(wrapper.element).toMatchInlineSnapshot(`
+        <pre
+          class="code-block rounded code"
+        >
+          DEFAULT SLOT
+        </pre>
+      `);
   });
 
-  describe('with maxHeight set to "200px"', () => {
-    beforeEach(() => {
-      createComponent({ maxHeight: '200px' });
-    });
+  it('renders with empty code prop', () => {
+    createComponent({});
 
-    it('renders correctly', () => {
-      expect(wrapper.element).toMatchSnapshot();
-    });
+    expect(wrapper.element).toMatchInlineSnapshot(`
+      <pre
+        class="code-block rounded code"
+      >
+        <code
+          class="d-block"
+        >
+          
+        </code>
+      </pre>
+    `);
+  });
+
+  it('renders code prop when provided', () => {
+    createComponent({ code });
+
+    expect(wrapper.element).toMatchInlineSnapshot(`
+        <pre
+          class="code-block rounded code"
+        >
+          <code
+            class="d-block"
+          >
+            test-code
+          </code>
+        </pre>
+      `);
+  });
+
+  it('sets maxHeight properly when provided', () => {
+    createComponent({ code, maxHeight: '200px' });
+
+    expect(wrapper.element).toMatchInlineSnapshot(`
+        <pre
+          class="code-block rounded code"
+          style="max-height: 200px; overflow-y: auto;"
+        >
+          <code
+            class="d-block"
+          >
+            test-code
+          </code>
+        </pre>
+      `);
   });
 });
diff --git a/storybook/config/preview.js b/storybook/config/preview.js
index a55d0d52a0c3bd502028d989fa2bf23a90b3e372..6f3b81907422c2e215579ddcb5312a7f141c5f4b 100644
--- a/storybook/config/preview.js
+++ b/storybook/config/preview.js
@@ -6,13 +6,16 @@ import translateMixin from '~/vue_shared/translate';
 const stylesheetsRequireCtx = require.context(
   '../../app/assets/stylesheets',
   true,
-  /(application|application_utilities)\.scss$/,
+  /(application|application_utilities|highlight\/themes\/white)\.scss$/,
 );
 
-window.gon = {};
+window.gon = {
+  user_color_scheme: 'white',
+};
 translateMixin(Vue);
 
 stylesheetsRequireCtx('./application.scss');
 stylesheetsRequireCtx('./application_utilities.scss');
+stylesheetsRequireCtx('./highlight/themes/white.scss');
 
 export const decorators = [withServer(createMockServer)];
diff --git a/yarn.lock b/yarn.lock
index 335451c2ab585f22cd4cd4e12a4537860a269f28..868ae38c64f474b8d606931487b704b8bb668a47 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3429,7 +3429,7 @@ clean-stack@^2.0.0:
   resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
   integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
 
-clipboard@^2.0.0, clipboard@^2.0.8:
+clipboard@^2.0.8:
   version "2.0.8"
   resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba"
   integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==
@@ -9731,13 +9731,6 @@ pretty@^2.0.0:
     extend-shallow "^2.0.1"
     js-beautify "^1.6.12"
 
-prismjs@^1.21.0:
-  version "1.21.0"
-  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3"
-  integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==
-  optionalDependencies:
-    clipboard "^2.0.0"
-
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"