diff --git a/app/assets/javascripts/custom_emoji/components/list.vue b/app/assets/javascripts/custom_emoji/components/list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..82d5f3320b4320d74e9a30010468f906c031c001
--- /dev/null
+++ b/app/assets/javascripts/custom_emoji/components/list.vue
@@ -0,0 +1,135 @@
+<script>
+import { GlLoadingIcon, GlTableLite, GlTabs, GlTab, GlBadge, GlKeysetPagination } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
+
+export default {
+  components: {
+    GlTableLite,
+    GlLoadingIcon,
+    GlTabs,
+    GlTab,
+    GlBadge,
+    GlKeysetPagination,
+  },
+  props: {
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    customEmojis: {
+      type: Array,
+      required: true,
+    },
+    pageInfo: {
+      type: Object,
+      required: true,
+    },
+    count: {
+      type: Number,
+      required: true,
+    },
+  },
+  methods: {
+    prevPage() {
+      this.$emit('input', {
+        before: this.pageInfo.startCursor,
+      });
+    },
+    nextPage() {
+      this.$emit('input', {
+        after: this.pageInfo.endCursor,
+      });
+    },
+    formatDate(date) {
+      return formatDate(date, 'mmmm d, yyyy');
+    },
+  },
+  primaryAction: {
+    text: __('New custom emoji'),
+    attributes: {
+      variant: 'info',
+      to: '/new',
+    },
+  },
+  fields: [
+    {
+      key: 'emoji',
+      label: __('Image'),
+      thClass: 'gl-border-t-0!',
+      tdClass: 'gl-vertical-align-middle!',
+      columnWidth: '70px',
+    },
+    {
+      key: 'name',
+      label: __('Name'),
+      thClass: 'gl-border-t-0!',
+      tdClass: 'gl-vertical-align-middle! gl-font-monospace',
+    },
+    {
+      key: 'created_at',
+      label: __('Created date'),
+      thClass: 'gl-border-t-0!',
+      tdClass: 'gl-vertical-align-middle!',
+      columnWidth: '25%',
+    },
+    {
+      key: 'action',
+      label: '',
+      thClass: 'gl-border-t-0!',
+      columnWidth: '64px',
+    },
+  ],
+};
+</script>
+
+<template>
+  <div>
+    <gl-loading-icon v-if="loading" size="lg" />
+    <template v-else>
+      <gl-tabs content-class="gl-pt-0" :action-primary="$options.primaryAction">
+        <gl-tab>
+          <template #title>
+            {{ __('Emoji') }}
+            <gl-badge size="sm" class="gl-tab-counter-badge">{{ count }}</gl-badge>
+          </template>
+          <gl-table-lite
+            :items="customEmojis"
+            :fields="$options.fields"
+            table-class="gl-table-layout-fixed"
+          >
+            <template #table-colgroup="scope">
+              <col
+                v-for="field in scope.fields"
+                :key="field.key"
+                :style="{ width: field.columnWidth }"
+              />
+            </template>
+            <template #cell(emoji)="data">
+              <gl-emoji
+                :data-name="data.item.name"
+                :data-fallback-src="data.item.url"
+                data-unicode-version="custom"
+              />
+            </template>
+            <template #cell(action)> </template>
+            <template #cell(created_at)="data">
+              {{ formatDate(data.item.createdAt) }}
+            </template>
+            <template #cell(name)="data">
+              <strong class="gl-str-truncated">:{{ data.item.name }}:</strong>
+            </template>
+          </gl-table-lite>
+          <gl-keyset-pagination
+            v-if="pageInfo.hasPreviousPage || pageInfo.hasNextPage"
+            v-bind="pageInfo"
+            class="gl-mt-4"
+            @prev="prevPage"
+            @next="nextPage"
+          />
+        </gl-tab>
+      </gl-tabs>
+    </template>
+  </div>
+</template>
diff --git a/app/assets/javascripts/custom_emoji/pages/index.vue b/app/assets/javascripts/custom_emoji/pages/index.vue
index 6d32ba41eaeeabe09ad6fdad0a62eab41c894cde..b8e9f0a62d7000e4d8f832514a32f86c1f40e945 100644
--- a/app/assets/javascripts/custom_emoji/pages/index.vue
+++ b/app/assets/javascripts/custom_emoji/pages/index.vue
@@ -1,7 +1,63 @@
 <script>
-export default {};
+import { fetchPolicies } from '~/lib/graphql';
+import customEmojisQuery from '../queries/custom_emojis.query.graphql';
+import List from '../components/list.vue';
+
+export default {
+  apollo: {
+    customEmojis: {
+      fetchPolicy: fetchPolicies.NETWORK_ONLY,
+      query: customEmojisQuery,
+      update: (r) => r.group?.customEmoji?.nodes,
+      variables() {
+        return {
+          groupPath: this.groupPath,
+          ...this.pagination,
+        };
+      },
+      result({ data }) {
+        const pageInfo = data.group?.customEmoji?.pageInfo;
+        this.count = data.group?.customEmoji?.count;
+
+        if (pageInfo) {
+          this.pageInfo = pageInfo;
+        }
+      },
+    },
+  },
+  components: {
+    List,
+  },
+  inject: {
+    groupPath: {
+      default: '',
+    },
+  },
+  data() {
+    return {
+      customEmojis: [],
+      count: 0,
+      pageInfo: {},
+      pagination: {},
+    };
+  },
+  methods: {
+    refetchCustomEmojis() {
+      this.$apollo.queries.customEmojis.refetch();
+    },
+    changePage(pageInfo) {
+      this.pagination = pageInfo;
+    },
+  },
+};
 </script>
 
 <template>
-  <div></div>
+  <list
+    :count="count"
+    :loading="$apollo.queries.customEmojis.loading"
+    :page-info="pageInfo"
+    :custom-emojis="customEmojis"
+    @input="changePage"
+  />
 </template>
diff --git a/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql b/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..5f8af6cf42d86c37cadab72c3da9c1f655fb9c31
--- /dev/null
+++ b/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql
@@ -0,0 +1,19 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getCustomEmojis($groupPath: ID!, $after: String = "", $before: String = "") {
+  group(fullPath: $groupPath) {
+    id
+    customEmoji(after: $after, before: $before) {
+      count
+      pageInfo {
+        ...PageInfo
+      }
+      nodes {
+        id
+        name
+        url
+        createdAt
+      }
+    }
+  }
+}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 834e89c6ee0b7a2ce5b1ef5ac90aa81f6cacb308..3adec4a9ea8e6d0728840a9f22c10bd6289c07b5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -17391,6 +17391,9 @@ msgstr ""
 msgid "Embed"
 msgstr ""
 
+msgid "Emoji"
+msgstr ""
+
 msgid "Empty file"
 msgstr ""
 
@@ -23561,6 +23564,9 @@ msgstr ""
 msgid "Ignored"
 msgstr ""
 
+msgid "Image"
+msgstr ""
+
 msgid "ImageDiffViewer|2-up"
 msgstr ""
 
@@ -30669,6 +30675,9 @@ msgstr ""
 msgid "New confidential issue title"
 msgstr ""
 
+msgid "New custom emoji"
+msgstr ""
+
 msgid "New deploy key"
 msgstr ""
 
diff --git a/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap b/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..4e87d4d81924b325da764d820e17d060046abb31
--- /dev/null
+++ b/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap
@@ -0,0 +1,220 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Custom emoji settings list component renders table of custom emoji 1`] = `
+<div>
+  <div
+    class="tabs gl-tabs"
+  >
+    <!---->
+    <div
+      class=""
+    >
+      <ul
+        class="nav gl-tabs-nav"
+        role="tablist"
+      >
+        <div
+          class="gl-actions-tabs-start"
+          data-testid="actions-tabs-start"
+        >
+          <a
+            class="btn btn-info btn-md gl-button"
+            data-testid="action-primary"
+            href="/new"
+            to="/new"
+          >
+            <!---->
+             
+            <!---->
+              
+            <span
+              class="gl-button-text"
+            >
+              
+        New custom emoji
+      
+            </span>
+          </a>
+           
+          <!---->
+           
+          <!---->
+        </div>
+        <div
+          class="gl-actions-tabs-end"
+          data-testid="actions-tabs-end"
+        >
+          <a
+            class="btn btn-info btn-md gl-button"
+            data-testid="action-primary"
+            href="/new"
+            to="/new"
+          >
+            <!---->
+             
+            <!---->
+              
+            <span
+              class="gl-button-text"
+            >
+              
+        New custom emoji
+      
+            </span>
+          </a>
+           
+          <!---->
+           
+          <!---->
+        </div>
+      </ul>
+    </div>
+    <div
+      class="tab-content gl-pt-0 gl-tab-content"
+    >
+      <transition-stub
+        css="true"
+        enteractiveclass=""
+        enterclass=""
+        entertoclass="show"
+        leaveactiveclass=""
+        leaveclass="show"
+        leavetoclass=""
+        mode="out-in"
+        name=""
+      >
+        <div
+          aria-hidden="true"
+          class="tab-pane"
+          role="tabpanel"
+          style="display: none;"
+        >
+           
+          <table
+            aria-busy=""
+            aria-colcount="4"
+            class="table b-table gl-table gl-table-layout-fixed"
+            role="table"
+          >
+            <!---->
+            <colgroup>
+              <col
+                style="width: 70px;"
+              />
+              <col />
+              <col
+                style="width: 25%;"
+              />
+              <col
+                style="width: 64px;"
+              />
+            </colgroup>
+            <thead
+              class=""
+              role="rowgroup"
+            >
+              <!---->
+              <tr
+                class=""
+                role="row"
+              >
+                <th
+                  aria-colindex="1"
+                  class="gl-border-t-0!"
+                  role="columnheader"
+                  scope="col"
+                >
+                  <div>
+                    Image
+                  </div>
+                </th>
+                <th
+                  aria-colindex="2"
+                  class="gl-border-t-0!"
+                  role="columnheader"
+                  scope="col"
+                >
+                  <div>
+                    Name
+                  </div>
+                </th>
+                <th
+                  aria-colindex="3"
+                  class="gl-border-t-0!"
+                  role="columnheader"
+                  scope="col"
+                >
+                  <div>
+                    Created date
+                  </div>
+                </th>
+                <th
+                  aria-colindex="4"
+                  aria-label="Action"
+                  class="gl-border-t-0!"
+                  role="columnheader"
+                  scope="col"
+                >
+                  <div />
+                </th>
+              </tr>
+            </thead>
+            <tbody
+              role="rowgroup"
+            >
+              <!---->
+              <tr
+                class=""
+                role="row"
+              >
+                <td
+                  aria-colindex="1"
+                  class="gl-vertical-align-middle!"
+                  role="cell"
+                >
+                  <gl-emoji
+                    data-fallback-src="https://gitlab.com/custom_emoji/custom_emoji/-/raw/main/img/confused_husky.gif"
+                    data-name="confused_husky"
+                    data-unicode-version="custom"
+                  />
+                </td>
+                <td
+                  aria-colindex="2"
+                  class="gl-vertical-align-middle! gl-font-monospace"
+                  role="cell"
+                >
+                  <strong
+                    class="gl-str-truncated"
+                  >
+                    :confused_husky:
+                  </strong>
+                </td>
+                <td
+                  aria-colindex="3"
+                  class="gl-vertical-align-middle!"
+                  role="cell"
+                >
+                  
+            created-at
+          
+                </td>
+                <td
+                  aria-colindex="4"
+                  class=""
+                  role="cell"
+                />
+              </tr>
+              <!---->
+              <!---->
+            </tbody>
+            <!---->
+          </table>
+           
+          <!---->
+        </div>
+      </transition-stub>
+      <!---->
+    </div>
+  </div>
+</div>
+`;
diff --git a/spec/frontend/custom_emoji/components/list_spec.js b/spec/frontend/custom_emoji/components/list_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ac5219e375b0f1b70b55c3217f571584a6ee2d18
--- /dev/null
+++ b/spec/frontend/custom_emoji/components/list_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import List from '~/custom_emoji/components/list.vue';
+import { CUSTOM_EMOJI } from '../mock_data';
+
+jest.mock('~/lib/utils/datetime/date_format_utility', () => ({
+  formatDate: (date) => date,
+}));
+
+Vue.config.ignoredElements = ['gl-emoji'];
+
+let wrapper;
+
+function createComponent(propsData = {}) {
+  wrapper = mountExtended(List, {
+    propsData: {
+      customEmojis: CUSTOM_EMOJI,
+      pageInfo: {},
+      count: CUSTOM_EMOJI.length,
+      ...propsData,
+    },
+  });
+}
+
+describe('Custom emoji settings list component', () => {
+  it('renders table of custom emoji', () => {
+    createComponent();
+
+    expect(wrapper.element).toMatchSnapshot();
+  });
+
+  describe('pagination', () => {
+    it.each`
+      emits                        | button          | pageInfo
+      ${{ before: 'startCursor' }} | ${'prevButton'} | ${{ hasPreviousPage: true, startCursor: 'startCursor' }}
+      ${{ after: 'endCursor' }}    | ${'nextButton'} | ${{ hasNextPage: true, endCursor: 'endCursor' }}
+    `('emits $emits when $button is clicked', async ({ emits, button, pageInfo }) => {
+      createComponent({ pageInfo });
+
+      await wrapper.findByTestId(button).vm.$emit('click');
+
+      expect(wrapper.emitted('input')[0]).toEqual([emits]);
+    });
+  });
+});
diff --git a/spec/frontend/custom_emoji/mock_data.js b/spec/frontend/custom_emoji/mock_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..19ba7022bf14b1f9e552103600b45ac608b1ca24
--- /dev/null
+++ b/spec/frontend/custom_emoji/mock_data.js
@@ -0,0 +1,8 @@
+export const CUSTOM_EMOJI = [
+  {
+    id: 'gid://gitlab/CustomEmoji/1',
+    name: 'confused_husky',
+    url: 'https://gitlab.com/custom_emoji/custom_emoji/-/raw/main/img/confused_husky.gif',
+    createdAt: 'created-at',
+  },
+];