diff --git a/app/assets/javascripts/projects/your_work/components/app.vue b/app/assets/javascripts/projects/your_work/components/app.vue
index 2ff12e61d5e725965f72d1b70e46eaccf32ca0f7..4ad6fa994ca1d413ad12365303772308b50ce76e 100644
--- a/app/assets/javascripts/projects/your_work/components/app.vue
+++ b/app/assets/javascripts/projects/your_work/components/app.vue
@@ -1,16 +1,78 @@
 <script>
+import { GlTabs, GlTab, GlBadge, GlSprintf } from '@gitlab/ui';
 import { __ } from '~/locale';
+import { joinPaths, updateHistory, pathSegments } from '~/lib/utils/url_utility';
+import { PROJECT_DASHBOARD_TABS, CONTRIBUTED_TAB } from 'ee_else_ce/projects/your_work/constants';
 
 export default {
   name: 'YourWorkProjectsApp',
   i18n: {
-    listText: __('Projects list'),
+    heading: __('Projects'),
+    activeTab: __('Active tab: %{tab}'),
+  },
+  components: {
+    GlTabs,
+    GlTab,
+    GlBadge,
+    GlSprintf,
+  },
+  data() {
+    return {
+      activeTabIndex: 0,
+    };
+  },
+  computed: {
+    formattedTabs() {
+      return PROJECT_DASHBOARD_TABS.map((tab) => ({ ...tab, count: 0 }));
+    },
+  },
+  created() {
+    this.getTabFromUrl();
+  },
+  methods: {
+    getTabFromUrl() {
+      const tab = pathSegments(window.location)?.pop();
+      const tabIndex = PROJECT_DASHBOARD_TABS.findIndex(({ value }) => value === tab);
+
+      this.activeTabIndex = tabIndex > 0 ? tabIndex : 0;
+    },
+    setTabInUrl() {
+      const tab = PROJECT_DASHBOARD_TABS[this.activeTabIndex] || CONTRIBUTED_TAB;
+      const url = joinPaths(gon.relative_url_root || '/', `/dashboard/projects/${tab.value}`);
+
+      updateHistory({ url, replace: true });
+    },
+    onTabUpdate(index) {
+      // This return will prevent us overwriting the root `/` and `/dashboard/projects` paths
+      // when we don't need to.
+      if (index === this.activeTabIndex) return;
+
+      this.activeTabIndex = index;
+      this.setTabInUrl();
+    },
   },
 };
 </script>
 
 <template>
   <div>
-    <p>{{ $options.i18n.listText }}</p>
+    <h1 class="page-title gl-font-size-h-display gl-mt-5">{{ $options.i18n.heading }}</h1>
+
+    <gl-tabs :value="activeTabIndex" @input="onTabUpdate">
+      <gl-tab v-for="tab in formattedTabs" :key="tab.text">
+        <template #title>
+          <span data-testid="projects-dashboard-tab-title">
+            <span>{{ tab.text }}</span>
+            <gl-badge size="sm" class="gl-tab-counter-badge">{{ tab.count }}</gl-badge>
+          </span>
+        </template>
+
+        <gl-sprintf :message="$options.i18n.activeTab">
+          <template #tab>
+            {{ tab.text }}
+          </template>
+        </gl-sprintf>
+      </gl-tab>
+    </gl-tabs>
   </div>
 </template>
diff --git a/app/assets/javascripts/projects/your_work/constants.js b/app/assets/javascripts/projects/your_work/constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..1bb0b5d502fe1dbde1135595bb709a2c5c14f2c9
--- /dev/null
+++ b/app/assets/javascripts/projects/your_work/constants.js
@@ -0,0 +1,23 @@
+import { __ } from '~/locale';
+
+export const CONTRIBUTED_TAB = {
+  text: __('Contributed'),
+  value: 'contributed',
+};
+
+export const STARRED_TAB = {
+  text: __('Starred'),
+  value: 'starred',
+};
+
+export const PERSONAL_TAB = {
+  text: __('Personal'),
+  value: 'personal',
+};
+
+export const MEMBER_TAB = {
+  text: __('Member'),
+  value: 'member',
+};
+
+export const PROJECT_DASHBOARD_TABS = [CONTRIBUTED_TAB, STARRED_TAB, PERSONAL_TAB, MEMBER_TAB];
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 3e303963f8092230e72955b75536dc84553a589e..2780ba121a23497d300e91ba996eb992018d6f2f 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -8,12 +8,13 @@
 - add_page_specific_style 'page_bundles/projects'
 
 = render "projects/last_push"
-- if show_projects?(@projects, params)
-  = render 'dashboard/projects_head'
-  = render 'nav'
-  - if Feature.enabled?(:your_work_projects_vue, current_user)
-    #js-your-work-projects-app
-  - else
-    = render 'projects'
+
+- if Feature.enabled?(:your_work_projects_vue, current_user)
+  #js-your-work-projects-app
 - else
-  = render "zero_authorized_projects"
+  - if show_projects?(@projects, params)
+    = render 'dashboard/projects_head'
+    = render 'nav'
+    = render 'projects'
+  - else
+    = render "zero_authorized_projects"
diff --git a/app/views/dashboard/projects/shared/_common.html.haml b/app/views/dashboard/projects/shared/_common.html.haml
index d07d55ca7c4d592cbd47a96a0f8ea024f848d78a..e91c430dc98d9ee141289b8c259e3399134ff76a 100644
--- a/app/views/dashboard/projects/shared/_common.html.haml
+++ b/app/views/dashboard/projects/shared/_common.html.haml
@@ -3,12 +3,13 @@
 = render_dashboard_ultimate_trial(current_user)
 
 = render "projects/last_push"
-= render 'dashboard/projects_head', project_tab_filter: :starred
 
-- if params[:filter_projects] || any_projects?(@projects)
-  - if Feature.enabled?(:your_work_projects_vue, current_user)
-    #js-your-work-projects-app
-  - else
-    = render 'projects'
+- if Feature.enabled?(:your_work_projects_vue, current_user)
+  #js-your-work-projects-app
 - else
-  = render empty_page
+  = render 'dashboard/projects_head', project_tab_filter: :starred
+
+  - if params[:filter_projects] || any_projects?(@projects)
+    = render 'projects'
+  - else
+    = render empty_page
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index 6a3aa5ff0c1fbb9b9845898f0b558b9621535932..5c5f063b2af86a4c35a7116d82e845a8d4878a04 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -25,7 +25,12 @@
 
     resources :projects, only: [:index] do
       collection do
+        ## TODO: Migrate this over to to: 'projects#index' as part of `:your_work_projects_vue` FF rollout
+        ## https://gitlab.com/gitlab-org/gitlab/-/issues/465889
         get :starred
+        get :contributed, to: 'projects#index'
+        get :personal, to: 'projects#index'
+        get :member, to: 'projects#index'
       end
     end
   end
diff --git a/ee/app/assets/javascripts/projects/your_work/constants.js b/ee/app/assets/javascripts/projects/your_work/constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..ed924e531355d9e543023a32e28f43e11dce805f
--- /dev/null
+++ b/ee/app/assets/javascripts/projects/your_work/constants.js
@@ -0,0 +1,16 @@
+import { PROJECT_DASHBOARD_TABS as PROJECT_DASHBOARD_TABS_CE } from '~/projects/your_work/constants';
+
+import { __ } from '~/locale';
+
+// Exports override for EE
+// eslint-disable-next-line import/export
+export * from '~/projects/your_work/constants';
+
+export const INACTIVE_TAB = {
+  text: __('Inactive'),
+  value: 'removed',
+};
+
+// Exports override for EE
+// eslint-disable-next-line import/export
+export const PROJECT_DASHBOARD_TABS = [...PROJECT_DASHBOARD_TABS_CE, INACTIVE_TAB];
diff --git a/ee/config/routes/dashboard.rb b/ee/config/routes/dashboard.rb
index a66cf2385f2444c28d02ee7a8b338d54c56b32ea..145d19f4fb780454d0bca31636be593272fff671 100644
--- a/ee/config/routes/dashboard.rb
+++ b/ee/config/routes/dashboard.rb
@@ -4,6 +4,8 @@
   scope module: :dashboard do
     resources :projects, only: [:index] do
       collection do
+        ## TODO: Migrate this over to to: 'projects#index' as part of `:your_work_projects_vue` FF rollout
+        ## https://gitlab.com/gitlab-org/gitlab/-/issues/465889
         get :removed
       end
     end
diff --git a/ee/spec/frontend/projects/your_work/components/app_spec.js b/ee/spec/frontend/projects/your_work/components/app_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd7bf258b11ede798d7df8220c6f43dab05e1225
--- /dev/null
+++ b/ee/spec/frontend/projects/your_work/components/app_spec.js
@@ -0,0 +1,39 @@
+import { GlTab, GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import YourWorkProjectsApp from '~/projects/your_work/components/app.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+
+jest.mock('~/alert');
+
+describe('YourWorkProjectsApp', () => {
+  let wrapper;
+
+  const createComponent = () => {
+    wrapper = shallowMountExtended(YourWorkProjectsApp, {
+      stubs: {
+        GlSprintf,
+        GlTab,
+      },
+    });
+  };
+
+  describe.each`
+    path                             | expectedIndex
+    ${'/dashboard/projects/removed'} | ${4}
+  `('onMount when path is $path', ({ path, expectedIndex }) => {
+    useMockLocationHelper();
+    beforeEach(async () => {
+      delete window.location;
+      window.location = new URL(`${TEST_HOST}/${path}`);
+
+      createComponent();
+      await nextTick();
+    });
+
+    it('initializes to the correct tab', () => {
+      expect(wrapper.vm.activeTabIndex).toBe(expectedIndex);
+    });
+  });
+});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 651d5b8d3147b8922b8522ce8ef15934cb163705..68f3b411b0b72f1029cbf6e9a52e261c41fd1920 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2976,6 +2976,9 @@ msgstr ""
 msgid "Active project access tokens"
 msgstr ""
 
+msgid "Active tab: %{tab}"
+msgstr ""
+
 msgid "Activity"
 msgstr ""
 
@@ -14681,6 +14684,9 @@ msgstr ""
 msgid "Contribute to GitLab"
 msgstr ""
 
+msgid "Contributed"
+msgstr ""
+
 msgid "Contribution"
 msgstr ""
 
@@ -38261,6 +38267,9 @@ msgstr ""
 msgid "Permissions and project features"
 msgstr ""
 
+msgid "Personal"
+msgstr ""
+
 msgid "Personal Access Token"
 msgstr ""
 
@@ -42224,9 +42233,6 @@ msgstr ""
 msgid "Projects in this group can use Git LFS"
 msgstr ""
 
-msgid "Projects list"
-msgstr ""
-
 msgid "Projects shared with %{group_name}"
 msgstr ""
 
@@ -51236,6 +51242,9 @@ msgstr ""
 msgid "Star labels to start sorting by priority."
 msgstr ""
 
+msgid "Starred"
+msgstr ""
+
 msgid "Starred Projects"
 msgstr ""
 
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index d8364ec09442fe390e01323a05c52ecf338d2375..0af3199fd32f368cfd709aa285bde8406b08e730 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -17,11 +17,11 @@
       stub_feature_flags(your_work_projects_vue: true)
     end
 
-    it 'mounts JS app' do
+    it 'mounts JS app and defaults to contributed tab' do
       visit dashboard_projects_path
 
       expect(page).to have_content('Projects')
-      expect(page).to have_content('Projects list')
+      expect(page).to have_content('Active tab: Contributed')
     end
   end
 
@@ -34,7 +34,7 @@
       visit dashboard_projects_path
 
       expect(page).to have_content('Projects')
-      expect(page).not_to have_content('Projects list')
+      expect(page).not_to have_content('Active tab')
     end
 
     it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do
diff --git a/spec/frontend/projects/your_work/components/app_spec.js b/spec/frontend/projects/your_work/components/app_spec.js
index 30b36938afee3d177fa86bc8e3dc42ed1187b304..a60b9971087348f0676dd4be658a5730807bed83 100644
--- a/spec/frontend/projects/your_work/components/app_spec.js
+++ b/spec/frontend/projects/your_work/components/app_spec.js
@@ -1,24 +1,145 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { nextTick } from 'vue';
+import { GlTabs } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { updateHistory } from '~/lib/utils/url_utility';
 import YourWorkProjectsApp from '~/projects/your_work/components/app.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import {
+  PROJECT_DASHBOARD_TABS,
+  CONTRIBUTED_TAB,
+  STARRED_TAB,
+  PERSONAL_TAB,
+  MEMBER_TAB,
+} from 'ee_else_ce/projects/your_work/constants';
 
-jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+  ...jest.requireActual('~/lib/utils/url_utility'),
+  updateHistory: jest.fn(),
+}));
 
 describe('YourWorkProjectsApp', () => {
   let wrapper;
 
   const createComponent = () => {
-    wrapper = shallowMountExtended(YourWorkProjectsApp);
+    wrapper = mountExtended(YourWorkProjectsApp);
   };
 
-  const findPageText = () => wrapper.find('p');
+  const findPageTitle = () => wrapper.find('h1');
+  const findGlTabs = () => wrapper.findComponent(GlTabs);
+  const findAllTabTitles = () => wrapper.findAllByTestId('projects-dashboard-tab-title');
+  const findActiveTab = () => wrapper.find('.tab-pane.active');
 
   describe('template', () => {
     beforeEach(() => {
       createComponent();
     });
 
-    it('renders Vue app with Projects list p tag', () => {
-      expect(findPageText().text()).toBe('Projects list');
+    it('renders Vue app with Projects h1 tag', () => {
+      expect(findPageTitle().text()).toBe('Projects');
+    });
+
+    it('renders all expected tabs with counts', () => {
+      const wrapperTabTitles = findAllTabTitles().wrappers.map((w) => w.text().replace(/ /g, ''));
+      const expectedTabTitles = PROJECT_DASHBOARD_TABS.map(({ text }) => `${text}0`);
+
+      expect(wrapperTabTitles).toStrictEqual(expectedTabTitles);
+    });
+
+    it('defaults to Contributed tab as active', () => {
+      expect(findActiveTab().text()).toContain('Contributed');
+    });
+  });
+
+  describe.each`
+    path                                 | expectedTab
+    ${'/'}                               | ${CONTRIBUTED_TAB}
+    ${'/dashboard'}                      | ${CONTRIBUTED_TAB}
+    ${'/dashboard/projects'}             | ${CONTRIBUTED_TAB}
+    ${'/dashboard/projects/contributed'} | ${CONTRIBUTED_TAB}
+    ${'/dashboard/projects/starred'}     | ${STARRED_TAB}
+    ${'/dashboard/projects/personal'}    | ${PERSONAL_TAB}
+    ${'/dashboard/projects/member'}      | ${MEMBER_TAB}
+    ${'/dashboard/projects/fake'}        | ${CONTRIBUTED_TAB}
+  `('onMount when path is $path', ({ path, expectedTab }) => {
+    useMockLocationHelper();
+    beforeEach(() => {
+      delete window.location;
+      window.location = new URL(`${TEST_HOST}/${path}`);
+
+      createComponent();
+    });
+
+    it('initializes to the correct tab', () => {
+      expect(findActiveTab().text()).toContain(expectedTab.text);
+    });
+  });
+
+  describe('onTabUpdate', () => {
+    describe('when tab is already active', () => {
+      beforeEach(() => {
+        createComponent();
+      });
+
+      it('does not update the url path', async () => {
+        findGlTabs().vm.$emit('input', 0);
+
+        await nextTick();
+
+        expect(updateHistory).not.toHaveBeenCalled();
+      });
+    });
+
+    describe('when tab is a valid tab', () => {
+      beforeEach(() => {
+        createComponent();
+      });
+
+      it('updates the url path correctly', async () => {
+        findGlTabs().vm.$emit('input', 2);
+
+        await nextTick();
+
+        expect(updateHistory).toHaveBeenCalledWith({
+          url: `/dashboard/projects/${PROJECT_DASHBOARD_TABS[2].value}`,
+          replace: true,
+        });
+      });
+    });
+
+    describe('when tab is an invalid tab', () => {
+      beforeEach(() => {
+        createComponent();
+      });
+
+      it('update the url path with the default Contributed tab', async () => {
+        findGlTabs().vm.$emit('input', 100);
+
+        await nextTick();
+
+        expect(updateHistory).toHaveBeenCalledWith({
+          url: `/dashboard/projects/${CONTRIBUTED_TAB.value}`,
+          replace: true,
+        });
+      });
+    });
+
+    describe('when gon.relative_url_root is set', () => {
+      beforeEach(() => {
+        gon.relative_url_root = '/gitlab';
+        createComponent();
+      });
+
+      it('update the url path correctly with relative url', async () => {
+        findGlTabs().vm.$emit('input', 3);
+
+        await nextTick();
+
+        expect(updateHistory).toHaveBeenCalledWith({
+          url: `/gitlab/dashboard/projects/${PROJECT_DASHBOARD_TABS[3].value}`,
+          replace: true,
+        });
+      });
     });
   });
 });
diff --git a/spec/views/dashboard/projects/index.html.haml_spec.rb b/spec/views/dashboard/projects/index.html.haml_spec.rb
index 3d9f988c4d2c1089bf9673818dfacad1fe403b9c..112766cb5c07da1120a394ffeb536d7b6193d449 100644
--- a/spec/views/dashboard/projects/index.html.haml_spec.rb
+++ b/spec/views/dashboard/projects/index.html.haml_spec.rb
@@ -37,11 +37,11 @@
         render
       end
 
-      it 'does not render #js-your-work-projects-app and renders empty state' do
+      it 'renders #js-your-work-projects-app and does not render HAML empty state' do
         render
 
-        expect(rendered).not_to have_selector('#js-your-work-projects-app')
-        expect(rendered).to render_template('dashboard/projects/_zero_authorized_projects')
+        expect(rendered).to have_selector('#js-your-work-projects-app')
+        expect(rendered).not_to render_template('dashboard/projects/_zero_authorized_projects')
       end
     end
   end
diff --git a/spec/views/dashboard/projects/shared/_common.html.haml_spec.rb b/spec/views/dashboard/projects/shared/_common.html.haml_spec.rb
index 9885f4aa80215ef70dd058066c0a79267fe4ec7c..24d12621479464c9964b4c2ec9213207add478f3 100644
--- a/spec/views/dashboard/projects/shared/_common.html.haml_spec.rb
+++ b/spec/views/dashboard/projects/shared/_common.html.haml_spec.rb
@@ -40,11 +40,11 @@
         render
       end
 
-      it 'does not render #js-your-work-projects-app and renders empty state' do
+      it 'renders #js-your-work-projects-app and does not render HAML empty state' do
         render
 
-        expect(rendered).not_to have_selector('#js-your-work-projects-app')
-        expect(rendered).to render_template('dashboard/projects/_starred_empty_state')
+        expect(rendered).to have_selector('#js-your-work-projects-app')
+        expect(rendered).not_to render_template('dashboard/projects/_zero_authorized_projects')
       end
     end
   end