diff --git a/app/assets/javascripts/packages/details/components/maven_installation.vue b/app/assets/javascripts/packages/details/components/maven_installation.vue
index c2f6f76967b597e17fc7b1dc0963ebe3b090dd55..35d3e293304e77c9d6eaac0165612e22761187da 100644
--- a/app/assets/javascripts/packages/details/components/maven_installation.vue
+++ b/app/assets/javascripts/packages/details/components/maven_installation.vue
@@ -12,9 +12,17 @@ export default {
     GlLink,
     GlSprintf,
   },
+  data() {
+    return {
+      instructionType: 'maven',
+    };
+  },
   computed: {
     ...mapState(['mavenHelpPath']),
     ...mapGetters(['mavenInstallationXml', 'mavenInstallationCommand', 'mavenSetupXml']),
+    showMaven() {
+      return this.instructionType === 'maven';
+    },
   },
   i18n: {
     xmlText: s__(
@@ -36,50 +44,51 @@ export default {
   <div>
     <h3 class="gl-font-lg">{{ __('Installation') }}</h3>
 
-    <p>
-      <gl-sprintf :message="$options.i18n.xmlText">
-        <template #code="{ content }">
-          <code>{{ content }}</code>
-        </template>
-      </gl-sprintf>
-    </p>
+    <template v-if="showMaven">
+      <p>
+        <gl-sprintf :message="$options.i18n.xmlText">
+          <template #code="{ content }">
+            <code>{{ content }}</code>
+          </template>
+        </gl-sprintf>
+      </p>
 
-    <code-instruction
-      :label="s__('PackageRegistry|Maven XML')"
-      :instruction="mavenInstallationXml"
-      :copy-text="s__('PackageRegistry|Copy Maven XML')"
-      multiline
-      :tracking-action="$options.trackingActions.COPY_MAVEN_XML"
-      :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
-    />
+      <code-instruction
+        :instruction="mavenInstallationXml"
+        :copy-text="s__('PackageRegistry|Copy Maven XML')"
+        :tracking-action="$options.trackingActions.COPY_MAVEN_XML"
+        :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+        multiline
+      />
 
-    <code-instruction
-      :label="s__('PackageRegistry|Maven Command')"
-      :instruction="mavenInstallationCommand"
-      :copy-text="s__('PackageRegistry|Copy Maven command')"
-      :tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND"
-      :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
-    />
+      <code-instruction
+        :label="s__('PackageRegistry|Maven Command')"
+        :instruction="mavenInstallationCommand"
+        :copy-text="s__('PackageRegistry|Copy Maven command')"
+        :tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND"
+        :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+      />
 
-    <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
-    <p>
-      <gl-sprintf :message="$options.i18n.setupText">
-        <template #code="{ content }">
-          <code>{{ content }}</code>
+      <h3 class="gl-font-lg">{{ s__('PackageRegistry|Registry setup') }}</h3>
+      <p>
+        <gl-sprintf :message="$options.i18n.setupText">
+          <template #code="{ content }">
+            <code>{{ content }}</code>
+          </template>
+        </gl-sprintf>
+      </p>
+      <code-instruction
+        :instruction="mavenSetupXml"
+        :copy-text="s__('PackageRegistry|Copy Maven registry XML')"
+        :tracking-action="$options.trackingActions.COPY_MAVEN_SETUP"
+        :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+        multiline
+      />
+      <gl-sprintf :message="$options.i18n.helpText">
+        <template #link="{ content }">
+          <gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link>
         </template>
       </gl-sprintf>
-    </p>
-    <code-instruction
-      :instruction="mavenSetupXml"
-      :copy-text="s__('PackageRegistry|Copy Maven registry XML')"
-      multiline
-      :tracking-action="$options.trackingActions.COPY_MAVEN_SETUP"
-      :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
-    />
-    <gl-sprintf :message="$options.i18n.helpText">
-      <template #link="{ content }">
-        <gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link>
-      </template>
-    </gl-sprintf>
+    </template>
   </div>
 </template>
diff --git a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
index bc7f8a2b17a5baa4638d2bde0d2e255ecd696642..1a85a641dd194f130dea8e6446f3fbe18fc3651b 100644
--- a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
@@ -56,27 +56,29 @@ export default {
 </script>
 
 <template>
-  <div v-if="!multiline" class="gl-mb-3">
+  <div>
     <label v-if="label" :for="generateFormId('instruction-input')">{{ label }}</label>
-    <div class="input-group gl-mb-3">
-      <input
-        :id="generateFormId('instruction-input')"
-        :value="instruction"
-        type="text"
-        class="form-control gl-font-monospace"
-        data-testid="instruction-input"
-        readonly
-        @copy="trackCopy"
-      />
-      <span class="input-group-append" data-testid="instruction-button" @click="trackCopy">
-        <clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
-      </span>
+    <div v-if="!multiline" class="gl-mb-3">
+      <div class="input-group gl-mb-3">
+        <input
+          :id="generateFormId('instruction-input')"
+          :value="instruction"
+          type="text"
+          class="form-control gl-font-monospace"
+          data-testid="instruction-input"
+          readonly
+          @copy="trackCopy"
+        />
+        <span class="input-group-append" data-testid="instruction-button" @click="trackCopy">
+          <clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
+        </span>
+      </div>
     </div>
-  </div>
 
-  <div v-else>
-    <pre class="gl-font-monospace" data-testid="multiline-instruction" @copy="trackCopy">{{
-      instruction
-    }}</pre>
+    <div v-else>
+      <pre class="gl-font-monospace" data-testid="multiline-instruction" @copy="trackCopy">{{
+        instruction
+      }}</pre>
+    </div>
   </div>
 </template>
diff --git a/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue b/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue
new file mode 100644
index 0000000000000000000000000000000000000000..36b1a9c49f4c09bd5f3598aaa6d43ff08e65b35d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+
+export default {
+  name: 'PersistedDropdownSelection',
+  components: {
+    GlDropdown,
+    GlDropdownItem,
+    LocalStorageSync,
+  },
+  props: {
+    options: {
+      type: Array,
+      required: true,
+    },
+    storageKey: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      selected: null,
+    };
+  },
+  computed: {
+    dropdownText() {
+      const selected = this.parsedOptions.find((o) => o.selected);
+      return selected?.label || this.options[0].label;
+    },
+    parsedOptions() {
+      return this.options.map((o) => ({ ...o, selected: o.value === this.selected }));
+    },
+  },
+  methods: {
+    setSelected(value) {
+      this.selected = value;
+      this.$emit('change', value);
+    },
+  },
+};
+</script>
+
+<template>
+  <local-storage-sync :storage-key="storageKey" :value="selected" @input="setSelected">
+    <gl-dropdown :text="dropdownText" lazy>
+      <gl-dropdown-item
+        v-for="option in parsedOptions"
+        :key="option.value"
+        :is-checked="option.selected"
+        :is-check-item="true"
+        @click="setSelected(option.value)"
+      >
+        {{ option.label }}
+      </gl-dropdown-item>
+    </gl-dropdown>
+  </local-storage-sync>
+</template>
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9d9452a3a9d8d74715a5a2f0c4fa9452f1962466..7d4e91afdcd1eee23936f0593c88db26d131d435 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -21617,9 +21617,6 @@ msgstr ""
 msgid "PackageRegistry|Maven Command"
 msgstr ""
 
-msgid "PackageRegistry|Maven XML"
-msgstr ""
-
 msgid "PackageRegistry|NuGet"
 msgstr ""
 
@@ -21656,6 +21653,9 @@ msgstr ""
 msgid "PackageRegistry|Recipe: %{recipe}"
 msgstr ""
 
+msgid "PackageRegistry|Registry setup"
+msgstr ""
+
 msgid "PackageRegistry|Remove package"
 msgstr ""
 
diff --git a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
index 6d22b372d41c394f4561a3fe43e83952fd333ee1..b615a8035a339bb64ba2a9af9be5d6b54552bef4 100644
--- a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
+++ b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
@@ -17,7 +17,7 @@ exports[`MavenInstallation renders all the messages 1`] = `
   <code-instruction-stub
     copytext="Copy Maven XML"
     instruction="foo/xml"
-    label="Maven XML"
+    label=""
     multiline="true"
     trackingaction="copy_maven_xml"
     trackinglabel="code_instruction"
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
index da49778f216079e4aab1cd16239fc3bb603175d3..30b7f0c2d28b489510e44098875008b955a70c87 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -2,20 +2,26 @@
 
 exports[`Package code instruction multiline to match the snapshot 1`] = `
 <div>
-  <pre
-    class="gl-font-monospace"
-    data-testid="multiline-instruction"
+  <label
+    for="instruction-input_3"
   >
-    this is some
+    foo_label
+  </label>
+   
+  <div>
+    <pre
+      class="gl-font-monospace"
+      data-testid="multiline-instruction"
+    >
+      this is some
 multiline text
-  </pre>
+    </pre>
+  </div>
 </div>
 `;
 
 exports[`Package code instruction single line to match the default snapshot 1`] = `
-<div
-  class="gl-mb-3"
->
+<div>
   <label
     for="instruction-input_2"
   >
@@ -23,42 +29,46 @@ exports[`Package code instruction single line to match the default snapshot 1`]
   </label>
    
   <div
-    class="input-group gl-mb-3"
+    class="gl-mb-3"
   >
-    <input
-      class="form-control gl-font-monospace"
-      data-testid="instruction-input"
-      id="instruction-input_2"
-      readonly="readonly"
-      type="text"
-    />
-     
-    <span
-      class="input-group-append"
-      data-testid="instruction-button"
+    <div
+      class="input-group gl-mb-3"
     >
-      <button
-        aria-label="Copy this value"
-        class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
-        data-clipboard-text="npm i @my-package"
-        title="Copy npm install command"
-        type="button"
+      <input
+        class="form-control gl-font-monospace"
+        data-testid="instruction-input"
+        id="instruction-input_2"
+        readonly="readonly"
+        type="text"
+      />
+       
+      <span
+        class="input-group-append"
+        data-testid="instruction-button"
       >
-        <!---->
-         
-        <svg
-          aria-hidden="true"
-          class="gl-button-icon gl-icon s16"
-          data-testid="copy-to-clipboard-icon"
+        <button
+          aria-label="Copy this value"
+          class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
+          data-clipboard-text="npm i @my-package"
+          title="Copy npm install command"
+          type="button"
         >
-          <use
-            href="#copy-to-clipboard"
-          />
-        </svg>
-          
-        <!---->
-      </button>
-    </span>
+          <!---->
+           
+          <svg
+            aria-hidden="true"
+            class="gl-button-icon gl-icon s16"
+            data-testid="copy-to-clipboard-icon"
+          >
+            <use
+              href="#copy-to-clipboard"
+            />
+          </svg>
+            
+          <!---->
+        </button>
+      </span>
+    </div>
   </div>
 </div>
 `;
diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c65ded000d32cafafa2e794c53f55506124b2988
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
@@ -0,0 +1,122 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import component from '~/vue_shared/components/registry/persisted_dropdown_selection.vue';
+
+describe('Persisted dropdown selection', () => {
+  let wrapper;
+
+  const defaultProps = {
+    storageKey: 'foo_bar',
+    options: [
+      { value: 'maven', label: 'Maven' },
+      { value: 'gradle', label: 'Gradle' },
+    ],
+  };
+
+  function createComponent({ props = {}, data = {} } = {}) {
+    wrapper = shallowMount(component, {
+      propsData: {
+        ...defaultProps,
+        ...props,
+      },
+      data() {
+        return data;
+      },
+    });
+  }
+
+  const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+  const findDropdown = () => wrapper.findComponent(GlDropdown);
+  const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  describe('local storage sync', () => {
+    it('uses the local storage sync component', () => {
+      createComponent();
+
+      expect(findLocalStorageSync().exists()).toBe(true);
+    });
+
+    it('passes the right props', () => {
+      createComponent({ data: { selected: 'foo' } });
+
+      expect(findLocalStorageSync().props()).toMatchObject({
+        storageKey: defaultProps.storageKey,
+        value: 'foo',
+      });
+    });
+
+    it('on input event updates the model and emits event', async () => {
+      const inputPayload = 'bar';
+      createComponent();
+      findLocalStorageSync().vm.$emit('input', inputPayload);
+
+      await nextTick();
+
+      expect(wrapper.emitted('change')).toStrictEqual([[inputPayload]]);
+      expect(findLocalStorageSync().props('value')).toBe(inputPayload);
+    });
+  });
+
+  describe('dropdown', () => {
+    it('has a dropdown component', () => {
+      createComponent();
+
+      expect(findDropdown().exists()).toBe(true);
+    });
+
+    describe('dropdown text', () => {
+      it('when no selection shows the first', () => {
+        createComponent();
+
+        expect(findDropdown().props('text')).toBe('Maven');
+      });
+
+      it('when an option is selected, shows that option label', () => {
+        createComponent({ data: { selected: defaultProps.options[1].value } });
+
+        expect(findDropdown().props('text')).toBe('Gradle');
+      });
+    });
+
+    describe('dropdown items', () => {
+      it('has one item for each option', () => {
+        createComponent();
+
+        expect(findDropdownItems()).toHaveLength(defaultProps.options.length);
+      });
+
+      it('binds the correct props', () => {
+        createComponent({ data: { selected: defaultProps.options[0].value } });
+
+        expect(findDropdownItems().at(0).props()).toMatchObject({
+          isChecked: true,
+          isCheckItem: true,
+        });
+
+        expect(findDropdownItems().at(1).props()).toMatchObject({
+          isChecked: false,
+          isCheckItem: true,
+        });
+      });
+
+      it('on click updates the data and emits event', async () => {
+        createComponent({ data: { selected: defaultProps.options[0].value } });
+        expect(findDropdownItems().at(0).props('isChecked')).toBe(true);
+
+        findDropdownItems().at(1).vm.$emit('click');
+
+        await nextTick();
+
+        expect(wrapper.emitted('change')).toStrictEqual([['gradle']]);
+        expect(findDropdownItems().at(0).props('isChecked')).toBe(false);
+        expect(findDropdownItems().at(1).props('isChecked')).toBe(true);
+      });
+    });
+  });
+});