UI components

Modals

Modals display focused content and actions without leaving the current page. Laravolt v7 uses Alpine.js and Preline UI for accessible, keyboard-navigable modals.

Basic modal

Blade
<div x-data="{ open: false }">
    {{-- Trigger button --}}
    <button 
        @click="open = true"
        type="button" 
        class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700"
    >
        Open modal
    </button>

    {{-- Modal --}}
    <div 
        x-show="open"
        x-cloak
        @keydown.escape.window="open = false"
        class="fixed inset-0 z-50 overflow-y-auto"
    >
        {{-- Backdrop --}}
        <div 
            x-show="open"
            x-transition:enter="ease-out duration-300"
            x-transition:enter-start="opacity-0"
            x-transition:enter-end="opacity-100"
            x-transition:leave="ease-in duration-200"
            x-transition:leave-start="opacity-100"
            x-transition:leave-end="opacity-0"
            @click="open = false"
            class="fixed inset-0 bg-gray-900 bg-opacity-50 dark:bg-opacity-80"
        ></div>

        {{-- Modal content --}}
        <div class="flex min-h-full items-center justify-center p-4">
            <div 
                x-show="open"
                x-transition:enter="ease-out duration-300"
                x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
                x-transition:leave="ease-in duration-200"
                x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
                x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                @click.away="open = false"
                class="relative w-full max-w-lg bg-white border shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-700"
            >
                {{-- Header --}}
                <div class="flex justify-between items-center py-3 px-4 border-b dark:border-neutral-700">
                    <h3 class="font-bold text-gray-800 dark:text-white">
                        Modal title
                    </h3>
                    <button 
                        @click="open = false"
                        type="button" 
                        class="inline-flex items-center justify-center size-7 text-sm font-semibold rounded-full border border-transparent text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-neutral-700"
                    >
                        <svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                        </svg>
                    </button>
                </div>

                {{-- Body --}}
                <div class="p-4 overflow-y-auto">
                    <p class="text-gray-800 dark:text-neutral-400">
                        Modal content goes here.
                    </p>
                </div>

                {{-- Footer --}}
                <div class="flex justify-end items-center gap-x-2 py-3 px-4 border-t dark:border-neutral-700">
                    <button 
                        @click="open = false"
                        type="button" 
                        class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 hover:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700"
                    >
                        Cancel
                    </button>
                    <button 
                        type="button" 
                        class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700"
                    >
                        Save changes
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>

Confirmation dialog

Confirm destructive actions:

Blade
<div x-data="{ confirmDelete: false }">
    <button 
        @click="confirmDelete = true"
        type="button" 
        class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-red-600 text-white hover:bg-red-700"
    >
        Delete user
    </button>

    <div 
        x-show="confirmDelete"
        x-cloak
        @keydown.escape.window="confirmDelete = false"
        class="fixed inset-0 z-50 overflow-y-auto"
    >
        <div 
            x-show="confirmDelete"
            @click="confirmDelete = false"
            class="fixed inset-0 bg-gray-900 bg-opacity-50"
        ></div>

        <div class="flex min-h-full items-center justify-center p-4">
            <div 
                x-show="confirmDelete"
                @click.away="confirmDelete = false"
                class="relative w-full max-w-md bg-white border shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-700"
            >
                <div class="p-4 sm:p-7">
                    <div class="text-center">
                        <div class="mb-4 inline-flex justify-center items-center size-12 rounded-full border-4 border-red-50 bg-red-100 text-red-500 dark:bg-red-700 dark:border-red-600 dark:text-red-100">
                            <svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
                            </svg>
                        </div>

                        <h3 class="mb-2 text-xl font-bold text-gray-800 dark:text-neutral-200">
                            Delete user?
                        </h3>
                        <p class="text-gray-500 dark:text-neutral-500">
                            This action cannot be undone. All data associated with this user will be permanently deleted.
                        </p>

                        <div class="mt-6 flex justify-center gap-x-4">
                            <button 
                                @click="confirmDelete = false"
                                type="button" 
                                class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 hover:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700"
                            >
                                Cancel
                            </button>
                            <form method="POST" action="{{ route('users.destroy', $user) }}">
                                @csrf
                                @method('DELETE')
                                <button 
                                    type="submit"
                                    class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-red-600 text-white hover:bg-red-700"
                                >
                                    Delete user
                                </button>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Form modal

Submit forms inside modals:

Blade
<div x-data="{ showForm: false }">
    <button 
        @click="showForm = true"
        type="button" 
        class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700"
    >
        Create user
    </button>

    <div 
        x-show="showForm"
        x-cloak
        @keydown.escape.window="showForm = false"
        class="fixed inset-0 z-50 overflow-y-auto"
    >
        <div 
            x-show="showForm"
            @click="showForm = false"
            class="fixed inset-0 bg-gray-900 bg-opacity-50"
        ></div>

        <div class="flex min-h-full items-center justify-center p-4">
            <div 
                x-show="showForm"
                @click.away="showForm = false"
                class="relative w-full max-w-lg bg-white border shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-700"
            >
                <div class="flex justify-between items-center py-3 px-4 border-b dark:border-neutral-700">
                    <h3 class="font-bold text-gray-800 dark:text-white">
                        Create new user
                    </h3>
                    <button 
                        @click="showForm = false"
                        type="button" 
                        class="inline-flex items-center justify-center size-7 text-sm font-semibold rounded-full border border-transparent text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-neutral-700"
                    >
                        <svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                        </svg>
                    </button>
                </div>

                <div class="p-4">
                    {!! PrelineForm::open('users.store')->post() !!}
                        <div class="space-y-4">
                            {!! PrelineForm::text('name')->label('Full name')->required() !!}
                            {!! PrelineForm::email('email')->label('Email')->required() !!}
                            {!! PrelineForm::password('password')->label('Password')->required() !!}
                        </div>

                        <div class="mt-6 flex justify-end gap-x-2">
                            <button 
                                @click="showForm = false"
                                type="button" 
                                class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 hover:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700"
                            >
                                Cancel
                            </button>
                            {!! PrelineForm::submit('Create user')->primary() !!}
                        </div>
                    {!! PrelineForm::close() !!}
                </div>
            </div>
        </div>
    </div>
</div>

Livewire integration

Use Livewire for dynamic modal content:

Blade
{{-- Livewire component --}}
<div>
    <button 
        wire:click="$set('showModal', true)"
        type="button" 
        class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700"
    >
        Edit user
    </button>

    @if($showModal)
    <div 
        x-data="{ open: @entangle('showModal') }"
        x-show="open"
        x-cloak
        @keydown.escape.window="open = false"
        class="fixed inset-0 z-50 overflow-y-auto"
    >
        <div 
            x-show="open"
            @click="open = false"
            class="fixed inset-0 bg-gray-900 bg-opacity-50"
        ></div>

        <div class="flex min-h-full items-center justify-center p-4">
            <div 
                x-show="open"
                @click.away="open = false"
                class="relative w-full max-w-lg bg-white border shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-700"
            >
                <div class="p-4">
                    <h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4">
                        Edit user
                    </h3>

                    <form wire:submit="save">
                        <div class="space-y-4">
                            <div>
                                <label class="block text-sm font-medium mb-2">Name</label>
                                <input wire:model="name" type="text" class="py-3 px-4 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400">
                                @error('name') <span class="text-sm text-red-600">{{ $message }}</span> @enderror
                            </div>
                        </div>

                        <div class="mt-6 flex justify-end gap-x-2">
                            <button 
                                wire:click="$set('showModal', false)"
                                type="button" 
                                class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 hover:bg-gray-50"
                            >
                                Cancel
                            </button>
                            <button 
                                type="submit"
                                class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700"
                            >
                                Save changes
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
    @endif
</div>

Accessibility

Modals must be keyboard-navigable and screen-reader friendly:

  • Escape key: Close modal with @keydown.escape.window="open = false"
  • Click outside: Close with @click.away="open = false"
  • Focus trap: Keep focus inside modal while open
  • ARIA attributes: Add role="dialog" and aria-labelledby
Blade
<div 
    role="dialog"
    aria-labelledby="modal-title"
    aria-modal="true"
    x-show="open"
    class="..."
>
    <h3 id="modal-title" class="...">
        Modal title
    </h3>
    ...
</div>

Common pitfalls

Missing x-cloak

Add x-cloak to prevent flash of unstyled content:

Blade
<div x-show="open" x-cloak class="...">

Z-index conflicts

Ensure modal z-index is higher than other elements:

Blade
<div class="fixed inset-0 z-50 ...">

Form submission in modals

Use Livewire or AJAX to avoid page reload:

Blade
<form wire:submit="save">
    {{-- Form fields --}}
</form>

Verification commands

Test modals:

Bash
# Open modal → verify backdrop appears
# Press Escape → verify modal closes
# Click outside → verify modal closes
# Submit form → verify validation errors display
# Check dark mode → verify dark: classes apply

# Accessibility check
# Tab through modal → verify focus stays inside
# Screen reader → verify role="dialog" and aria-* attributes
Previous
Cards