diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
index 0a7a22ed3a878e08f9e158d7ede0e3ab01167a4f..62de76e46b52e525c3fef7757dba724c61b2026f 100644
--- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
+++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
@@ -41,6 +41,16 @@ export default {
       required: false,
       default: false,
     },
+    inputFieldName: {
+      type: String,
+      required: false,
+      default: 'upload_file',
+    },
+    shouldUpdateInputOnFileDrop: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
   },
   data() {
     return {
@@ -84,6 +94,30 @@ export default {
         return;
       }
 
+      // NOTE: This is a temporary solution to integrate dropzone into a Rails
+      // form. On file drop if `shouldUpdateInputOnFileDrop` is true, the file
+      // input value is updated. So that when the form is submitted — the file
+      // value would be send together with the form data. This solution should
+      // be removed when License file upload page is fully migrated:
+      // https://gitlab.com/gitlab-org/gitlab/-/issues/352501
+      // NOTE: as per https://caniuse.com/mdn-api_htmlinputelement_files, IE11
+      // is not able to set input.files property, thought the user would still
+      // be able to use the file picker dialogue option, by clicking the
+      // "openFileUpload" button
+      if (this.shouldUpdateInputOnFileDrop) {
+        // Since FileList cannot be easily manipulated, to match requirement of
+        // singleFileSelection, we're throwing an error if multiple files were
+        // dropped on the dropzone
+        // NOTE: we can drop this logic together with
+        // `shouldUpdateInputOnFileDrop` flag
+        if (this.singleFileSelection && files.length > 1) {
+          this.$emit('error');
+          return;
+        }
+
+        this.$refs.fileUpload.files = files;
+      }
+
       this.$emit('change', this.singleFileSelection ? files[0] : files);
     },
     ondragenter(e) {
@@ -116,6 +150,7 @@ export default {
     <slot>
       <button
         class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+        type="button"
         @click="openFileUpload"
       >
         <div
@@ -147,7 +182,7 @@ export default {
       <input
         ref="fileUpload"
         type="file"
-        name="upload_file"
+        :name="inputFieldName"
         :accept="validFileMimetypes"
         class="hide"
         :multiple="!singleFileSelection"
diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md
index 7d2d134bf45b6cc0a4e910f007d59f59d28b727f..22133e30aa0383ef52ae4b2134c7188d0bc9c484 100644
--- a/doc/user/admin_area/license.md
+++ b/doc/user/admin_area/license.md
@@ -59,8 +59,10 @@ Otherwise, to upload your license:
 1. On the left sidebar, select **Settings**.
 1. In the **License file** area, select **Upload a license**.
 1. Upload a license:
-   - For a file, select **Upload `.gitlab-license` file**, **Choose file**, and
-     select the license file from your local machine.
+   - For a file, either:
+     - Select **Upload `.gitlab-license` file**, then **Choose File** and
+       select the license file from your local machine.
+     - Drag and drop the license file to the **Drag your license file here** area.
    - For plain text, select **Enter license key** and paste the contents in
      **License key**.
 1. Select the **Terms of Service** checkbox.
diff --git a/ee/app/assets/javascripts/admin/licenses/new/components/license_new_app.vue b/ee/app/assets/javascripts/admin/licenses/new/components/license_new_app.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d55c10a94ba5ff46ce37eee45600c6e3d546a330
--- /dev/null
+++ b/ee/app/assets/javascripts/admin/licenses/new/components/license_new_app.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import createFlash from '~/flash';
+import {
+  DROPZONE_DESCRIPTION_TEXT,
+  FILE_UPLOAD_ERROR_MESSAGE,
+  FILE_DROP_ERROR_MESSAGE,
+  DROP_TO_START_MESSAGE,
+} from '../constants';
+
+const VALID_LICENSE_FILE_MIMETYPES = ['.gitlab_license', '.gitlab-license', '.txt'];
+const FILE_EXTENSION_REGEX = /\.(gitlab[-_]license|txt)$/;
+
+const isValidLicenseFile = ({ name }) => {
+  return FILE_EXTENSION_REGEX.test(name);
+};
+
+export default {
+  name: 'LicenseNewApp',
+  components: {
+    UploadDropzone,
+    GlLink,
+    GlSprintf,
+  },
+  VALID_LICENSE_FILE_MIMETYPES,
+  isValidLicenseFile,
+  i18n: {
+    DROPZONE_DESCRIPTION_TEXT,
+    FILE_UPLOAD_ERROR_MESSAGE,
+    FILE_DROP_ERROR_MESSAGE,
+    DROP_TO_START_MESSAGE,
+  },
+  data() {
+    return { fileName: null };
+  },
+  computed: {
+    dropzoneDescription() {
+      return this.fileName ?? this.$options.i18n.DROPZONE_DESCRIPTION_TEXT;
+    },
+  },
+  methods: {
+    onChange(file) {
+      this.fileName = file?.name;
+    },
+    onError() {
+      createFlash({ message: this.$options.i18n.FILE_UPLOAD_ERROR_MESSAGE });
+    },
+  },
+};
+</script>
+<template>
+  <upload-dropzone
+    input-field-name="license[data_file]"
+    :is-file-valid="$options.isValidLicenseFile"
+    :valid-file-mimetypes="$options.VALID_LICENSE_FILE_MIMETYPES"
+    :should-update-input-on-file-drop="true"
+    :single-file-selection="true"
+    :enable-drag-behavior="false"
+    :drop-to-start-message="$options.i18n.DROP_TO_START_MESSAGE"
+    @change="onChange"
+    @error="onError"
+  >
+    <template #upload-text="{ openFileUpload }">
+      <gl-sprintf :message="dropzoneDescription">
+        <template #link="{ content }">
+          <gl-link @click.stop="openFileUpload">{{ content }}</gl-link>
+        </template>
+      </gl-sprintf>
+    </template>
+
+    <template #invalid-drag-data-slot>
+      {{ $options.i18n.FILE_DROP_ERROR_MESSAGE }}
+    </template>
+  </upload-dropzone>
+</template>
diff --git a/ee/app/assets/javascripts/admin/licenses/new/constants.js b/ee/app/assets/javascripts/admin/licenses/new/constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..64fbe6cfb5af95f0b43d49da888a7bf61ab447e2
--- /dev/null
+++ b/ee/app/assets/javascripts/admin/licenses/new/constants.js
@@ -0,0 +1,10 @@
+import { s__ } from '~/locale';
+
+export const DROPZONE_DESCRIPTION_TEXT = s__(
+  'Licenses|Drag your license file here or %{linkStart}click to upload%{linkEnd}.',
+);
+export const FILE_UPLOAD_ERROR_MESSAGE = s__('Licenses|The file could not be uploaded.');
+export const FILE_DROP_ERROR_MESSAGE = s__(
+  'Licenses|Error: You are trying to upload something other than a file',
+);
+export const DROP_TO_START_MESSAGE = s__('Licenses|Drop your license file to start the upload.');
diff --git a/ee/app/assets/javascripts/pages/admin/licenses/new/index.js b/ee/app/assets/javascripts/pages/admin/licenses/new/index.js
index b5ea6c2764f93d6b456f5939e9f660147c487d0d..b0a846b5fb2f8f21c5409ca71326aa1a19497a46 100644
--- a/ee/app/assets/javascripts/pages/admin/licenses/new/index.js
+++ b/ee/app/assets/javascripts/pages/admin/licenses/new/index.js
@@ -1,3 +1,6 @@
+import Vue from 'vue';
+import LicenseNewApp from 'ee/admin/licenses/new/components/license_new_app.vue';
+
 const licenseFile = document.querySelector('.license-file');
 const licenseKey = document.querySelector('.license-key');
 const acceptEULACheckBox = document.querySelector('#accept_eula');
@@ -15,6 +18,21 @@ const toggleUploadLicenseButton = () => {
   uploadLicenseBtn.toggleAttribute('disabled', !acceptEULACheckBox.checked);
 };
 
+const initLicenseUploadDropzone = () => {
+  const el = document.getElementById('js-license-new-app');
+
+  return new Vue({
+    el,
+    components: {
+      LicenseNewApp,
+    },
+    render(createElement) {
+      return createElement(LicenseNewApp);
+    },
+  });
+};
+
 licenseType.forEach((el) => el.addEventListener('change', showLicenseType));
 acceptEULACheckBox.addEventListener('change', toggleUploadLicenseButton);
 showLicenseType();
+initLicenseUploadDropzone();
diff --git a/ee/app/views/admin/licenses/new.html.haml b/ee/app/views/admin/licenses/new.html.haml
index a92936e54cfcb60b78642c4d9d8bc02bc2607985..ceceac31b20b77b5f7cb9e9c8a7ef26a30423b68 100644
--- a/ee/app/views/admin/licenses/new.html.haml
+++ b/ee/app/views/admin/licenses/new.html.haml
@@ -31,9 +31,10 @@
       = label_tag :license_type_file, class: 'form-check-label' do
         .option-title
           = _('Upload %{file_name} file').html_safe % { file_name: '<code>.gitlab-license</code>'.html_safe }
+
     .form-group.license-file.gl-mt-4
-      = f.label :data_file, _('License file'), class: 'gl-sr-only'
-      = f.file_field :data_file, accept: ".gitlab-license,.gitlab_license,.txt", class: "form-control"
+      #js-license-new-app
+
     .form-check.gl-my-4
       = radio_button_tag :license_type, :key, @license.data.present?, class: 'form-check-input', data: { qa_selector: 'license_type_key_radio' }
       = label_tag :license_type_key, class: 'form-check-label' do
diff --git a/ee/spec/features/admin/licenses/admin_uploads_license_spec.rb b/ee/spec/features/admin/licenses/admin_uploads_license_spec.rb
index 4e9f4803b111a95d7d18c9ed3f8bc12054be0cf2..9c7a5b9f68ca7b50257443ee45fb45d99658901c 100644
--- a/ee/spec/features/admin/licenses/admin_uploads_license_spec.rb
+++ b/ee/spec/features/admin/licenses/admin_uploads_license_spec.rb
@@ -113,7 +113,7 @@
   private
 
   def attach_and_upload(path)
-    attach_file("license_data_file", path)
+    attach_file("license[data_file]", path, make_visible: true)
     check("accept_eula")
     click_button("Upload License")
   end
diff --git a/ee/spec/frontend/admin/licenses/new/components/license_new_app_spec.js b/ee/spec/frontend/admin/licenses/new/components/license_new_app_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..e2a7cd9366c7dd66766631924b93b025b521f1d3
--- /dev/null
+++ b/ee/spec/frontend/admin/licenses/new/components/license_new_app_spec.js
@@ -0,0 +1,84 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import LicenseNewApp from 'ee/admin/licenses/new/components/license_new_app.vue';
+import { FILE_UPLOAD_ERROR_MESSAGE } from 'ee/admin/licenses/new/constants';
+import createFlash from '~/flash';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+
+jest.mock('~/flash');
+
+describe('Upload dropzone component', () => {
+  let wrapper;
+
+  const findUploadDropzone = () => wrapper.find(UploadDropzone);
+
+  function createComponent() {
+    wrapper = shallowMount(LicenseNewApp, {
+      stubs: {
+        GlSprintf,
+      },
+    });
+  }
+
+  beforeEach(() => {
+    createFlash.mockClear();
+    createComponent();
+  });
+
+  afterEach(() => {
+    wrapper.destroy();
+    wrapper = null;
+  });
+
+  it('displays an error when upload-dropzone emits an error', async () => {
+    findUploadDropzone().vm.$emit('error');
+
+    await nextTick();
+    expect(createFlash).toHaveBeenCalledWith({ message: FILE_UPLOAD_ERROR_MESSAGE });
+  });
+
+  it('displays filename when the file is set in upload-dropzone', async () => {
+    const uploadDropzone = findUploadDropzone();
+    uploadDropzone.vm.$emit('change', { name: 'test-license.txt' });
+
+    await nextTick();
+    expect(wrapper.text()).toEqual(expect.stringContaining('test-license.txt'));
+  });
+
+  it('properly resets filename when the file was unset by the upload-dropzone', async () => {
+    const uploadDropzone = findUploadDropzone();
+    uploadDropzone.vm.$emit('change', { name: 'test-license.txt' });
+    await nextTick();
+
+    uploadDropzone.vm.$emit('change', null);
+    await nextTick();
+
+    expect(wrapper.text()).not.toEqual(expect.stringContaining('test-license.txt'));
+  });
+
+  describe('allows only license file types for the dropzone', () => {
+    const properLicenseFileExtensions = ['.gitlab_license', '.gitlab-license', '.txt'];
+    let isFileValid;
+    let validFileMimetypes;
+
+    beforeEach(() => {
+      createComponent();
+      const uploadDropzone = findUploadDropzone();
+      isFileValid = uploadDropzone.props('isFileValid');
+      validFileMimetypes = uploadDropzone.props('validFileMimetypes');
+    });
+
+    it('should pass proper extension list for file picker dialogue', () => {
+      expect(validFileMimetypes).toEqual(properLicenseFileExtensions);
+    });
+
+    it.each(properLicenseFileExtensions)('allows %s file extension', (extension) => {
+      expect(isFileValid({ name: `license${extension}` })).toBe(true);
+    });
+
+    it.each(['.pdf', '.jpg', '.html'])('rejects %s file extension', (extension) => {
+      expect(isFileValid({ name: `license${extension}` })).toBe(false);
+    });
+  });
+});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ef2825c9a26ac13edeaed3278b59052a681d1990..b95143580dcf6a938c80896552525b4193951b87 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -21660,9 +21660,18 @@ msgstr ""
 msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest successful%{linkEnd} scan"
 msgstr ""
 
+msgid "Licenses|Drag your license file here or %{linkStart}click to upload%{linkEnd}."
+msgstr ""
+
+msgid "Licenses|Drop your license file to start the upload."
+msgstr ""
+
 msgid "Licenses|Error fetching the license list. Please check your network connection and try again."
 msgstr ""
 
+msgid "Licenses|Error: You are trying to upload something other than a file"
+msgstr ""
+
 msgid "Licenses|License Compliance"
 msgstr ""
 
@@ -21681,6 +21690,9 @@ msgstr ""
 msgid "Licenses|Specified policies in this project"
 msgstr ""
 
+msgid "Licenses|The file could not be uploaded."
+msgstr ""
+
 msgid "Licenses|The license list details information about the licenses used within your project."
 msgstr ""
 
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
index 0f1e118d44c7c70ce7e5ddd3ca15ea91c13bef31..a613b325462494b771977b638ac14b064f5fd4ef 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
+++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
@@ -6,6 +6,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
 >
   <button
     class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+    type="button"
   >
     <div
       class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -86,6 +87,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
 >
   <button
     class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+    type="button"
   >
     <div
       class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -170,6 +172,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
 >
   <button
     class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+    type="button"
   >
     <div
       class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -254,6 +257,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
 >
   <button
     class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+    type="button"
   >
     <div
       class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -339,6 +343,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
 >
   <button
     class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+    type="button"
   >
     <div
       class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -424,6 +429,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
 >
   <button
     class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+    type="button"
   >
     <div
       class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
@@ -509,6 +515,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
 >
   <button
     class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+    type="button"
   >
     <div
       class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
index 2f5afeec1fc1f4f9f7e7d9952eb72fa3067884e0..21e9b401215cd36c4467e07f93895dd73721c744 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
+++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
@@ -16,6 +16,7 @@ describe('Upload dropzone component', () => {
   const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
   const findIcon = () => wrapper.find(GlIcon);
   const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text();
+  const findFileInput = () => wrapper.find('input[type="file"]');
 
   function createComponent({ slots = {}, data = {}, props = {} } = {}) {
     wrapper = shallowMount(UploadDropzone, {
@@ -197,4 +198,60 @@ describe('Upload dropzone component', () => {
 
     expect(wrapper.element).toMatchSnapshot();
   });
+
+  describe('file input form name', () => {
+    it('applies inputFieldName as file input name', () => {
+      createComponent({ props: { inputFieldName: 'test_field_name' } });
+      expect(findFileInput().attributes('name')).toBe('test_field_name');
+    });
+
+    it('uses default file input name if no inputFieldName provided', () => {
+      createComponent();
+      expect(findFileInput().attributes('name')).toBe('upload_file');
+    });
+  });
+
+  describe('updates file input files value', () => {
+    // NOTE: the component assigns dropped files from the drop event to the
+    // input.files property. There's a restriction that nothing but a FileList
+    // can be assigned to this property. While FileList can't be created
+    // manually: it has no constructor. And currently there's no good workaround
+    // for jsdom. So we have to stub the file input in vm.$refs to ensure that
+    // the files property is updated. This enforces following tests to know a
+    // bit too much about the SUT internals See this thread for more details on
+    // FileList in jsdom: https://github.com/jsdom/jsdom/issues/1272
+    function stubFileInputOnWrapper() {
+      const fakeFileInput = { files: [] };
+      wrapper.vm.$refs.fileUpload = fakeFileInput;
+    }
+
+    it('assigns dragged files to the input files property', async () => {
+      const mockFile = { name: 'test', type: 'image/jpg' };
+      const mockEvent = mockDragEvent({ files: [mockFile] });
+      createComponent({ props: { shouldUpdateInputOnFileDrop: true } });
+      stubFileInputOnWrapper();
+
+      wrapper.trigger('dragenter', mockEvent);
+      await nextTick();
+      wrapper.trigger('drop', mockEvent);
+      await nextTick();
+
+      expect(wrapper.vm.$refs.fileUpload.files).toEqual([mockFile]);
+    });
+
+    it('throws an error when multiple files are dropped on a single file input dropzone', async () => {
+      const mockFile = { name: 'test', type: 'image/jpg' };
+      const mockEvent = mockDragEvent({ files: [mockFile, mockFile] });
+      createComponent({ props: { shouldUpdateInputOnFileDrop: true, singleFileSelection: true } });
+      stubFileInputOnWrapper();
+
+      wrapper.trigger('dragenter', mockEvent);
+      await nextTick();
+      wrapper.trigger('drop', mockEvent);
+      await nextTick();
+
+      expect(wrapper.vm.$refs.fileUpload.files).toEqual([]);
+      expect(wrapper.emitted('error')).toHaveLength(1);
+    });
+  });
 });