diff --git a/app/assets/javascripts/repository/components/table/header.vue b/app/assets/javascripts/repository/components/table/header.vue
new file mode 100644
index 0000000000000000000000000000000000000000..9d30aa8815584f5ace2e2afed6d709d0e6190b76
--- /dev/null
+++ b/app/assets/javascripts/repository/components/table/header.vue
@@ -0,0 +1,9 @@
+<template>
+  <thead>
+    <tr>
+      <th id="name" scope="col">{{ s__('ProjectFileTree|Name') }}</th>
+      <th id="last-commit" scope="col" class="d-none d-sm-table-cell">{{ __('Last commit') }}</th>
+      <th id="last-update" scope="col" class="text-right">{{ __('Last update') }}</th>
+    </tr>
+  </thead>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7119861c7b3bbc04a3f71e8796883cce47c004b3
--- /dev/null
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { sprintf, __ } from '../../../locale';
+import getRefMixin from '../../mixins/get_ref';
+import getFiles from '../../queries/getFiles.graphql';
+import TableHeader from './header.vue';
+
+export default {
+  components: {
+    GlLoadingIcon,
+    TableHeader,
+  },
+  mixins: [getRefMixin],
+  apollo: {
+    files: {
+      query: getFiles,
+      variables() {
+        return {
+          ref: this.ref,
+          path: this.path,
+        };
+      },
+    },
+  },
+  props: {
+    path: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      files: [],
+    };
+  },
+  computed: {
+    tableCaption() {
+      return sprintf(
+        __('Files, directories, and submodules in the path %{path} for commit reference %{ref}'),
+        { path: this.path, ref: this.ref },
+      );
+    },
+    isLoadingFiles() {
+      return this.$apollo.queries.files.loading;
+    },
+  },
+};
+</script>
+
+<template>
+  <div class="tree-content-holder">
+    <div class="table-holder bordered-box">
+      <table class="table tree-table qa-file-tree" aria-live="polite">
+        <caption class="sr-only">
+          {{
+            tableCaption
+          }}
+        </caption>
+        <table-header />
+        <tbody></tbody>
+      </table>
+      <gl-loading-icon v-if="isLoadingFiles" class="my-3" size="md" />
+    </div>
+  </div>
+</template>
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index febfcce780cff5056b1ba5608c01f2f67b059598..0aedc73fc1224e5b6bb2674be0ae8bbb445c6c82 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -4,7 +4,13 @@ import createDefaultClient from '~/lib/graphql';
 
 Vue.use(VueApollo);
 
-const defaultClient = createDefaultClient({});
+const defaultClient = createDefaultClient({
+  Query: {
+    files() {
+      return [];
+    },
+  },
+});
 
 export default new VueApollo({
   defaultClient,
diff --git a/app/assets/javascripts/repository/mixins/get_ref.js b/app/assets/javascripts/repository/mixins/get_ref.js
new file mode 100644
index 0000000000000000000000000000000000000000..b06087d6f42e3bc1e86502505087952f3e368e8b
--- /dev/null
+++ b/app/assets/javascripts/repository/mixins/get_ref.js
@@ -0,0 +1,14 @@
+import getRef from '../queries/getRef.graphql';
+
+export default {
+  apollo: {
+    ref: {
+      query: getRef,
+    },
+  },
+  data() {
+    return {
+      ref: '',
+    };
+  },
+};
diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue
index fdbf195f0f61adb5577d01c0c88978dc343301c2..2d92e9174ca52443534f3e0bf835c9f9b0b72819 100644
--- a/app/assets/javascripts/repository/pages/index.vue
+++ b/app/assets/javascripts/repository/pages/index.vue
@@ -1,11 +1,9 @@
 <script>
-import getRef from '../queries/getRef.graphql';
+import FileTable from '../components/table/index.vue';
 
 export default {
-  apollo: {
-    ref: {
-      query: getRef,
-    },
+  components: {
+    FileTable,
   },
   data() {
     return {
@@ -16,9 +14,5 @@ export default {
 </script>
 
 <template>
-  <div>
-    <router-link :to="{ path: `/tree/${ref}/app` }">
-      Go to tree
-    </router-link>
-  </div>
+  <file-table path="/" />
 </template>
diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue
index f51aafee775573b1f6e4465f688de6ac3a2e628c..413102b2cd36645b011f0c37ee9587be552a84b5 100644
--- a/app/assets/javascripts/repository/pages/tree.vue
+++ b/app/assets/javascripts/repository/pages/tree.vue
@@ -1,5 +1,10 @@
 <script>
+import FileTable from '../components/table/index.vue';
+
 export default {
+  component: {
+    FileTable,
+  },
   props: {
     path: {
       type: String,
@@ -11,5 +16,5 @@ export default {
 </script>
 
 <template>
-  <div>{{ path }}</div>
+  <file-table :path="path" />
 </template>
diff --git a/app/assets/javascripts/repository/queries/getFiles.graphql b/app/assets/javascripts/repository/queries/getFiles.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..5ceaf67ea82f45a701d0d81ab76547c2bf296d9c
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getFiles.graphql
@@ -0,0 +1,8 @@
+query getFiles($path: String!, $ref: String!) {
+  files(path: $path, ref: $ref) @client {
+    id
+    name
+    fullPath
+    type
+  }
+}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6021d16bb5c4cb4944cf96a6baa8e393fda2ed22..1234dbbf547af9ecaa281c49f0edde3f16c3d17b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5359,6 +5359,9 @@ msgstr ""
 msgid "Files"
 msgstr ""
 
+msgid "Files, directories, and submodules in the path %{path} for commit reference %{ref}"
+msgstr ""
+
 msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
 msgstr ""
 
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6f52cffe0773251a7333cdb82ac92d3c76f726dd
--- /dev/null
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Table from '~/repository/components/table/index.vue';
+
+let vm;
+
+function factory(path, loading = false) {
+  vm = shallowMount(Table, {
+    propsData: {
+      path,
+    },
+    mocks: {
+      $apollo: {
+        queries: {
+          files: { loading },
+        },
+      },
+    },
+  });
+}
+
+describe('Repository table component', () => {
+  afterEach(() => {
+    vm.destroy();
+  });
+
+  it.each`
+    path            | ref
+    ${'/'}          | ${'master'}
+    ${'app/assets'} | ${'master'}
+    ${'/'}          | ${'test'}
+  `('renders table caption for $ref in $path', ({ path, ref }) => {
+    factory(path);
+
+    vm.setData({ ref });
+
+    expect(vm.find('caption').text()).toEqual(
+      `Files, directories, and submodules in the path ${path} for commit reference ${ref}`,
+    );
+  });
+
+  it('renders loading icon', () => {
+    factory('/', true);
+
+    expect(vm.find(GlLoadingIcon).exists()).toBe(true);
+  });
+});