diff --git a/app/assets/javascripts/merge_request_dashboard/components/collapsible_section.stories.js b/app/assets/javascripts/merge_request_dashboard/components/collapsible_section.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..0db6f4642b9a1cd8e35a789f679aa27970367b1e
--- /dev/null
+++ b/app/assets/javascripts/merge_request_dashboard/components/collapsible_section.stories.js
@@ -0,0 +1,20 @@
+import CollapsibleSection from './collapsible_section.vue';
+
+const Template = (_, { argTypes }) => {
+  return {
+    components: { CollapsibleSection },
+    props: Object.keys(argTypes),
+    template: '<collapsible-section v-bind="$props">Opened</collapsible-section>',
+  };
+};
+
+export default {
+  component: CollapsibleSection,
+  title: 'merge_requests_dashboard/collapsible_section',
+};
+
+export const Default = Template.bind({});
+Default.args = { title: 'Approved', count: 3 };
+
+export const ClosedByDefault = Template.bind({});
+ClosedByDefault.args = { ...Default.args, count: 0 };
diff --git a/app/assets/javascripts/merge_request_dashboard/components/collapsible_section.vue b/app/assets/javascripts/merge_request_dashboard/components/collapsible_section.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c77eae6f16e065f04606b06234e98a03f96e8e59
--- /dev/null
+++ b/app/assets/javascripts/merge_request_dashboard/components/collapsible_section.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlButton, GlBadge } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+
+export default {
+  components: {
+    GlButton,
+    GlBadge,
+  },
+  props: {
+    title: {
+      type: String,
+      required: true,
+    },
+    count: {
+      type: Number,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      open: this.count > 0,
+    };
+  },
+  computed: {
+    toggleButtonIcon() {
+      return this.open ? 'chevron-down' : 'chevron-right';
+    },
+    toggleButtonLabel() {
+      return sprintf(
+        this.open
+          ? __('Collapse %{section} merge requests')
+          : __('Expand %{section} merge requests'),
+        {
+          section: this.title.toLowerCase(),
+        },
+      );
+    },
+  },
+  methods: {
+    toggleOpen() {
+      this.open = !this.open;
+    },
+  },
+};
+</script>
+
+<template>
+  <section class="gl-bg-gray-50 gl-p-4 gl-rounded-base">
+    <header class="gl-display-flex gl-align-items-center">
+      <gl-button
+        :icon="toggleButtonIcon"
+        size="small"
+        category="tertiary"
+        class="gl-mr-3"
+        :aria-label="toggleButtonLabel"
+        data-testid="section-toggle-button"
+        @click="toggleOpen"
+      />
+      <strong>{{ title }}</strong>
+      <gl-badge class="gl-ml-3" variant="neutral" size="sm">{{ count }}</gl-badge>
+    </header>
+    <div v-if="open" class="gl-mt-3" data-testid="section-content">
+      <slot></slot>
+    </div>
+  </section>
+</template>
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d1f4bf53cdd5086b96151911d01eab3cd1ad6eb9..f5a2710ba2bf6d4a64b53c267cb5b879961aed0d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -12630,6 +12630,9 @@ msgstr ""
 msgid "Collapse"
 msgstr ""
 
+msgid "Collapse %{section} merge requests"
+msgstr ""
+
 msgid "Collapse eligible approvers"
 msgstr ""
 
@@ -21082,6 +21085,9 @@ msgstr ""
 msgid "Expand"
 msgstr ""
 
+msgid "Expand %{section} merge requests"
+msgstr ""
+
 msgid "Expand Readme"
 msgstr ""
 
diff --git a/spec/frontend/merge_request_dashboard/components/__snapshots__/collapsible_section_spec.js.snap b/spec/frontend/merge_request_dashboard/components/__snapshots__/collapsible_section_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..66e40e29b7610535f49f9ea08c13f39a85611d78
--- /dev/null
+++ b/spec/frontend/merge_request_dashboard/components/__snapshots__/collapsible_section_spec.js.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Merge request dashboard collapsible section renders section 1`] = `
+<section
+  class="gl-bg-gray-50 gl-p-4 gl-rounded-base"
+>
+  <header
+    class="gl-align-items-center gl-display-flex"
+  >
+    <gl-button-stub
+      aria-label="Collapse approved merge requests"
+      buttontextclasses=""
+      category="tertiary"
+      class="gl-mr-3"
+      data-testid="section-toggle-button"
+      icon="chevron-down"
+      size="small"
+      variant="default"
+    />
+    <strong>
+      Approved
+    </strong>
+    <gl-badge-stub
+      class="gl-ml-3"
+      iconsize="md"
+      size="sm"
+      variant="neutral"
+    >
+      3
+    </gl-badge-stub>
+  </header>
+  <div
+    class="gl-mt-3"
+    data-testid="section-content"
+  >
+    content
+  </div>
+</section>
+`;
diff --git a/spec/frontend/merge_request_dashboard/components/collapsible_section_spec.js b/spec/frontend/merge_request_dashboard/components/collapsible_section_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b7923d3cef0d5fbfd5780eaf5a453920f4ddcce3
--- /dev/null
+++ b/spec/frontend/merge_request_dashboard/components/collapsible_section_spec.js
@@ -0,0 +1,42 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CollapsibleSection from '~/merge_request_dashboard/components/collapsible_section.vue';
+
+describe('Merge request dashboard collapsible section', () => {
+  let wrapper;
+
+  function createComponent(count = 3) {
+    wrapper = shallowMountExtended(CollapsibleSection, {
+      slots: {
+        default: 'content',
+      },
+      propsData: {
+        title: 'Approved',
+        count,
+      },
+    });
+  }
+
+  it('renders section', () => {
+    createComponent();
+
+    expect(wrapper.element).toMatchSnapshot();
+  });
+
+  it('collapses by default', () => {
+    createComponent(0);
+
+    expect(wrapper.findByTestId('section-content').exists()).toBe(false);
+  });
+
+  it('expands collapsed content', async () => {
+    createComponent(0);
+
+    wrapper.findByTestId('section-toggle-button').vm.$emit('click');
+
+    await nextTick();
+
+    expect(wrapper.findByTestId('section-content').exists()).toBe(true);
+    expect(wrapper.findByTestId('section-content').text()).toContain('content');
+  });
+});