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
.
The article will consider the option of selecting a file using dragging.
Uploading a file to the server consists of three parts:
- The user selects a file on their device.
- Checking the parameters processing the file and forming data to contact the server.
- 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) } }) }
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
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
. 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
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:
drop
sets the area for processing the selected file;File Zone status
points to the hint about uploading the file;Text set
is needed to store status text;Status upload
sets the area for the button to upload a file without dragging.Input
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 set
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 prevent
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 XML
— 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 processing
function, which takes the selected user file file
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:
drop
, in which the data to be sent to the server will be stored using theZone Data Form
object;Data xhr
for contacting the server usingXML
.Http Request
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 XML
when transmitting the file to the server:
- The selected file is saved for sending.
- For
XML
, an event handler forHttp Request progress
is added, which tracks the file upload process. To show the hidden loading indicator element, it adds the classupload
, and the upload hint is hidden by removing the class- loader _ visible upload
.- hint _ visible - The
open
method performs a POST request to the management file stored on the server.( ) - The user-selected file is sent to the server.
- The loading file event is handled for
XML
.Http Request
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" }