Get expert guidance in a few days with a Laravel code review

Building a modal with Vue.js and Tailwind CSS

Published on by

Building a modal with Vue.js and Tailwind CSS image

Modal windows are a popular UI component and are useful for many different scenarios. You might use one for alerting a user, showing a form, or even popping up a login form. The uses are limitless.

In this tutorial, we’ll walk through how to build a reusable card modal using Vue.js and Tailwind CSS. The component will use Vue.js slots, so you can change the contents of the modal wherever it is used while retaining the open/close functionality and the wrapper design.

We’ll be starting with a brand-new Laravel 5.8 project. The only additional setup we need to perform is setting up Tailwind, but I won’t be going into detail on how to setup Vue and Tailwind in this tutorial.

Getting started with the modal

To begin, let’s create a CardModal Vue component and register it in the resources/js/app.js file.

// resources/assets/js/components/CardModal.vue
<template>
<div>
The modal will go here.
</div>
</template>
 
<script>
export default {
//
}
</script>
// resources/js/app.js
Vue.component('card-modal', require('./components/CardModal.vue').default);
 
const app = new Vue({
el: '#app',
});

To start using the component, we need to update the resources/views/welcome.blade.php view to the following. Note the .relative class on the body tag.

 
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
 
<title>{{ config('app.name', 'Laravel') }}</title>
 
<script src="{{ asset('js/app.js') }}" defer></script>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body class="relative font-sans p-8">
<div id="app">
<h1 class="font-bold text-2xl text-gray-900">Example Project</h1>
<p class="mb-6">This is just a example text for my tutorial.</p>
 
<card-modal></card-modal>
</div>
</body>
</html>

Making the modal appear

Right now, the text inside the modal will always show. Let’s start by making the component accept a prop to show or hide the contents.

Update the component to accept a showing prop and add a v-if directive to the div in the template to show/hide the contents when the showing prop changes.

<template>
<div v-if="showing">
The modal will go here.
</div>
</template>
 
<script>
export default {
props: {
showing: {
required: true,
type: Boolean
}
}
}
</script>

We’ll also need to add a data property to our Vue instance so we can show or hide the modal from outside the CardModal component. We’ll default the property to false so the modal will be hidden when the page loads.

const app = new Vue({
el: '#app',
data: {
exampleModalShowing: false,
},
});

Then, we need to pass the exampleModalShowing prop to the CardModal in our welcome view. We’ll also need a button to show the modal.

<div id="app">
<h1 class="font-bold text-2xl text-gray-900 ">Example Project</h1>
<p class="mb-6">This is just a example text for my tutorial.</p>
 
<button
class="bg-blue-600 text-white px-4 py-2 text-sm uppercase tracking-wide font-bold rounded-lg"
@click="exampleModalShowing = true"
>
Show Modal
</button>
<card-modal :showing="exampleModalShowing"></card-modal>
</div>

Styling the modal

Next, let’s add some styling to the modal. We’ll need a card surrounding the contents and a semi-transparent background around the card. The background will also need to be position fixed so it can take up the full screen without moving any of the other contents on the page. Let’s start by adding the background and centering the contents. For the transparent background, we will need to add a semi-75 color to our Tailwind configuration.

<template>
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
>
The modal will go here.
</div>
</template>

To add the semi-75 color so the bg-semi-75 class works, we will extend the colors configuration in our tailwind.config.js file.

module.exports = {
theme: {
extend: {
colors: {
'bg-semi-75': 'rgba(0, 0, 0, 0.75)'
}
}
}
};

Now, we need to set a max width, background color, shadow, rounded edges, and padding for the card. We’ll add a div to wrap the content inside the modal and add these classes to it.

<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
>
<div class="w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
The modal will go here.
</div>
</div>

Using slots for the content

Now that we have the basic styling finished, let’s update the component to use a slot so the content of the modal can be configured where the component is used instead of inside the component. This will make the component much more reusable.

First, we need to replace the content inside the component with a <slot>. If you’re not familiar with Vue.js slots, essentially, they allow you to pass html into a component and it will be rendered wherever you specify the <slot> tags.

<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
>
<div class="w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
<slot />
</div>
</div>

Second, in the welcome view, we just place the html we want to show inside the modal between the <card-modal> and </card-modal> tags.

<card-modal :showing="exampleModalShowing">
<h2>Example modal</h2>
<p>This is example text passed through to the modal via a slot.</p>
</card-modal>

Closing the modal

The component is getting close to finished, but we have one little problem. We haven’t made a way to close the modal yet. I’d like to add a few different ways to close the modal. First, we’ll add a simple close x at the top right of the card. We need to add a button to the template that calls a close method inside the component. Be sure to add the .relative class to the card div.

<template>
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
>
<div class="relative w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
<button
aria-label="close"
class="absolute top-0 right-0 text-xl text-gray-500 my-2 mx-4"
@click.prevent="close"
>
×
</button>
<slot />
</div>
</div>
</template>
 
<script>
export default {
props: {
showing: {
required: true,
type: Boolean
}
},
methods: {
close() {
this.$emit('close');
}
}
};
</script>

You’ll see that the close method emits a close event. We’ll need to listen for the event outside the component and update the exampleModalShowing property to false. In the welcome view, we can listen for the event by adding a @close listener on the <card-modal> tag.

<card-modal :showing="exampleModalShowing" @close="exampleModalShowing = false">
<h2 class="text-xl font-bold text-gray-900">Example modal</h2>
<p>This is example text passed through to the modal via a slot.</p>
</card-modal>

To close the modal from outside the component, we can add a button that sets exampleModalShowing to false as well.

<card-modal :showing="exampleModalShowing" @close="exampleModalShowing = false">
<h2 class="text-xl font-bold text-gray-900">Example modal</h2>
<p class="mb-6">This is example text passed through to the modal via a slot.</p>
<button
class="bg-blue-600 text-white px-4 py-2 text-sm uppercase tracking-wide font-bold rounded-lg"
@click="exampleModalShowing = false"
>
Close
</button>
</card-modal>

Now when we click the “Show Modal” button, the modal should appear. When we click the close button or the x inside the modal, the modal should disappear.

I’d also like the modal to close when the background behind the card is clicked. Using Vue.js, it’s pretty easy to add that functionality. We can just add @click.self="close" to the background div and Vue will handle the rest. The .self modifier will make it so the listener is only triggered when the background itself is clicked. Without that modifier, the modal would close whenever anything inside the card is clicked as well, which is not what we want.

<template>
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
@click.self="close"
>
<div class="relative w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
<button
aria-label="close"
class="absolute top-0 right-0 text-xl text-gray-500 my-2 mx-4"
@click.prevent="close"
>
×
</button>
<slot />
</div>
</div>
</template>

Adding a transition

To make the component feel smoother, let’s wrap the component in a transition so the modal fades in. Once again, Vue makes this pretty easy with <Transition> components. We just need to wrap the background div in a <Transition> tag and add a few CSS classes to the bottom of the component.

<template>
<Transition name="fade">
<div
v-if="showing"
class="fixed inset-0 w-full h-screen flex items-center justify-center bg-semi-75"
@click.self="close"
>
<div class="relative w-full max-w-2xl bg-white shadow-lg rounded-lg p-8">
<button
aria-label="close"
class="absolute top-0 right-0 text-xl text-gray-500 my-2 mx-4"
@click.prevent="close"
>
×
</button>
<slot />
</div>
</div>
</Transition>
</template>
 
// script...
 
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: all 0.4s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

Fixing scroll issues

Overall, the component is working pretty well. We can open/close the modal, it fades in nicely, and is really reusable. If you add the component to a page with a lot of content though, you might notice one issue. While the modal is open, if you try to scroll the page, the background is allowed to scroll. This is usually not desirable, so I’ll show you how to fix that issue. We can add a Vue watcher to the showing prop. When the showing prop is set to true, we need to add overflow: hidden to the body element of our page. When it is set to false, we need to remove that style. We can use the .overflow-hidden class provided by Tailwind.

<script>
export default {
props: {
showing: {
required: true,
type: Boolean
}
},
watch: {
showing(value) {
if (value) {
return document.querySelector('body').classList.add('overflow-hidden');
}
 
document.querySelector('body').classList.remove('overflow-hidden');
}
},
methods: {
close() {
this.$emit('close');
}
}
};
</script>

Conclusion

Now that our component is complete, you’re free to use it as you wish, in multiple places with different content in each place. It’s a really useful component for showing small forms, getting user confirmations, and other use cases. I’d love to hear how you end up using the component!

This component is based on some principles taught in Adam Wathan’s “Advanced Vue Component Design” course and simplified/modified for my needs. If you’re interested in learning more about this subject and other advanced Vue.js practices, I would highly recommend checking out his course!

Jason Beggs photo

TALL stack (Tailwind CSS, Alpine.js, Laravel, and Livewire) consultant and owner of designtotailwind.com.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Battle Ready Laravel

The ultimate guide to auditing, testing, fixing and improving your Laravel applications so you can build better apps faster and with more confidence.

Visit Battle Ready Laravel
Curotec logo

Curotec

World class Laravel experts with GenAI dev skills. LATAM-based, embedded engineers that ship fast, communicate clearly, and elevate your product. No bloat, no BS.

Curotec
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $3200/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
Cut PHP Code Review Time & Bugs into Half with CodeRabbit logo

Cut PHP Code Review Time & Bugs into Half with CodeRabbit

CodeRabbit is an AI-powered code review tool that specializes in PHP and Laravel, running PHPStan and offering automated PR analysis, security checks, and custom review features while remaining free for open-source projects.

Cut PHP Code Review Time & Bugs into Half with CodeRabbit
Get expert guidance in a few days with a Laravel code review logo

Get expert guidance in a few days with a Laravel code review

Expert code review! Get clear, practical feedback from two Laravel devs with 10+ years of experience helping teams build better apps.

Get expert guidance in a few days with a Laravel code review
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Harpoon: Next generation time tracking and invoicing logo

Harpoon: Next generation time tracking and invoicing

The next generation time-tracking and billing software that helps your agency plan and forecast a profitable future.

Harpoon: Next generation time tracking and invoicing
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit

The latest

View all →
Laravel 12.44 Adds HTTP Client afterResponse() Callbacks image

Laravel 12.44 Adds HTTP Client afterResponse() Callbacks

Read article
Handle Nested Data Structures in PHP with the Data Block Package image

Handle Nested Data Structures in PHP with the Data Block Package

Read article
Detect and Clean Up Unchanged Vendor Files with Laravel Vendor Cleanup image

Detect and Clean Up Unchanged Vendor Files with Laravel Vendor Cleanup

Read article
Seamless PropelAuth Integration in Laravel with Earhart image

Seamless PropelAuth Integration in Laravel with Earhart

Read article
Laravel API Route image

Laravel API Route

Read article
Laravel News 2025 Recap image

Laravel News 2025 Recap

Read article