Tutorials / / 4 min read
How to create a countdown with Tailwind CSS: JavaScript and Alpine.js
Build a countdown timer with Tailwind CSS, two ways: vanilla JavaScript or Alpine.js. Days, hours, minutes and seconds ticking down to your end date.
Let’s build a fun countdown timer, twice: first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.
What is a countdown timer?
A countdown timer displays the remaining time until a certain event or deadline. It’s a great way to showcase the urgency of an event or to give people a sense of the time left.
Use cases
- Product launches and sales: build anticipation and a sense of urgency for limited-time offers.
- Events: show attendees exactly how long until the conference, meetup or milestone.
- Online auctions: display the time remaining to bid, fostering competitiveness.
- Fitness and cooking: time workout intervals or keep a dish from overcooking.
The markup
id="countdown": the container that will hold the countdown timer; JavaScript targets this id and injects the timer.
Classes are omitted for brevity and clarity.
<div id="countdown" class="..."></div>The script
document.addEventListener("DOMContentLoaded", () => {: waits for the DOM, then grabs thecountdowncontainer.const endDate = new Date("2024-12-31T23:59:59").getTime();: the end date of the countdown.formatTime(time): breaks the remaining milliseconds into days, hours, minutes and seconds.createCountdownElement(value, label): returns the little block of markup for each unit.updateCountdown(): computes the remaining time withMath.max(0, endDate - now), then renders either the four units or the “Countdown has ended!” message.setInterval(updateCountdown, 1000);: refreshes the countdown every second.
This is the template for the countdown element:
<div class="countdown-item ...">
<div class="countdown-value ...">${value}</div>
<div class="countdown-label ...">${label}</div>
</div>The final code:
document.addEventListener("DOMContentLoaded", () => {
const countdownContainer = document.getElementById("countdown");
const endDate = new Date("2024-12-31T23:59:59").getTime();
function formatTime(time) {
const days = Math.floor(time / (1000 * 60 * 60 * 24));
const hours = Math.floor((time % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((time % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((time % (1000 * 60)) / 1000);
return {
days,
hours,
minutes,
seconds,
};
}
function createCountdownElement(value, label) {
return `
<div class="countdown-item ...">
<div class="countdown-value ...">${value}</div>
<div class="countdown-label ...">${label}</div>
</div>
`;
}
function updateCountdown() {
const now = new Date().getTime();
const remainingTime = Math.max(0, endDate - now);
const { days, hours, minutes, seconds } = formatTime(remainingTime);
countdownContainer.innerHTML =
remainingTime > 0
? createCountdownElement(days, "Days") +
createCountdownElement(hours, "Hours") +
createCountdownElement(minutes, "Minutes") +
createCountdownElement(seconds, "Seconds")
: `<div class="..."><div class="...">Countdown has ended!</div></div>`;
}
setInterval(updateCountdown, 1000);
});The Alpine.js version
Same component, no separate script: the end date, the remaining time and the formatting all live in x-data.
x-data="{ endDate: ..., remainingTime: 0, formatTime(time) { ... } }": stores the end date, the remaining time and the same formatting math as the vanilla version.x-init: starts asetIntervalthat recalculatesremainingTimeevery second, clamping it to 0 with$data.remainingTime = remainingTime > 0 ? remainingTime : 0;.x-if="remainingTime > 0": shows the running countdown.x-text="formatTime(remainingTime).days", and the same for.hours,.minutesand.seconds: display each unit.x-if="remainingTime <= 0": swaps in the “Countdown has ended!” message.
<div
x-data="{
endDate: new Date('2024-12-31T23:59:59').getTime(),
remainingTime: 0,
formatTime(time) {
const days = Math.floor(time / (1000 * 60 * 60 * 24));
const hours = Math.floor((time % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((time % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((time % (1000 * 60)) / 1000);
return { days, hours, minutes, seconds };
}
}"
x-init="() => {
setInterval(() => {
const now = new Date().getTime();
const remainingTime = endDate - now;
$data.remainingTime = remainingTime > 0 ? remainingTime : 0;
}, 1000);
}"
>
<template x-if="remainingTime > 0">
<div>
<div>
<div x-text="formatTime(remainingTime).days"></div>
<div>Days</div>
</div>
<div>
<div x-text="formatTime(remainingTime).hours"></div>
<div>Hours</div>
</div>
<div>
<div x-text="formatTime(remainingTime).minutes"></div>
<div>Minutes</div>
</div>
<div>
<div x-text="formatTime(remainingTime).seconds"></div>
<div>Seconds</div>
</div>
</div>
</template>
<template x-if="remainingTime <= 0">
<div>
<div>Countdown has ended!</div>
</div>
</template>
</div>You can try the Alpine version separately: live demo and source code.
Which one should you use?
If Alpine is already loaded, the whole timer fits in the markup with x-data and two templates. If not, the vanilla script is just as small and renders the same markup itself.
Conclusion
A date, a bit of math and a one-second interval: that’s a countdown timer in either flavor. Remember to make it responsive and as accessible as possible, and test it thoroughly so it works as expected.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza