Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13: Confirm Box Before Deleting a Record (Blade and JavaScript)

๐Ÿ› ๏ธ Laravel Tutorials5 min read

Laravel 13: Confirm Box Before Deleting a Record (Blade and JavaScript)

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 7, 2026
Laravel 13: Confirm Box Before Deleting a Record (Blade and JavaScript)

To show a confirm box before deleting a record in Laravel 13, send the delete as a DELETE form and add a small JavaScript confirm() or a dialog on submit. Always keep the real protection on the server too: CSRF token, authorisation and route model binding. This guide targets Laravel 13 and PHP 8.3 or newer and uses plain Blade and JavaScript without extra libraries.

Why use a form instead of a link?

A delete link is a GET request. Browsers, crawlers and prefetchers can follow GET links, so a link that deletes data is dangerous. Use a form with @method('DELETE') and @csrf so the request is a real DELETE with a CSRF token.

Prerequisites

  • Laravel 13 on PHP 8.3 or newer.
  • A resource such as Post with a model, migration and controller.

Step 1: Register the route

CSS
// routes/web.php
use App\Http\Controllers\PostController;

Route::middleware('auth')->group(function () {
    Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
    Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
});

Step 2: Write the controller action

JavaScript
<?php

namespace App\Http\Controllers;

use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        return view('posts.index', [
            'posts' => Post::latest()->paginate(10),
        ]);
    }

    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);

        $post->delete();

        return redirect()
            ->route('posts.index')
            ->with('status', 'Post deleted.');
    }
}

Route model binding loads the record and returns 404 when it does not exist. The authorize() call runs the policy so a user cannot delete someone else's record just by changing the id in a form.

Step 3: How do you add the confirm box in Blade?

The simplest option is the browser's built-in confirm dialog, attached with an inline onsubmit handler:

Shell
@foreach ($posts as $post)
    <tr>
        <td>{{ $post->title }}</td>
        <td>
            <form method="POST"
                  action="{{ route('posts.destroy', $post) }}"
                  onsubmit="return confirm('Delete this post? This cannot be undone.');">
                @csrf
                @method('DELETE')
                <button type="submit">Delete</button>
            </form>
        </td>
    </tr>
@endforeach

If the user clicks Cancel, confirm() returns false and the browser does not submit the form. Nothing reaches the server.

Step 4: Can you avoid inline JavaScript?

Yes, and it is cleaner, especially if you use a Content Security Policy. Mark the forms with a data attribute and attach one listener:

Shell
<form method="POST" action="{{ route('posts.destroy', $post) }}"
      data-confirm="Delete this post? This cannot be undone.">
    @csrf
    @method('DELETE')
    <button type="submit">Delete</button>
</form>
JavaScript
document.addEventListener('submit', (event) => {
    const message = event.target.dataset.confirm;

    if (message && !window.confirm(message)) {
        event.preventDefault();
    }
});

Put this snippet in resources/js/app.js. Every form with a data-confirm attribute now asks first, so you do not repeat JavaScript for each button.

How do you use a styled dialog instead?

The native HTML <dialog> element works in modern browsers and gives you a styled confirm without a library:

HTML / XML
<dialog id="confirm-dialog">
    <form method="dialog">
        <p id="confirm-text"></p>
        <button value="cancel">Cancel</button>
        <button value="ok">Delete</button>
    </form>
</dialog>
JavaScript
const dialog = document.getElementById('confirm-dialog');

document.addEventListener('submit', (event) => {
    const form = event.target;
    if (!form.dataset.confirm || form.dataset.confirmed) return;

    event.preventDefault();
    document.getElementById('confirm-text').textContent = form.dataset.confirm;
    dialog.returnValue = '';
    dialog.showModal();

    dialog.addEventListener('close', () => {
        if (dialog.returnValue === 'ok') {
            form.dataset.confirmed = '1';
            form.submit();
        }
    }, { once: true });
});

How do you show the status message after deleting?

The controller flashes a status value to the session. Display it once at the top of your layout so users see that the action worked.

TypeScript
@if (session('status'))
    <div role="status" class="alert">
        {{ session('status') }}
    </div>
@endif

Flash data is removed after the next request, so a page refresh will not show the message again.

How do you test the delete route?

Test two things: the owner can delete, and another user cannot. The second test protects you if someone removes the policy check later.

PHP
it('lets the owner delete a post', function () {
    $user = User::factory()->create();
    $post = Post::factory()->for($user)->create();

    $this->actingAs($user)
        ->delete(route('posts.destroy', $post))
        ->assertRedirect(route('posts.index'));

    $this->assertModelMissing($post);
});

it('blocks other users from deleting', function () {
    $post = Post::factory()->create();

    $this->actingAs(User::factory()->create())
        ->delete(route('posts.destroy', $post))
        ->assertForbidden();

    $this->assertModelExists($post);
});

These tests assume your PostPolicy allows only the owner to delete. Adjust the expectations to match your own rules.

Common mistakes

  • Using an <a href> link for deletion. It is unsafe because it is a GET request.
  • Forgetting @method('DELETE'), which results in a 405 error.
  • Forgetting @csrf, which gives a 419 page expired error.
  • Relying only on the confirm box. It is a user-experience feature, not security. Authorisation must be on the server.
  • Deleting related files or child records manually. Use foreign key cascades or model events so data stays consistent.

How do you verify it works?

  1. Open the list page and click Delete. The confirm box should appear.
  2. Click Cancel and check that the record is still there and no request appears in the network tab.
  3. Click OK and confirm that you are redirected with the status message.
  4. Try sending a DELETE request as a different user, for example in a feature test, and check it returns 403.

FAQ

Should I use soft deletes?

Often yes. Add the SoftDeletes trait so users can restore mistakes. A confirm box does not replace a recovery option.

Why did I get a 419 error?

The CSRF token is missing or expired. Make sure the form contains @csrf and that the session has not timed out.

Can I delete with AJAX instead?

Yes. Send a fetch request with the method DELETE and the X-CSRF-TOKEN header, then remove the row from the page when the response succeeds.

How do I restrict who can delete?

Create a policy with php artisan make:policy PostPolicy --model=Post and implement the delete method.

References

Article Index & Topic Tags
LaravelBladeJavascriptCrud

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.