How to Build a Modal Video Component with Tailwind CSS and Vue

How to Build a Modal Video Component with Tailwind CSS and Vue

·

12 min read

Originally published at cruip.com

--

👉 Live Demo / Download

Welcome to the third and final part of our series on How to Create a Pricing Table with a Monthly/Yearly Toggle Switch in Tailwind CSS. As for the previous (and upcoming) series, our objective is to provide step-by-step instructions for building different Tailwind CSS components in HTML, React (with Next.js), and Vue. Our primary focus, however, is to show how to maintain code that is as similar and consistent as possible across these frameworks, especially Next.js and Vue.

In this final part, we will dive into creating the pricing table component using Vue, closely resembling what we previously demonstrated in the Next.js tutorial. By crafting the Vue equivalent of the Next.js component, we aim to compare the two approaches, gain a comprehensive understanding of each framework’s logic, and elevate our development skills.

Let’s get started by creating a new file named PricingTable.vue within the components folder of our app. Copy the HTML code from the previous tutorial and proceed to complete it using Vue.

Then, define the structure of our Vue component:

<script setup lang="ts">
</script>
<template>
    <div>
        <!-- Pricing toggle -->
        <!-- Pricing table -->
    </div>
</template>

Creating a variable to display annual or monthly pricing

Just like we did previously with Next.js using useState, we will create a variable called isAnnual in Vue. This variable controls whether annual or monthly prices are displayed.

Let’s begin by creating this variable within the <script> tag using Vue’s ref function:

<script setup lang="ts">
import { ref } from 'vue'
const isAnnual = ref<boolean>(true)
</script>
<template>
    <div>
        <!-- Pricing toggle -->
        <!-- Pricing table -->
    </div>
</template>

We are utilizing a ref because every time the user clicks on the buttons, we need to update the variable’s state, and subsequently update the component to display either annual or monthly prices.

The initial state of the ref is set to true since we want to show the annual prices by default.

Adding the pricing toggle

Let’s now create the toggle that allows users to switch between annual and monthly prices – as a reminder, the toggle consists of two button elements:

<script setup lang="ts">
import { ref } from 'vue'
const isAnnual = ref<boolean>(true)
</script>
<template>
    <div>
        <!-- Pricing toggle -->
        <div class="flex justify-center max-w-[14rem] m-auto mb-8 lg:mb-16">
            <div class="relative flex w-full p-1 bg-white dark:bg-slate-900 rounded-full">
                <span class="absolute inset-0 m-1 pointer-events-none" aria-hidden="true">
                    <span class="absolute inset-0 w-1/2 bg-indigo-500 rounded-full shadow-sm shadow-indigo-950/10 transform transition-transform duration-150 ease-in-out" :class="isAnnual ? 'translate-x-0' : 'translate-x-full'"></span>
                </span>
                <button
                    class="relative flex-1 text-sm font-medium h-8 rounded-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ease-in-out"
                    :class="isAnnual ? 'text-white' : 'text-slate-500 dark:text-slate-400'"
                    @click="isAnnual = true"
                >Yearly <span
                    :class="isAnnual ? 'text-indigo-200' : 'text-slate-400 dark:text-slate-500'">-20%</span></button>
                <button
                    class="relative flex-1 text-sm font-medium h-8 rounded-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ease-in-out"
                    :class="isAnnual ? 'text-slate-500 dark:text-slate-400' : 'text-white'"
                    @click="isAnnual = false"
                >Monthly</button>
            </div>
        </div>        
        <!-- Pricing table -->
    </div>
</template>

As you may have noticed, we use the :class directive to dynamically manage CSS classes based on the value of the isAnnual variable.

When isAnnual is true, the element with the indigo background is positioned to the left, highlighting the “Yearly” button. Vice versa, when isAnnual is false, the element shifts to the right, highlighting the “Monthly” button.

Additionally, when isAnnual is true, the text of the first button appears white, while the text of the second button is gray. When isAnnual is false, the text of the first button appears gray, while the text of the second button is white.

Lastly, we add the @click directive to handle the click event on each button. Clicking the first button sets isAnnual to true, while clicking the second button sets it to false.

Creating the pricing table

With the toggle in place, we can now insert the pricing table. We will copy the HTML code from our previous tutorial, and complete it with Vue.

To dynamically handle the displayed prices based on the isAnnual variable, we’ll add the v-text directive. For example, in the first pricing tab:

Copy

Here’s the complete code for the pricing table section:

<div class="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none">
    <!-- Pricing tab 1 -->
    <div class="h-full">
        <div class="relative flex flex-col h-full p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-900 shadow shadow-slate-950/5">
            <div class="mb-5">
                <div class="text-slate-900 dark:text-slate-200 font-semibold mb-1">Essential</div>
                <div class="inline-flex items-baseline mb-2">
                    <span class="text-slate-900 dark:text-slate-200 font-bold text-3xl">$</span>
                    <span class="text-slate-900 dark:text-slate-200 font-bold text-4xl" v-text="isAnnual ? '29' : '35'"></span>
                    <span class="text-slate-500 font-medium">/mo</span>
                </div>
                <div class="text-sm text-slate-500 mb-5">There are many variations available, but the majority have suffered.</div>
                <a class="w-full inline-flex justify-center whitespace-nowrap rounded-lg bg-indigo-500 px-3.5 py-2.5 text-sm font-medium text-white shadow-sm shadow-indigo-950/10 hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                    Purchase Plan
                </a>
            </div>
            <div class="text-slate-900 dark:text-slate-200 font-medium mb-3">Includes:</div>
            <ul class="text-slate-600 dark:text-slate-400 text-sm space-y-3 grow">
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Unlimited placeholder texts</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Consectetur adipiscing elit</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Excepteur sint occaecat cupidatat</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Officia deserunt mollit anim</span>
                </li>
            </ul>
        </div>
    </div>
    <!-- Pricing tab 2 -->
    <div class="h-full dark">
        <div class="relative flex flex-col h-full p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-900 shadow shadow-slate-950/5">
            <div class="absolute top-0 right-0 mr-6 -mt-4">
                <div class="inline-flex items-center text-xs font-semibold py-1.5 px-3 bg-emerald-500 text-white rounded-full shadow-sm shadow-slate-950/5">Most Popular</div>
            </div>
            <div class="mb-5">
                <div class="text-slate-900 dark:text-slate-200 font-semibold mb-1">Perform</div>
                <div class="inline-flex items-baseline mb-2">
                    <span class="text-slate-900 dark:text-slate-200 font-bold text-3xl">$</span>
                    <span class="text-slate-900 dark:text-slate-200 font-bold text-4xl" v-text="isAnnual ? '49' : '55'"></span>
                    <span class="text-slate-500 font-medium">/mo</span>
                </div>
                <div class="text-sm text-slate-500 mb-5">There are many variations available, but the majority have suffered.</div>
                <a class="w-full inline-flex justify-center whitespace-nowrap rounded-lg bg-indigo-500 px-3.5 py-2.5 text-sm font-medium text-white shadow-sm shadow-indigo-950/10 hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                    Purchase Plan
                </a>
            </div>
            <div class="text-slate-900 dark:text-slate-200 font-medium mb-3">Includes:</div>
            <ul class="text-slate-600 dark:text-slate-400 text-sm space-y-3 grow">
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Unlimited placeholder texts</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Consectetur adipiscing elit</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Excepteur sint occaecat cupidatat</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Officia deserunt mollit anim</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Predefined chunks as necessary</span>
                </li>
            </ul>
        </div>
    </div>
    <!-- Pricing tab 3 -->
    <div class="h-full">
        <div class="relative flex flex-col h-full p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-900 shadow shadow-slate-950/5">
            <div class="mb-5">
                <div class="text-slate-900 dark:text-slate-200 font-semibold mb-1">Enterprise</div>
                <div class="inline-flex items-baseline mb-2">
                    <span class="text-slate-900 dark:text-slate-200 font-bold text-3xl">$</span>
                    <span class="text-slate-900 dark:text-slate-200 font-bold text-4xl" v-text="isAnnual ? '79' : '85'"></span>
                    <span class="text-slate-500 font-medium">/mo</span>
                </div>
                <div class="text-sm text-slate-500 mb-5">There are many variations available, but the majority have suffered.</div>
                <a class="w-full inline-flex justify-center whitespace-nowrap rounded-lg bg-indigo-500 px-3.5 py-2.5 text-sm font-medium text-white shadow-sm shadow-indigo-950/10 hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                    Purchase Plan
                </a>
            </div>
            <div class="text-slate-900 dark:text-slate-200 font-medium mb-3">Includes:</div>
            <ul class="text-slate-600 dark:text-slate-400 text-sm space-y-3 grow">
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Unlimited placeholder texts</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Consectetur adipiscing elit</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Excepteur sint occaecat cupidatat</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Officia deserunt mollit anim</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Predefined chunks as necessary</span>
                </li>
                <li class="flex items-center">
                    <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                    </svg>
                    <span>Free from repetition</span>
                </li>
            </ul>
        </div>
    </div>
</div>

The component is now complete and fully functional. However, we can further refine the code.

A significant portion of it is identical for all three pricing tabs, so we can create a reusable pricing tab component. By passing only the properties that distinguish the individual tabs to this component, we can keep our code DRY, improve cleanliness, and enhance maintainability.

Creating a reusable pricing tab component

To create the reusable pricing tab component, we need to create a new file in the components folder named PricingTab.vue. This approach differs from the Next.js project, as Vue’s template syntax requires a separate file for this component.

The properties that we’ll pass to the component are as follows:

  • yearly: a boolean indicating whether the pricing table is for annual or monthly pricing

  • popular: an optional prop indicating if the pricing tab should be highlighted as the most popular

  • planName: the name of the plan

  • price: an object containing the monthly and annual prices

  • planDescription: the description of the plan

  • features: an array of strings containing the plan’s features

Since TypeScript is being used, we’ll also define an interface for the props. Here’s the complete code for the PricingTab.vue component:

<script setup lang="ts">
interface Props {
    yearly: boolean
    popular?: boolean
    planName: string
    price: {
        monthly: number
        yearly: number
    }
    planDescription: string
    features: string[]
}
const props = defineProps<Props>()
</script>
<template>
    <div class="h-full" :class="{ 'dark': props.popular }">
        <div class="relative flex flex-col h-full p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-900 shadow shadow-slate-950/5">
            <div v-if="props.popular" class="absolute top-0 right-0 mr-6 -mt-4">
                <div class="inline-flex items-center text-xs font-semibold py-1.5 px-3 bg-emerald-500 text-white rounded-full shadow-sm shadow-slate-950/5">Most Popular</div>
            </div>
            <div class="mb-5">
                <div class="text-slate-900 dark:text-slate-200 font-semibold mb-1">{{ props.planName }}</div>
                <div class="inline-flex items-baseline mb-2">
                    <span class="text-slate-900 dark:text-slate-200 font-bold text-3xl">$</span>
                    <span class="text-slate-900 dark:text-slate-200 font-bold text-4xl" v-text="yearly ? props.price.yearly : props.price.monthly"></span>
                    <span class="text-slate-500 font-medium">/mo</span>
                </div>
                <div class="text-sm text-slate-500 mb-5">{{ props.planDescription }}</div>
                <a class="w-full inline-flex justify-center whitespace-nowrap rounded-lg bg-indigo-500 px-3.5 py-2.5 text-sm font-medium text-white shadow-sm shadow-indigo-950/10 hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                    Purchase Plan
                </a>
            </div>
            <div class="text-slate-900 dark:text-slate-200 font-medium mb-3">Includes:</div>
            <ul class="text-slate-600 dark:text-slate-400 text-sm space-y-3 grow">
                <template v-for="feature in props.features">
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>{{ feature }}</span>
                    </li>
                </template>
            </ul>
        </div>
    </div>
</template>

Note that the features list is an array, so we can use a <template> tag to iterate over it using the v-for directive, and render each individual feature.

Now that we have the component, we can import it into the PricingTable.vue file and use it instead of the code that defined the individual tabs. Here’s an example:

<PricingTab
    :yearly="isAnnual"
    planName="Essential"
    :price="{ yearly: 29, monthly: 35 }"
    planDescription="There are many variations available, but the majority have suffered."
    :features="[
        'Unlimited placeholder texts',
        'Consectetur adipiscing elit',
        'Excepteur sint occaecat cupidatat',
        'Officia deserunt mollit anim',
    ]" />

Conclusions

The PricingTable.vue and PricingTab.vue components are both ready, and PricingTab can be imported and used within PricingTable.

Here is the complete code for PricingTable.vue:

<script setup lang="ts">
import { ref } from 'vue'
import PricingTab from './PricingTab.vue';
const isAnnual = ref<boolean>(true)
</script>
<template>
    <div>
        <!-- Pricing toggle -->
        <div class="flex justify-center max-w-[14rem] m-auto mb-8 lg:mb-16">
            <div class="relative flex w-full p-1 bg-white dark:bg-slate-900 rounded-full">
                <span class="absolute inset-0 m-1 pointer-events-none" aria-hidden="true">
                    <span class="absolute inset-0 w-1/2 bg-indigo-500 rounded-full shadow-sm shadow-indigo-950/10 transform transition-transform duration-150 ease-in-out" :class="isAnnual ? 'translate-x-0' : 'translate-x-full'"></span>
                </span>
                                <button
                    class="relative flex-1 text-sm font-medium h-8 rounded-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ease-in-out"
                    :class="isAnnual ? 'text-white' : 'text-slate-500 dark:text-slate-400'"
                    @click="isAnnual = true"
                >Yearly <span
                    :class="isAnnual ? 'text-indigo-200' : 'text-slate-400 dark:text-slate-500'">-20%</span></button>
                <button
                    class="relative flex-1 text-sm font-medium h-8 rounded-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ease-in-out"
                    :class="isAnnual ? 'text-slate-500 dark:text-slate-400' : 'text-white'"
                    @click="isAnnual = false"
                >Monthly</button>
            </div>
        </div>
        <div class="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none">
            <!-- Pricing tab 1 -->
            <PricingTab
                :yearly="isAnnual"
                planName="Essential"
                :price="{ yearly: 29, monthly: 35 }"
                planDescription="There are many variations available, but the majority have suffered."
                :features="[
                    'Unlimited placeholder texts',
                    'Consectetur adipiscing elit',
                    'Excepteur sint occaecat cupidatat',
                    'Officia deserunt mollit anim',
                ]" />
            <!-- Pricing tab 2 -->
            <PricingTab
                :yearly="isAnnual"
                :popular="true"
                planName="Perform"
                :price="{ yearly: 49, monthly: 55 }"
                planDescription="There are many variations available, but the majority have suffered."
                :features="[
                    'Unlimited placeholder texts',
                    'Consectetur adipiscing elit',
                    'Excepteur sint occaecat cupidatat',
                    'Officia deserunt mollit anim',
                    'Predefined chunks as necessary',
                ]" />
            <!-- Pricing tab 3 -->
            <PricingTab
                :yearly="isAnnual"
                planName="Enterprise"
                :price="{ yearly: 79, monthly: 85 }"
                planDescription="There are many variations available, but the majority have suffered."
                :features="[
                    'Unlimited placeholder texts',
                    'Consectetur adipiscing elit',
                    'Excepteur sint occaecat cupidatat',
                    'Officia deserunt mollit anim',
                    'Predefined chunks as necessary',
                    'Free from repetition',
                ]" />
        </div>
    </div>
</template>

It looks great, isn’t it 🙂 If you enjoyed this tutorial, don’t forget to check out the other versions of this component in Alpine.js and Next.js.

If you’re looking for a pre-coded version of this component, we recommend having a look at our Tailwind CSS templates. Specifically, at this dark Next.js landing page template or if you’re seeking something more minimalist, this simple website template.

Â