UI Components Easy
Countdown Timer
A customizable countdown timer for events and deadlines. Features a sleek circular progress indicator and automatic cleanup.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React React Native Vue Svelte
Expo Snack
Code
:root {
--timer-bg: #0f172a;
--timer-card: rgba(30, 41, 59, 0.7);
--timer-accent: #38bdf8;
--timer-text: #f8fafc;
--timer-muted: #94a3b8;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
background: var(--timer-bg);
color: var(--timer-text);
font-family: "Inter", system-ui, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
.countdown-widget {
width: 100%;
max-width: 500px;
padding: clamp(1.5rem, 5vw, 2.5rem);
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
.countdown-header {
display: flex;
gap: 0.75rem;
margin-bottom: 2rem;
}
.date-input {
flex: 1;
background: var(--timer-card);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
padding: 0.75rem 1rem;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
min-width: 0;
}
.date-input:focus {
border-color: var(--timer-accent);
}
.start-btn {
background: var(--timer-accent);
color: var(--timer-bg);
border: none;
border-radius: 12px;
padding: 0.75rem 1.25rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.start-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.countdown-display {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
}
.timer-segment {
background: var(--timer-card);
padding: clamp(1rem, 3vw, 1.5rem) 0.25rem;
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.value {
font-size: clamp(1.5rem, 6vw, 2.5rem);
font-weight: 800;
color: var(--timer-accent);
line-height: 1;
margin-bottom: 0.25rem;
font-variant-numeric: tabular-nums;
}
.label {
font-size: clamp(0.6rem, 2vw, 0.75rem);
text-transform: uppercase;
color: var(--timer-muted);
letter-spacing: 0.1em;
font-weight: 600;
}
.status-message {
margin-top: 1.5rem;
text-align: center;
font-size: 0.813rem;
color: var(--timer-muted);
}
@media (max-width: 480px) {
.countdown-header {
flex-direction: column;
}
.countdown-display {
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
}let countdownInterval;
function startCountdown() {
const targetDateInput = document.getElementById("target-date");
const targetTime = new Date(targetDateInput.value).getTime();
if (isNaN(targetTime)) {
updateStatus("Please select a valid date and time.");
return;
}
if (targetTime <= new Date().getTime()) {
updateStatus("Target date must be in the future.");
return;
}
// Clear existing interval if any
if (countdownInterval) clearInterval(countdownInterval);
updateStatus("Countdown active...");
countdownInterval = setInterval(() => {
const now = new Date().getTime();
const distance = targetTime - now;
if (distance < 0) {
clearInterval(countdownInterval);
updateDisplay(0, 0, 0, 0);
updateStatus("Countdown finished!");
return;
}
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
updateDisplay(days, hours, minutes, seconds);
}, 1000);
}
function updateDisplay(days, hours, minutes, seconds) {
document.getElementById("days").textContent = String(days).padStart(2, "0");
document.getElementById("hours").textContent = String(hours).padStart(2, "0");
document.getElementById("minutes").textContent = String(minutes).padStart(2, "0");
document.getElementById("seconds").textContent = String(seconds).padStart(2, "0");
}
function updateStatus(msg) {
const statusEl = document.getElementById("countdown-status");
if (statusEl) statusEl.textContent = msg;
}
// Event Listeners
const startBtn = document.getElementById("start-timer");
if (startBtn) {
startBtn.addEventListener("click", startCountdown);
}
// Set default target to 24 hours from now
const defaultTarget = new Date();
defaultTarget.setHours(defaultTarget.getHours() + 24);
const targetInput = document.getElementById("target-date");
if (targetInput) {
// Format for datetime-local: YYYY-MM-DDTHH:MM
const tzoffset = new Date().getTimezoneOffset() * 60000;
const localISOTime = new Date(defaultTarget.getTime() - tzoffset).toISOString().slice(0, 16);
targetInput.value = localISOTime;
}<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Countdown Timer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="countdown-widget">
<div class="countdown-header">
<input type="datetime-local" id="target-date" class="date-input" />
<button id="start-timer" class="start-btn">Set Countdown</button>
</div>
<div class="countdown-display" id="timer-display">
<div class="timer-segment">
<span class="value" id="days">00</span>
<span class="label">Days</span>
</div>
<div class="timer-segment">
<span class="value" id="hours">00</span>
<span class="label">Hours</span>
</div>
<div class="timer-segment">
<span class="value" id="minutes">00</span>
<span class="label">Mins</span>
</div>
<div class="timer-segment">
<span class="value" id="seconds">00</span>
<span class="label">Secs</span>
</div>
</div>
<div id="countdown-status" class="status-message">Set a future date to begin</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useEffect } from "react";
function pad(n: number) {
return String(n).padStart(2, "0");
}
export default function CountdownTimerRC() {
const [targetDate, setTargetDate] = useState(() => {
const d = new Date();
d.setDate(d.getDate() + 7);
return d.toISOString().slice(0, 16);
});
const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
const [expired, setExpired] = useState(false);
useEffect(() => {
function calc() {
const diff = new Date(targetDate).getTime() - Date.now();
if (diff <= 0) {
setExpired(true);
return;
}
setExpired(false);
setTimeLeft({
days: Math.floor(diff / 86400000),
hours: Math.floor((diff % 86400000) / 3600000),
minutes: Math.floor((diff % 3600000) / 60000),
seconds: Math.floor((diff % 60000) / 1000),
});
}
calc();
const id = setInterval(calc, 1000);
return () => clearInterval(id);
}, [targetDate]);
const units = [
{ label: "Days", value: timeLeft.days },
{ label: "Hours", value: timeLeft.hours },
{ label: "Minutes", value: timeLeft.minutes },
{ label: "Seconds", value: timeLeft.seconds },
];
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div className="w-full max-w-lg">
<h2 className="text-center text-[#e6edf3] font-bold text-xl mb-6">Countdown Timer</h2>
<div className="mb-6 flex justify-center">
<input
type="datetime-local"
value={targetDate}
onChange={(e) => setTargetDate(e.target.value)}
className="bg-[#161b22] border border-[#30363d] text-[#e6edf3] rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-[#58a6ff]"
/>
</div>
{expired ? (
<p className="text-center text-[#f85149] font-semibold text-lg">Time's up!</p>
) : (
<div className="grid grid-cols-4 gap-3">
{units.map(({ label, value }) => (
<div
key={label}
className="bg-[#161b22] border border-[#30363d] rounded-xl p-4 text-center"
>
<p className="text-[40px] font-mono font-bold text-[#58a6ff] tabular-nums leading-none mb-1">
{pad(value)}
</p>
<p className="text-[11px] text-[#8b949e] uppercase tracking-wider">{label}</p>
</div>
))}
</div>
)}
</div>
</div>
);
}import React, { useState, useEffect, useRef } from "react";
import { View, Text, Animated, StyleSheet } from "react-native";
interface CountdownTimerProps {
targetDate: Date;
onComplete?: () => void;
}
interface TimeLeft {
days: number;
hours: number;
minutes: number;
seconds: number;
}
function getTimeLeft(target: Date): TimeLeft {
const diff = Math.max(0, target.getTime() - Date.now());
return {
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
minutes: Math.floor((diff / (1000 * 60)) % 60),
seconds: Math.floor((diff / 1000) % 60),
};
}
function FlipDigit({ value, label }: { value: number; label: string }) {
const scaleY = useRef(new Animated.Value(1)).current;
const prevValue = useRef(value);
useEffect(() => {
if (prevValue.current !== value) {
prevValue.current = value;
scaleY.setValue(0.6);
Animated.spring(scaleY, {
toValue: 1,
useNativeDriver: true,
tension: 300,
friction: 12,
}).start();
}
}, [value]);
const display = String(value).padStart(2, "0");
return (
<View style={styles.digitColumn}>
<Animated.View style={[styles.digitBox, { transform: [{ scaleY }] }]}>
<Text style={styles.digitText}>{display}</Text>
</Animated.View>
<Text style={styles.digitLabel}>{label}</Text>
</View>
);
}
function CountdownTimer({ targetDate, onComplete }: CountdownTimerProps) {
const [timeLeft, setTimeLeft] = useState<TimeLeft>(getTimeLeft(targetDate));
const completedRef = useRef(false);
useEffect(() => {
completedRef.current = false;
const interval = setInterval(() => {
const tl = getTimeLeft(targetDate);
setTimeLeft(tl);
if (
!completedRef.current &&
tl.days === 0 &&
tl.hours === 0 &&
tl.minutes === 0 &&
tl.seconds === 0
) {
completedRef.current = true;
onComplete?.();
}
}, 1000);
return () => clearInterval(interval);
}, [targetDate]);
return (
<View style={styles.timerRow}>
<FlipDigit value={timeLeft.days} label="Days" />
<Text style={styles.colon}>:</Text>
<FlipDigit value={timeLeft.hours} label="Hours" />
<Text style={styles.colon}>:</Text>
<FlipDigit value={timeLeft.minutes} label="Minutes" />
<Text style={styles.colon}>:</Text>
<FlipDigit value={timeLeft.seconds} label="Seconds" />
</View>
);
}
// --- Demo ---
export default function App() {
const threeDaysFromNow = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000);
const [fiveMinDone, setFiveMinDone] = useState(false);
return (
<View style={styles.container}>
<Text style={styles.title}>Countdown Timer</Text>
<Text style={styles.sectionLabel}>Event in 3 days</Text>
<CountdownTimer targetDate={threeDaysFromNow} />
<View style={styles.divider} />
<Text style={styles.sectionLabel}>5-minute countdown {fiveMinDone ? "(Done!)" : ""}</Text>
<CountdownTimer targetDate={fiveMinutesFromNow} onComplete={() => setFiveMinDone(true)} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f172a",
alignItems: "center",
justifyContent: "center",
padding: 24,
},
title: {
color: "#f8fafc",
fontSize: 24,
fontWeight: "700",
marginBottom: 40,
},
sectionLabel: {
color: "#94a3b8",
fontSize: 14,
marginBottom: 16,
fontWeight: "600",
},
timerRow: {
flexDirection: "row",
alignItems: "center",
},
digitColumn: {
alignItems: "center",
},
digitBox: {
backgroundColor: "#1e293b",
borderRadius: 12,
borderWidth: 1,
borderColor: "#334155",
paddingHorizontal: 16,
paddingVertical: 14,
minWidth: 64,
alignItems: "center",
},
digitText: {
color: "#f8fafc",
fontSize: 32,
fontWeight: "700",
fontVariant: ["tabular-nums"],
},
digitLabel: {
color: "#64748b",
fontSize: 11,
marginTop: 6,
textTransform: "uppercase",
fontWeight: "600",
letterSpacing: 1,
},
colon: {
color: "#64748b",
fontSize: 28,
fontWeight: "700",
marginHorizontal: 6,
marginBottom: 20,
},
divider: {
width: 200,
height: 1,
backgroundColor: "#1e293b",
marginVertical: 32,
},
});<script setup>
import { ref, computed, watch, onUnmounted } from "vue";
function pad(n) {
return String(n).padStart(2, "0");
}
const targetDate = ref(
(() => {
const d = new Date();
d.setDate(d.getDate() + 7);
return d.toISOString().slice(0, 16);
})()
);
const timeLeft = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 });
const expired = ref(false);
let intervalId;
function calc() {
const diff = new Date(targetDate.value).getTime() - Date.now();
if (diff <= 0) {
expired.value = true;
return;
}
expired.value = false;
timeLeft.value = {
days: Math.floor(diff / 86400000),
hours: Math.floor((diff % 86400000) / 3600000),
minutes: Math.floor((diff % 3600000) / 60000),
seconds: Math.floor((diff % 60000) / 1000),
};
}
watch(
targetDate,
() => {
calc();
clearInterval(intervalId);
intervalId = setInterval(calc, 1000);
},
{ immediate: true }
);
onUnmounted(() => clearInterval(intervalId));
const units = computed(() => [
{ label: "Days", value: timeLeft.value.days },
{ label: "Hours", value: timeLeft.value.hours },
{ label: "Minutes", value: timeLeft.value.minutes },
{ label: "Seconds", value: timeLeft.value.seconds },
]);
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="w-full max-w-lg">
<h2 class="text-center text-[#e6edf3] font-bold text-xl mb-6">Countdown Timer</h2>
<div class="mb-6 flex justify-center">
<input type="datetime-local" v-model="targetDate" class="bg-[#161b22] border border-[#30363d] text-[#e6edf3] rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-[#58a6ff]"/>
</div>
<p v-if="expired" class="text-center text-[#f85149] font-semibold text-lg">Time's up!</p>
<div v-else class="grid grid-cols-4 gap-3">
<div v-for="u in units" :key="u.label" class="bg-[#161b22] border border-[#30363d] rounded-xl p-4 text-center">
<p class="text-[40px] font-mono font-bold text-[#58a6ff] tabular-nums leading-none mb-1">{{ pad(u.value) }}</p>
<p class="text-[11px] text-[#8b949e] uppercase tracking-wider">{{ u.label }}</p>
</div>
</div>
</div>
</div>
</template><script>
import { onMount, onDestroy } from "svelte";
function pad(n) {
return String(n).padStart(2, "0");
}
let targetDate = (() => {
const d = new Date();
d.setDate(d.getDate() + 7);
return d.toISOString().slice(0, 16);
})();
let timeLeft = { days: 0, hours: 0, minutes: 0, seconds: 0 };
let expired = false;
let intervalId;
function calc() {
const diff = new Date(targetDate).getTime() - Date.now();
if (diff <= 0) {
expired = true;
return;
}
expired = false;
timeLeft = {
days: Math.floor(diff / 86400000),
hours: Math.floor((diff % 86400000) / 3600000),
minutes: Math.floor((diff % 3600000) / 60000),
seconds: Math.floor((diff % 60000) / 1000),
};
}
$: targetDate,
(() => {
calc();
clearInterval(intervalId);
intervalId = setInterval(calc, 1000);
})();
onDestroy(() => clearInterval(intervalId));
$: units = [
{ label: "Days", value: timeLeft.days },
{ label: "Hours", value: timeLeft.hours },
{ label: "Minutes", value: timeLeft.minutes },
{ label: "Seconds", value: timeLeft.seconds },
];
</script>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="w-full max-w-lg">
<h2 class="text-center text-[#e6edf3] font-bold text-xl mb-6">Countdown Timer</h2>
<div class="mb-6 flex justify-center">
<input type="datetime-local" bind:value={targetDate} class="bg-[#161b22] border border-[#30363d] text-[#e6edf3] rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-[#58a6ff]"/>
</div>
{#if expired}
<p class="text-center text-[#f85149] font-semibold text-lg">Time's up!</p>
{:else}
<div class="grid grid-cols-4 gap-3">
{#each units as { label, value }}
<div class="bg-[#161b22] border border-[#30363d] rounded-xl p-4 text-center">
<p class="text-[40px] font-mono font-bold text-[#58a6ff] tabular-nums leading-none mb-1">{pad(value)}</p>
<p class="text-[11px] text-[#8b949e] uppercase tracking-wider">{label}</p>
</div>
{/each}
</div>
{/if}
</div>
</div>Countdown Timer
An interactive countdown timer that allows users to set a distance target and see the remaining time in Days, Hours, Minutes, and Seconds.
Features
- Dynamic time calculation
- Interactive target date input
- Responsive grid layout
- Event handling for timer completion