Drag-and-drop for uploading files

We will upload files by simply dragging and dropping without clicks.

Time to read: 14 min

Task

Uploading files by the user to the server is a common task when creating websites and applications. Current capabilities of JavaScript allow us to select the desired file by simply dragging it into a set area of the browser page.

Modern desktop browsers provide wide support for drag events, while such support is currently low among mobile browsers. Therefore, if it is necessary to implement file transfer to the server also for users of mobile devices, it is worth adding the ability to select a file using the <input type="file">.

The article will consider the option of selecting a file using dragging.

Uploading a file to the server consists of three parts:

  1. The user selects a file on their device.
  2. Checking the parameters processing the file and forming data to contact the server.
  3. Processing the data on the server and sending a response to the client.

Organizing the complete process of uploading a file is only possible with the use of a server-side implementation, which is beyond the scope of this article. Therefore, we will consider organizing the sending of the file on the client side: HTML markup, styling elements, and JavaScript code to send the file to the server.

The server-side part for file exchange can be implemented in different programming languages. For example, you can learn more about processing files on the server side using PHP in the PHP documentation.

Solution for file upload

On the page, we will place HTML markup with the necessary elements:

        
          
          <div id="uploadFile_Loader" class="upload-zone">  <form class="form-upload" id="uploadForm" method="post" enctype="multipart/form-data">    <div class="upload-zone_dragover">      <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" viewBox="0 0 24 24" class="upload-loader__image">        <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9"/>        <path d="m16 16-4-4-4 4"/>      </svg>      <p>Drag the file here</p>      <span class="form-upload__hint" id="hint">You can only upload images</span>    </div>    <label class="form-upload__label" for="uploadForm_file">      <span class="form-upload__title">Or click the button</span>      <input class="form-upload__input" id="uploadForm_File" type="file" name="file_name" accept="image/*" aria-describedby="hint">    </label>    <div class="form-upload__container">      <span class="form-upload__hint" id="uploadForm_Hint"></span>    </div>  </form></div>
          <div id="uploadFile_Loader" class="upload-zone">
  <form class="form-upload" id="uploadForm" method="post" enctype="multipart/form-data">
    <div class="upload-zone_dragover">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" viewBox="0 0 24 24" class="upload-loader__image">
        <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9"/>
        <path d="m16 16-4-4-4 4"/>
      </svg>
      <p>Drag the file here</p>
      <span class="form-upload__hint" id="hint">You can only upload images</span>
    </div>
    <label class="form-upload__label" for="uploadForm_file">
      <span class="form-upload__title">Or click the button</span>
      <input class="form-upload__input" id="uploadForm_File" type="file" name="file_name" accept="image/*" aria-describedby="hint">
    </label>
    <div class="form-upload__container">
      <span class="form-upload__hint" id="uploadForm_Hint"></span>
    </div>
  </form>
</div>

        
        
          
        
      

To style the elements, we will create the following CSS rules:

        
          
          .form-upload {  display: grid;  align-items: center;  width: 80vw;  min-width: 360px;}.upload-zone_dragover {  display: grid;  height: 50vh;  min-height: 360px;  margin-bottom: 25px;  border: 1px solid currentColor;  color: #FFFFFF;  font-weight: 500;  font-size: 18px;  place-content: center;  text-align: center;}.upload-zone_dragover svg {  width: 10vw;  margin: auto;  pointer-events: none;}.form-upload__hint {  margin-top: 10px;  font-size: 14px;  font-weight: 400;}.upload-zone_dragover._active {  color: #c56fff;  background-color: #c56fff77;}.form-upload__label {  display: flex;  justify-content: space-between;  align-items: center;}.form-upload__title {  margin-right: 55px;  font-size: 18px;  font-weight: 500;}.form-upload__input {  font-family: inherit;  font-size: 18px;}.form-upload__input::file-selector-button {  margin-right: 30px;  border: none;  border-radius: 6px;  padding: 9px 15px;  font-family: inherit;  font-weight: inherit;  transition: background-color 0.2s linear;  cursor: pointer;}.form-upload__input::file-selector-button:hover {  background-color: #c56fff;}.form-upload__container {  width: 360px;  margin-top: 10px;  font-size: 16px;}.upload-zone_gragover {  background-color: #593273;}.upload-hint,.upload-status {  width: 75%;}.upload-hint {  display: none;}.upload-hint_visible {  display: block;  pointer-events: none;}.upload-loader {  display: none;  position: absolute;  top: 0;  left: 0;  width: 100%;  height: 100%;}.upload-loader_visible {  display: flex;  justify-content: center;  align-items: center;  background-color: #593273;}.upload-loader__image {  width: 150px;  height: 150px;}@media (max-width: 768px) {  .upload-zone {    padding: 55px 30px;  }  .form-upload__title {    display: block;    margin-right: 0;  }  .form-upload__input::file-selector-button {    min-width: initial;    margin-right: 10px;  }}
          .form-upload {
  display: grid;
  align-items: center;
  width: 80vw;
  min-width: 360px;
}

.upload-zone_dragover {
  display: grid;
  height: 50vh;
  min-height: 360px;
  margin-bottom: 25px;
  border: 1px solid currentColor;
  color: #FFFFFF;
  font-weight: 500;
  font-size: 18px;
  place-content: center;
  text-align: center;
}

.upload-zone_dragover svg {
  width: 10vw;
  margin: auto;
  pointer-events: none;
}

.form-upload__hint {
  margin-top: 10px;
  font-size: 14px;
  font-weight: 400;
}

.upload-zone_dragover._active {
  color: #c56fff;
  background-color: #c56fff77;
}

.form-upload__label {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.form-upload__title {
  margin-right: 55px;
  font-size: 18px;
  font-weight: 500;
}

.form-upload__input {
  font-family: inherit;
  font-size: 18px;
}

.form-upload__input::file-selector-button {
  margin-right: 30px;
  border: none;
  border-radius: 6px;
  padding: 9px 15px;
  font-family: inherit;
  font-weight: inherit;
  transition: background-color 0.2s linear;
  cursor: pointer;
}

.form-upload__input::file-selector-button:hover {
  background-color: #c56fff;
}

.form-upload__container {
  width: 360px;
  margin-top: 10px;
  font-size: 16px;
}

.upload-zone_gragover {
  background-color: #593273;
}

.upload-hint,
.upload-status {
  width: 75%;
}

.upload-hint {
  display: none;
}

.upload-hint_visible {
  display: block;
  pointer-events: none;
}

.upload-loader {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.upload-loader_visible {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #593273;
}

.upload-loader__image {
  width: 150px;
  height: 150px;
}

@media (max-width: 768px) {
  .upload-zone {
    padding: 55px 30px;
  }

  .form-upload__title {
    display: block;
    margin-right: 0;
  }

  .form-upload__input::file-selector-button {
    min-width: initial;
    margin-right: 10px;
  }
}

        
        
          
        
      

At the end of the HTML page or in a separate JavaScript file, we will add code that will ensure communication between the user and the server:

        
          
          const dropFileZone = document.querySelector(".upload-zone_dragover")const statusText = document.getElementById("uploadForm_Status")const sizeText = document.getElementById("uploadForm_Size")const uploadInput = document.querySelector(".form-upload__input")let setStatus = (text) => {  statusText.textContent = text}const uploadUrl = "/unicorns";["dragover", "drop"].forEach(function(event) {  document.addEventListener(event, function(evt) {    evt.preventDefault()    return false  })})dropFileZone.addEventListener("dragenter", function() {  dropFileZone.classList.add("_active")})dropFileZone.addEventListener("dragleave", function() {  dropFileZone.classList.remove("_active")})dropFileZone.addEventListener("drop", function() {  dropFileZone.classList.remove("_active")  const file = event.dataTransfer?.files[0]  if (!file) {    return  }  if (file.type.startsWith("image/")) {    uploadInput.files = event.dataTransfer.files    processingUploadFile()  } else {    setStatus("You can only upload images")    return false  }})uploadInput.addEventListener("change", (event) => {  const file = uploadInput.files?.[0]  if (file && file.type.startsWith("image/")) {    processingUploadFile()  } else {    setStatus("You can only upload images")    return false  }})function processingUploadFile(file) {  if (file) {    const dropZoneData = new FormData()    const xhr = new XMLHttpRequest()    dropZoneData.append("file", file)    xhr.open("POST", uploadUrl, true)    xhr.send(dropZoneData)    xhr.onload = function () {      if (xhr.status == 200) {        setStatus("All uploaded")      } else {        setStatus("Upload error")      }      HTMLElement.style.display = "none"    }  }}function processingDownloadFileWithFetch() {  fetch(url, {    method: "POST",  }).then(async (res) => {    const reader = res?.body?.getReader();    while (true && reader) {      const { value, done } = await reader?.read()      console.log("value", value)      if (done) break      console.log("Received", value)    }  })}
          const dropFileZone = document.querySelector(".upload-zone_dragover")
const statusText = document.getElementById("uploadForm_Status")
const sizeText = document.getElementById("uploadForm_Size")
const uploadInput = document.querySelector(".form-upload__input")

let setStatus = (text) => {
  statusText.textContent = text
}

const uploadUrl = "/unicorns";

["dragover", "drop"].forEach(function(event) {
  document.addEventListener(event, function(evt) {
    evt.preventDefault()
    return false
  })
})

dropFileZone.addEventListener("dragenter", function() {
  dropFileZone.classList.add("_active")
})

dropFileZone.addEventListener("dragleave", function() {
  dropFileZone.classList.remove("_active")
})

dropFileZone.addEventListener("drop", function() {
  dropFileZone.classList.remove("_active")
  const file = event.dataTransfer?.files[0]
  if (!file) {
    return
  }

  if (file.type.startsWith("image/")) {
    uploadInput.files = event.dataTransfer.files
    processingUploadFile()
  } else {
    setStatus("You can only upload images")
    return false
  }
})

uploadInput.addEventListener("change", (event) => {
  const file = uploadInput.files?.[0]
  if (file && file.type.startsWith("image/")) {
    processingUploadFile()
  } else {
    setStatus("You can only upload images")
    return false
  }
})

function processingUploadFile(file) {
  if (file) {
    const dropZoneData = new FormData()
    const xhr = new XMLHttpRequest()

    dropZoneData.append("file", file)

    xhr.open("POST", uploadUrl, true)

    xhr.send(dropZoneData)

    xhr.onload = function () {
      if (xhr.status == 200) {
        setStatus("All uploaded")
      } else {
        setStatus("Upload error")
      }
      HTMLElement.style.display = "none"
    }
  }
}

function processingDownloadFileWithFetch() {
  fetch(url, {
    method: "POST",
  }).then(async (res) => {
    const reader = res?.body?.getReader();
    while (true && reader) {
      const { value, done } = await reader?.read()
      console.log("value", value)
      if (done) break
      console.log("Received", value)
    }
  })
}

        
        
          
        
      
Open demo in the new window

The complete version of uploading a file with its saving on the server looks like this:

Breakdown of the solution

Markup

A container with the identifier upload-zone is used to process the file. Inside this block, a form <form> with elements that ensure informational interaction with the user is placed. For example, by changing the background color of this area when dragging an element.

For each element involved in the file processing, an id attribute is specified — this will allow the JavaScript code to refer to the necessary elements to perform the required actions.

        
          
          <div id="uploadFile_Loader" class="upload-zone">  <form class="form-upload" id="uploadForm" method="post" enctype="multipart/form-data">    <div class="upload-zone_dragover">      <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" viewBox="0 0 24 24" class="upload-loader__image">        <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9"/>        <path d="m16 16-4-4-4 4"/>      </svg>      <p>Drag the file here</p>      <span class="form-upload__hint" id="hint">You can only upload images</span>    </div>    <label class="form-upload__label" for="uploadForm_file">      <span class="form-upload__title">Or click the button</span>      <input class="form-upload__input" id="uploadForm_File" type="file" name="file_name" accept="image/*" aria-describedby="hint">    </label>    <div class="form-upload__container">      <span class="form-upload__hint" id="uploadForm_Hint"></span>    </div>  </form></div>
          <div id="uploadFile_Loader" class="upload-zone">
  <form class="form-upload" id="uploadForm" method="post" enctype="multipart/form-data">
    <div class="upload-zone_dragover">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" viewBox="0 0 24 24" class="upload-loader__image">
        <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9"/>
        <path d="m16 16-4-4-4 4"/>
      </svg>
      <p>Drag the file here</p>
      <span class="form-upload__hint" id="hint">You can only upload images</span>
    </div>
    <label class="form-upload__label" for="uploadForm_file">
      <span class="form-upload__title">Or click the button</span>
      <input class="form-upload__input" id="uploadForm_File" type="file" name="file_name" accept="image/*" aria-describedby="hint">
    </label>
    <div class="form-upload__container">
      <span class="form-upload__hint" id="uploadForm_Hint"></span>
    </div>
  </form>
</div>

        
        
          
        
      

In addition to the area for dragging the file, we use a special field for uploading it <input type="file" accept="image/*">. This will add an alternative way to upload for those users who do not use a mouse, and at the same time fulfill one of the criteria of WCAG.

The field will be linked with a hint that you can only upload images using the aria-describedby attribute. This attribute programmatically links the hint with the field and is useful for screen reader users.

To display the upload of the file, you can also use a special element <progress> — this option is discussed in detail in the recipe "Uploading a file with a progress bar"(/recipes/progress/). This tag already has the progressbar role built in, thanks to which screen readers automatically announce the upload progress.

Styles

We style the area for uploading files. We set a minimum height, border, and center-align the elements.

        
          
          .upload-zone_dragover {  display: grid;  height: 50vh;  min-height: 360px;  margin-bottom: 25px;  border: 1px solid currentColor;  color: #FFFFFF;  font-weight: 500;  font-size: 18px;  place-content: center;  text-align: center;}
          .upload-zone_dragover {
  display: grid;
  height: 50vh;
  min-height: 360px;
  margin-bottom: 25px;
  border: 1px solid currentColor;
  color: #FFFFFF;
  font-weight: 500;
  font-size: 18px;
  place-content: center;
  text-align: center;
}

        
        
          
        
      

When dragging a file into the upload area, we will change the background color using an additional class:

        
          
          .upload-zone_gragover {  background-color: #593273;}
          .upload-zone_gragover {
  background-color: #593273;
}

        
        
          
        
      

JavaScript

First, we will declare variables and retrieve all the necessary elements of the DOM tree to subscribe to events:

  • dropFileZone sets the area for processing the selected file;
  • statusText points to the hint about uploading the file;
  • setStatus is needed to store status text;
  • uploadInput sets the area for the button to upload a file without dragging.
        
          
          const dropFileZone = document.querySelector(".upload-zone_dragover")const statusText = document.getElementById("uploadForm_Status")const uploadInput = document.querySelector(".form-upload__input")let setStatus = (text) => {  statusText.textContent = text}const uploadUrl = "/unicorns"
          const dropFileZone = document.querySelector(".upload-zone_dragover")
const statusText = document.getElementById("uploadForm_Status")
const uploadInput = document.querySelector(".form-upload__input")

let setStatus = (text) => {
  statusText.textContent = text
}

const uploadUrl = "/unicorns"

        
        
          
        
      

Since the variable setStatus is declared without assigning a value, the let keyword is used. More details about the differences between variables and principles of working with them are discussed in the article “Variables const, let and var”.

When tracking the dragging of a file, the following events will be used:

  • dragover is executed during the movement of the file over the file processing area;
  • dragenter triggers when the file enters the processing area;
  • dragleave triggers if the file leaves the processing area but has not yet been "dropped";
  • drop is executed when the user releases the mouse button and the selected file has been placed ("dropped") in the designated area.

When the selected file is within the active page, the browser will open it. To ensure the file is processed in the designated area, it is necessary to cancel the default behavior of the browser for the dragover and drop events by calling the preventDefault() method:

        
          
          ["dragover", "drop"].forEach(function(event) {  document.addEventListener(event, function(evt) {    evt.preventDefault()    return false  })})dropFileZone.addEventListener("dragenter", function() {  dropFileZone.classList.add("_active")})dropFileZone.addEventListener("dragleave", function() {  dropFileZone.classList.remove("_active")})
          ["dragover", "drop"].forEach(function(event) {
  document.addEventListener(event, function(evt) {
    evt.preventDefault()
    return false
  })
})

dropFileZone.addEventListener("dragenter", function() {
  dropFileZone.classList.add("_active")
})

dropFileZone.addEventListener("dragleave", function() {
  dropFileZone.classList.remove("_active")
})

        
        
          
        
      

To send the file to the server without reloading the page, we will use XMLHttpRequest — a set of mechanisms for exchanging data between the client and the server without reloading the page. You can read more about it on MDN.

The main work will be performed by the processingUploadFile() function, which takes the selected user file fileInstanceUpload and sends it to the server:

        
          
          function processingUploadFile(file) {  // The code of the function is discussed below}
          function processingUploadFile(file) {
  // The code of the function is discussed below
}

        
        
          
        
      

First, we declare variables:

  • dropZoneData, in which the data to be sent to the server will be stored using the FormData object;
  • xhr for contacting the server using XMLHttpRequest.
        
          
          const dropZoneData = new FormData()const xhr = new XMLHttpRequest()
          const dropZoneData = new FormData()
const xhr = new XMLHttpRequest()

        
        
          
        
      

After that, we specify the sequence of operations for XMLHttpRequest when transmitting the file to the server:

  1. The selected file is saved for sending.
  2. For XMLHttpRequest, an event handler for progress is added, which tracks the file upload process. To show the hidden loading indicator element, it adds the class upload-loader_visible, and the upload hint is hidden by removing the class upload-hint_visible.
  3. The open() method performs a POST request to the management file stored on the server.
  4. The user-selected file is sent to the server.
  5. The loading file event is handled for XMLHttpRequest.

If the file is saved on the server, the loading indicator is hidden, and the user is shown a message of successful file upload. If the file is not accepted by the server, the loading indicator is hidden, and the user is shown an error message.

        
          
          dropZoneData.append("file", file)xhr.open("POST", uploadUrl, true)xhr.send(dropZoneData)xhr.onload = function () {  if (xhr.status == 200) {    setStatus("All uploaded")  } else {    setStatus("Upload error")  }  HTMLElement.style.display = "none"}
          dropZoneData.append("file", file)

xhr.open("POST", uploadUrl, true)

xhr.send(dropZoneData)

xhr.onload = function () {
  if (xhr.status == 200) {
    setStatus("All uploaded")
  } else {
    setStatus("Upload error")
  }
  HTMLElement.style.display = "none"
}

        
        
          
        
      
Open demo in the new window