Reader Stacks

AJAX Image Upload With Live Preview in Laravel

A working example of uploading an image via AJAX in Laravel with a live browser preview before the request is sent, using FormData and the public disk.

AJAX Image Upload With Live Preview in Laravel

This covers the two parts people usually get stuck on separately: showing the image in the browser before it's uploaded (pure JavaScript, no server round-trip needed), and actually saving the file through an AJAX request without a full page reload.

1. The form

<form id="avatar-form">
    @csrf
    <input type="file" name="avatar" id="avatar-input" accept="image/*">
    <img id="avatar-preview" src="#" alt="" style="display:none; max-width:200px;">
    <button type="submit">Upload</button>
</form>

2. Live preview with FileReader

This part never touches the server — it reads the file the browser already has selected:

document.getElementById('avatar-input').addEventListener('change', function (e) {
    const file = e.target.files[0];
    if (! file) return;

    const reader = new FileReader();
    reader.onload = function (event) {
        const preview = document.getElementById('avatar-preview');
        preview.src = event.target.result;
        preview.style.display = 'block';
    };
    reader.readAsDataURL(file);
});

3. Submitting via AJAX with FormData

File inputs can't be serialized as JSON — FormData is required for a multipart upload:

document.getElementById('avatar-form').addEventListener('submit', function (e) {
    e.preventDefault();

    const formData = new FormData(this);

    fetch('/profile/avatar', {
        method: 'POST',
        headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
        body: formData,
    })
        .then(res => res.json())
        .then(data => console.log('Uploaded:', data.url))
        .catch(err => console.error(err));
});

Note there's no manual Content-Type header set — the browser needs to generate its own multipart boundary string, which it can only do if you let it set the header itself.

4. The controller

public function store(Request $request)
{
    $request->validate([
        'avatar' => ['required', 'image', 'max:4096'],
    ]);

    $path = $request->file('avatar')->store('avatars', 'public');

    $request->user()->update(['avatar_path' => $path]);

    return response()->json([
        'url' => Storage::disk('public')->url($path),
    ]);
}

Make sure php artisan storage:link has been run at least once so public/storage resolves to storage/app/public — without it, the returned URL will 404 even though the file uploaded successfully.

Topics: File Uploads & Media Forms & Validation