Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

@tsommie's avatar

Unable to swap Jetstream default TailwindCSS modal for Bootstrap Modal and still achieve the expected or same result as its was with TailwindCSS

Am working on a Laravel 8 package that swaps the #TALL stack for what I call the #BALL stack, and its basically a Bootstrap, AlpineJs Laravel, Livewire stack. Bootstrap 5 is leaning towards utility classes and no longer makes use of JQuery which gives room for AlpineJs so I really don't see a lot of upsides with Tailwind, not to mention I've gotten really comfortable with Bootstrap which should be all that matters at the end right?

Now the problem is this, I've been able to make changes to a lot of the JetStream blade files and if you've installed and made use of Jetstream then your familiar with this image:

enter image description here

(Yes!!!! Its the exact same thing but with Bootstrap!)

but one particular component has kept me up for days and its the Bootstrap Modal.

  1. Whenever I call the Bootstrap modal Livewire requests seem to make the Modal itself disappear leaving just the backdrop, forcing a page request before anything else can be clicked.

  2. The final request is never completed

  3. I have no way to hold the modal until a confirmation request is loaded and then programmatically disable the modal when finished.

The exact place I have this problem is in the views/api/api-token-manager.blade which is added by Jetstream when you install the livewire stack. My files looks like so:

My views/api/api-token-manager.blade

<!-- API Token List -->
// Inside a x-jet-action-section component
<x-slot name="content">
    <div class="space-y-6">
    @foreach ($this->user->tokens->sortBy('name') as $token)
        <div class="d-flex justify-content-between">
            <div>
                {{ $token->name }}
            </div>

            <div class="d-flex">
            @if ($token->last_used_at)
                <div class="text-sm text-muted">
                    Last used {{ $token->last_used_at->diffForHumans() }}
                </div>
            @endif

            @if (Laravel\Jetstream\Jetstream::hasPermissions())
                <button class="btn btn-link text-secondary" data-toggle="modal"
                                                wire:click="manageApiTokenPermissions({{ $token->id }})"
                                                data-target="#managingApiTokenPermissions-{{ $token->id }}">
                   Permissions
               </button>
            @endif

            <button class="btn btn-link text-danger text-decoration-none"
                                            data-toggle="modal"
                                            data-target="#confirmApiTokenDeletion-{{ $token->id }}">
                Delete
            </button>
            </div>
        </div>

                            <!-- API Token Permissions Modal -->
                            <x-jet-dialog-modal id="managingApiTokenPermissions-{{ $token->id }}">
                                <x-slot name="title">
                                    API Token Permissions
                                </x-slot>

                                <x-slot name="content">
                                    <div class="mt-2 row">
                                        @foreach (Laravel\Jetstream\Jetstream::$permissions as $permission)
                                            <div class="col-6">
                                                <div class="form-check">
                                                    <input class="form-check-input" type="checkbox" value="{{ $permission }}" wire:model.defer="updateApiTokenForm.permissions">
                                                    <label class="form-check-label">
                                                        {{ $permission }}
                                                    </label>
                                                </div>
                                            </div>
                                        @endforeach
                                    </div>
                                </x-slot>

                                <x-slot name="footer">
                                    <x-jet-secondary-button data-dismiss="modal">
                                        {{ __('Nevermind') }}
                                    </x-jet-secondary-button>

                                    <x-jet-button class="ml-2" wire:click="updateApiToken"
                                                  wire:click="$emit('updateApiToken', {{ $token->id }})"
                                                  data-dismiss="modal">
                                        {{ __('Save') }}
                                    </x-jet-button>
                                </x-slot>
                            </x-jet-dialog-modal>
                            @push('scripts')
                                <script>
                                    Livewire.on('updateApiToken', id => {
                                        @this.manageApiTokenPermissions(id)
                                        @this.updateApiToken()
                                    })
                                </script>
                            @endpush

                            <!-- Delete Token Confirmation Modal -->
                            <x-jet-confirmation-modal id="confirmApiTokenDeletion-{{ $token->id }}">
                                <x-slot name="title">
                                    Delete API Token
                                </x-slot>

                                <x-slot name="content">
                                    Are you sure you would like to delete this API token?
                                </x-slot>

                                <x-slot name="footer">
                                    <x-jet-secondary-button data-dismiss="modal">
                                        Nevermind
                                    </x-jet-secondary-button>

                                    <x-jet-danger-button class="ml-2"
                                                         wire:click="$emit('deleteApiToken', {{ $token->id }})"
                                                         data-dismiss="modal">
                                        Delete
                                    </x-jet-danger-button>
                                </x-slot>
                            </x-jet-confirmation-modal>
                            @push('scripts')
                                <script>
                                    Livewire.on('deleteApiToken', id => {
                                        @this.confirmApiTokenDeletion(id)
                                        @this.deleteApiToken()
                                    })
                                </script>
                            @endpush
                        @endforeach
                    </div>
                </x-slot>

My x-jet-dialog-modal component

@props(['id' => null, 'maxWidth' => null])

<x-jet-modal :id="$id" :maxWidth="$maxWidth" {{ $attributes }}>
    <div class="modal-content">
        <div class="modal-header">
            <h5 class="modal-title">{{ $title }}</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
        <div class="modal-body">
            {{ $content }}
        </div>
        <div class="modal-footer">
            {{ $footer }}
        </div>
    </div>
</x-jet-modal>

My x-jet-modal component

@props(['id', 'maxWidth', 'modal' => false])

@php
$id = $id ?? md5($attributes->wire('model'));

switch ($maxWidth ?? '') {
    case 'sm':
        $maxWidth = ' modal-sm';
        break;
    case 'md':
        $maxWidth = '';
        break;
    case 'lg':
        $maxWidth = ' modal-lg';
        break;
    case 'xl':
        $maxWidth = ' modal-xl';
        break;
    case '2xl':
    default:
        $maxWidth = '';
        break;
}
@endphp

<!-- Modal -->
<div class="modal fade" tabindex="-1" id="{{ $id }}" aria-labelledby="{{ $id }}" aria-hidden="true">
    <div class="modal-dialog{{ $maxWidth }}">
        {{ $slot }}
    </div>
</div>

I really don't want to make the change to Tailwind regardless of the hype so I would love any assistance the community can render.

To make a direct contribution to the project click here.

Thanks!!!

0 likes
3 replies
armadillo's avatar

This composer package brings the ui into Laravel 8. It lets you build scaffolding and auth the same as in Laravel 7 , but this is for Laravel 8: https://github.com/rogervila/laravel-legacy-ui

Thats what i use for my new Laravel 8 projects with Bootstrap 4 admin themes that i need for custom CMS and CRM projects, Tailwind doesnt have many options for these type of projects, unless one is willing to code all of those UI components that are ready out of the box for Bootstrap.

@tsommie's avatar

The objective is not to create a version of Laravel UI that runs on Laravel 8. The goal is to change the entire view of Jetstream from Tailwind to Bootstrap with little or no difference when viewed on the browser. You suggested won't come with Two-factor auth, Teams, and User profile that comes with jetstream, which is a major setback.

But I think it's complete so you can try it: Jetstrap

Please or to participate in this conversation.