Upload file with progress bar using Laravel, VueJS and Tailwind CSS

Laravel Backend (API route + controller)

routes/api.php 

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\FileUploadController;

Route::post('/upload', [FileUploadController::class, 'upload']);

app/Http/Controllers/FileUploadController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FileUploadController extends Controller
{
    public function upload(Request $request)
    {
        $request->validate([
            'file' => 'required|file|max:10240', // max 10MB
        ]);

        if ($request->hasFile('file')) {
            $path = $request->file('file')->store('uploads', 'public');
            return response()->json(['message' => 'File uploaded successfully', 'path' => $path]);
        }

        return response()->json(['message' => 'No file uploaded'], 400);
    }
}

Make sure your storage:link is set up:

php artisan storage:link

Vue.js Frontend Component (with progress bar)

Vue Component: FileUploader.vue

<template>
  <div class="max-w-md mx-auto p-4 border rounded-xl bg-white shadow">
    <h2 class="text-xl font-semibold mb-4">Upload File</h2>

    <input type="file" @change="handleFileChange" class="mb-4" />

    <button
      :disabled="!file || isUploading"
      @click="uploadFile"
      class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
    >
      {{ isUploading ? 'Uploading...' : 'Upload' }}
    </button>

    <div v-if="isUploading" class="mt-4 w-full bg-gray-200 rounded-full h-4 overflow-hidden">
      <div
        class="bg-blue-500 h-full transition-all duration-300"
        :style="{ width: progress + '%' }"
      ></div>
    </div>

    <p v-if="uploadMessage" class="mt-4 text-green-600 font-medium">{{ uploadMessage }}</p>
    <p v-if="errorMessage" class="mt-4 text-red-600 font-medium">{{ errorMessage }}</p>
  </div>
</template>

<script>
import axios from "axios";

export default {
  name: "FileUploader",
  data() {
    return {
      file: null,
      progress: 0,
      isUploading: false,
      uploadMessage: "",
      errorMessage: ""
    };
  },
  methods: {
    handleFileChange(e) {
      this.file = e.target.files[0];
      this.uploadMessage = "";
      this.errorMessage = "";
    },
    uploadFile() {
      if (!this.file) return;

      const formData = new FormData();
      formData.append("file", this.file);

      this.isUploading = true;
      this.progress = 0;

      axios.post("/api/upload", formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        },
        onUploadProgress: (progressEvent) => {
          this.progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
        }
      })
      .then((res) => {
        this.uploadMessage = res.data.message;
        this.errorMessage = "";
      })
      .catch((err) => {
        this.errorMessage = err.response?.data?.message || "Upload failed.";
        this.uploadMessage = "";
      })
      .finally(() => {
        this.isUploading = false;
      });
    }
  }
};
</script>

Mount Vue App (example)

resources/js/app.js

import { createApp } from 'vue'
import FileUploader from './components/FileUploader.vue'

const app = createApp({})
app.component('file-uploader', FileUploader)
app.mount('#app')

resources/views/welcome.blade.php

<div id="app">
  <file-uploader></file-uploader>
</div>
Baca Juga  Laravel firstOrNew, firstOrCreate, firstOr, and updateOrCreate method
Share your love