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:
<span class="text-slate-900 dark:text-slate-200 font-bold text-4xl" v-text="isAnnual ? '29' : '35'"></span>
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.
Top comments (4)
Hi there, looks like this got triple-posted by your tooling, I've lowered the visibility of the previous two posts, but you may wish to remove them, or roll up the minor content variations into this one? 😉
Hi Phil, it's an article series, so the three posts are not the same. One is for HTML, another for Next.js and a third for Vue.
Could you please restore the visibility of all three? Thanks!
Yep, I noticed this and adjusted my comment. You might want to create a series from them 😁
Thank you, Phil 🙂