Home / Blog / Vue File Upload: Build a File Upload Component
Vue File Upload: Build a File Upload Component

Vue File Upload: Build a File Upload Component

This entry is part 1 of 2 in the series File Upload in Vue.js

To upload files in Vue, wrap a native file input in a component, capture the selected files on the change event, store them in a reactive ref, and emit them to the parent. Style a label to replace the default input UI, and support multiple files with the multiple attribute.

In this tutorial, we’ll build a flexible vue file upload component in Vue.js using the Composition API (<script setup>). It will:

  1. Allow the user to select one or multiple files
  2. Display each file name below an upload button
  3. Validate file types and size limits on the client side
  4. Allow a file to be deleted from the list
  5. Emit a changed event with an array of the selected File objects whenever files are added or removed.

If you want to learn about developing a more robust file upload system including features like displaying image previews, supporting drag-and-drop, and connecting to a backend API, check out our comprehensive course File Uploads in Vue.js.

Let’s Get Started With an HTML File Input

The first step in creating any file upload component is creating an HTML file input element.

<!-- FileInput.vue -->
<script setup lang="ts"></script>

<template>
  <input type="file" />
</template>

It can optionally accept multiple files with the multiple attribute.

<input type="file" multiple />

Or we can make multiple a prop using defineProps to give control to the consuming component.

<!-- FileInput.vue -->
<script setup lang="ts">
defineProps<{
  multiple?: boolean
}>()
</script>

<template>
  <input type="file" :multiple="multiple" />
</template>

Use a Button Styled Label to Trigger the File Input

Out of the box, the browser's native file input is hard to style consistently across browsers. We can work around this by taking advantage of the default behavior of HTML labels.

Default file input style isn’t so pretty

When a label is clicked, the browser automatically triggers the associated <input>. We hide the native file input accessibly and style the <label> as a button.

<template>
  <label for="file-input" class="btn cursor-pointer">
    Upload File
  </label>
  <input id="file-input" type="file" class="sr-only" hidden />
</template>
button-upload.gif

Capture Files on the Input Change Event

In order to capture the files that the user selects, listen for the change event on the file input element.

<input type="file" multiple @change="handleFileSelect" hidden />

Selected files are available on event.target.files as a FileList.

function handleFileSelect(e: Event) {
  const input = e.target as HTMLInputElement
  const filesAsArray = Array.from(input?.files || [])
}

Store the Selected Files in a Reactive Ref

To render the selected files in the UI and emit them when files are added or removed, store them in a reactive array using a Vue ref.

import { ref } from 'vue'

const files = ref<File[]>([])

In your handleFileSelect function, append newly selected files to the files ref:

function handleFileSelect(e: Event) {
  const input = e.target as HTMLInputElement
  const filesAsArray = Array.from(input?.files || [])
  files.value = files.value.concat(filesAsArray)
}

Client-Side File Validation (Type & Size Limits)

Before adding files to state, it's best practice to validate file types and maximum allowed file sizes on the client side:

const props = withDefaults(defineProps<{
  multiple?: boolean
  maxSizeMB?: number
  accept?: string
}>(), {
  maxSizeMB: 5,
  accept: 'image/*,application/pdf'
})

const errorMessage = ref<string | null>(null)

function handleFileSelect(e: Event) {
  errorMessage.value = null
  const input = e.target as HTMLInputElement
  const selectedFiles = Array.from(input?.files || [])

  const validFiles = selectedFiles.filter(file => {
    const sizeInMB = file.size / (1024 * 1024)
    if (sizeInMB > props.maxSizeMB) {
      errorMessage.value = `File "${file.name}" exceeds the ${props.maxSizeMB}MB limit.`
      return false
    }
    return true
  })

  files.value = files.value.concat(validFiles)
}

Render the Selected Files in the UI

To render the selected files in the UI, loop over the files ref with v-for:

<template>
  <ul>
    <li v-for="file in files" :key="file.name">
      {{ file.name }}
    </li>
  </ul>
</template>
list-files.gif

Emit the Selected Files

To inform the parent component whenever a file is added or removed, use defineEmits and a watcher:

import { watch } from 'vue'

const emit = defineEmits<{
  (e: 'changed', files: File[]): void
}>()

watch(files, (newFiles) => {
  emit('changed', newFiles)
})

Remove a File from the List

To remove a file from the list, use splice on the files ref:

function removeFile(index: number) {
  files.value.splice(index, 1)
}

Add a button in the template so users can delete individual files:

<template>
  <ul>
    <li v-for="(file, index) in files" :key="file.name">
      {{ file.name }}
      <button @click="removeFile(index)">Remove</button>
    </li>
  </ul>
</template>
delete-files.gif

Image Previews & Sending Files to a Server

1. Generating Image Previews

For image uploads, generate a temporary preview URL using URL.createObjectURL:

function getPreviewUrl(file: File): string {
  return URL.createObjectURL(file)
}
<template>
  <img v-if="file.type.startsWith('image/')" :src="getPreviewUrl(file)" class="w-16 h-16 object-cover rounded" />
</template>

2. Uploading Files with FormData

When submitting files to a server API, append them to a FormData object:

async function uploadToServer() {
  const formData = new FormData()
  files.value.forEach((file, index) => {
    formData.append(`files[${index}]`, file)
  })

  await fetch('/api/upload', {
    method: 'POST',
    body: formData
    // Do NOT set Content-Type header manually; fetch sets the multipart boundary automatically
  })
}

The Complete Component

Putting all the pieces together yields a production-ready Vue 3 Composition API file upload component:

<template>
  <div class="file-upload-container">
    <label for="file-input" class="btn">Upload File</label>
    <input
      id="file-input"
      type="file"
      :multiple="multiple"
      :accept="accept"
      @change="handleFileSelect"
      hidden
    />
    <p v-if="errorMessage" class="error-text">{{ errorMessage }}</p>
    <ul v-if="files.length" class="file-list">
      <li v-for="(file, index) in files" :key="`${file.name}-${index}`">
        <img v-if="file.type.startsWith('image/')" :src="getPreviewUrl(file)" class="file-preview" />
        <span>{{ file.name }} ({{ (file.size / 1024).toFixed(1) }} KB)</span>
        <button @click="removeFile(index)">Remove</button>
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'

const props = withDefaults(defineProps<{
  multiple?: boolean
  maxSizeMB?: number
  accept?: string
}>(), {
  multiple: false,
  maxSizeMB: 5,
  accept: 'image/*,application/pdf'
})

const emit = defineEmits<{
  (e: 'changed', files: File[]): void
}>()

const files = ref<File[]>([])
const errorMessage = ref<string | null>(null)

function handleFileSelect(e: Event) {
  errorMessage.value = null
  const input = e.target as HTMLInputElement
  const selectedFiles = Array.from(input?.files || [])

  const validFiles = selectedFiles.filter(file => {
    const sizeMB = file.size / (1024 * 1024)
    if (sizeMB > props.maxSizeMB) {
      errorMessage.value = `File "${file.name}" exceeds the ${props.maxSizeMB}MB size limit.`
      return false
    }
    return true
  })

  files.value = files.value.concat(validFiles)
}

function removeFile(index: number) {
  files.value.splice(index, 1)
}

function getPreviewUrl(file: File): string {
  return URL.createObjectURL(file)
}

watch(files, (newFiles) => {
  emit('changed', newFiles)
}, { deep: true })
</script>

Frequently Asked Questions (FAQ)

How do you upload a file in Vue?

Wrap a native HTML <input type="file"> in a Vue component, capture the selected files on the @change event, store them in a reactive ref<File[]>([]), and emit them or send them to a server backend using fetch and FormData.

How do I handle multiple file uploads in Vue?

Add the multiple attribute to the native file input element (<input type="file" multiple />), convert event.target.files into an array using Array.from(), and append them to a reactive array ref.

How do I style the file input in Vue?

Hide the native file input using CSS (hidden or sr-only) and create an accessible <label for="file-input"> styled with custom CSS or Tailwind CSS. Clicking the label opens the browser's native file picker dialog.

How do I preview an uploaded image before sending it?

Generate a temporary object URL using URL.createObjectURL(file) and bind it to an <img> tag's src attribute. Remember to clean up object URLs when files are removed.

How do I send the uploaded file to a server?

Append each File object to a JavaScript FormData instance (formData.append('file', file)) and POST it via fetch or Axios. Do not manually set the Content-Type header—the browser automatically sets the correct multipart/form-data header with boundary delimiters.

Conclusion

This simple yet functional file upload component demonstrates the power and simplicity of Vue.js with the Composition API. It provides a solid foundation that you can build upon based on your specific needs. Style it however you’d like! With Tailwind CSS, it’s easy to make it look great.

Add more advanced features like:

  • file previews
  • drag and drop
  • validation
  • and hook it up to a backend API

in our comprehensive course File Uploads in Vue.js or master production-grade component architectures in the Vue.js 3 Master Class! Don't miss it

How do you upload a file in Vue?

Use a native file input, capture files on its change event, store them in a reactive ref, and emit them or send with fetch/FormData.

How do I handle multiple file uploads in Vue?

Add the multiple attribute, read event.target.files as an array, store in a ref, render each with a remove control.

How do I style the file input in Vue?

Hide the native input and trigger it from a styled label bound to the input id.

How do I preview an uploaded image before sending it?

Create an object URL with URL.createObjectURL and bind it to an img src; revoke it when removed.

How do I send the uploaded file to a server?

Append the file to a FormData object and POST with fetch. Do not set Content-Type manually; the browser sets the multipart boundary.

Start learning Vue.js for free

Comments

Latest Vue School Articles

5 Component Design Patterns to Boost Your Vue.js Applications

5 Component Design Patterns to Boost Your Vue.js Applications

5 essential Vue.js component design patterns, including branching components, slots usage, list organization, smart vs dumb components, and form handling - perfect for both Vue beginners and experienced developers looking to improve code maintainability and scalability.
Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Why I built a Nuxt + Jazz powered real time editor, how you can use it, and a list of takeaways on building with the help of AI.
VueSchool logo

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social