Countdown
countdown ui Countdown timer that shows time remaining to a target date. Supports DD:HH:MM:SS, HH:MM:SS, MM:SS, and SS formats plus custom token formats, named slots for days/hours/minutes/seconds, a label, leading-zero padding, a custom separator, paused state, and on-finish/tick events.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/countdown.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/countdown.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/countdown.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/countdown.json Named registry:
npx shadcn-vue@latest add @uipkge/countdown Installs to: app/components/ui/countdown/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
target Target date/time. Accepts a Date, ISO string, or epoch ms number. | Date | string | number | — | required |
format Display format. Custom tokens: DD days, HH hours, MM minutes, SS seconds. | Format | string | 'DD:HH:MM:SS' | optional |
paused Pause the countdown. | boolean | false | optional |
label Optional label rendered above the countdown. | string | '' | optional |
pad Show leading zeros (e.g. 05 vs 5). | boolean | true | optional |
separator Separator between units. | string | ':' | optional |
class | HTMLAttributes['class'] | — | optional |
Files installed (2)
-
app/components/ui/countdown/Countdown.vue 5.3 kB
<script setup lang="ts"> import { computed, onBeforeUnmount, ref, watch } from 'vue' import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' type Format = 'DD:HH:MM:SS' | 'HH:MM:SS' | 'MM:SS' | 'SS' const props = withDefaults( defineProps<{ /** Target date/time. Accepts a Date, ISO string, or epoch ms number. */ target: Date | string | number /** Display format. Custom tokens: DD days, HH hours, MM minutes, SS seconds. */ format?: Format | string /** Pause the countdown. */ paused?: boolean /** Optional label rendered above the countdown. */ label?: string /** Show leading zeros (e.g. 05 vs 5). */ pad?: boolean /** Separator between units. */ separator?: string class?: HTMLAttributes['class'] }>(), { format: 'DD:HH:MM:SS', paused: false, label: '', pad: true, separator: ':', }, ) const emit = defineEmits<{ (e: 'finish'): void (e: 'tick', remaining: number): void }>() const now = ref(Date.now()) let timer: ReturnType<typeof setInterval> | null = null const finished = ref(false) const targetMs = computed(() => { if (props.target instanceof Date) return props.target.getTime() if (typeof props.target === 'number') return props.target return new Date(props.target).getTime() }) const remainingMs = computed(() => Math.max(0, targetMs.value - now.value)) const parts = computed(() => { const total = remainingMs.value const days = Math.floor(total / 86_400_000) const hours = Math.floor((total % 86_400_000) / 3_600_000) const minutes = Math.floor((total % 3_600_000) / 60_000) const seconds = Math.floor((total % 60_000) / 1000) return { days, hours, minutes, seconds } }) function pad2(n: number) { return props.pad ? String(n).padStart(2, '0') : String(n) } const display = computed(() => { const f = props.format const { days, hours, minutes, seconds } = parts.value const sep = props.separator if (f === 'DD:HH:MM:SS') return `${pad2(days)}${sep}${pad2(hours)}${sep}${pad2(minutes)}${sep}${pad2(seconds)}` if (f === 'HH:MM:SS') return `${pad2(days * 24 + hours)}${sep}${pad2(minutes)}${sep}${pad2(seconds)}` if (f === 'MM:SS') return `${pad2(days * 24 * 60 + hours * 60 + minutes)}${sep}${pad2(seconds)}` if (f === 'SS') return pad2(Math.floor(remainingMs.value / 1000)) // Custom token format: replace DD, HH, MM, SS tokens. return f .replace('DD', pad2(days)) .replace('HH', pad2(hours)) .replace('MM', pad2(minutes)) .replace('SS', pad2(seconds)) }) function startTimer() { stopTimer() if (props.paused) return timer = setInterval(() => { now.value = Date.now() emit('tick', remainingMs.value) if (remainingMs.value <= 0 && !finished.value) { finished.value = true stopTimer() emit('finish') } }, 1000) } function stopTimer() { if (timer) { clearInterval(timer) timer = null } } watch( () => props.paused, (paused) => { if (paused) stopTimer() else startTimer() }, ) watch( () => props.target, () => { finished.value = false now.value = Date.now() startTimer() }, ) onBeforeUnmount(stopTimer) // Kick off immediately. now.value = Date.now() startTimer() </script> <template> <div data-uipkge data-slot="countdown" :data-finished="finished" :data-paused="paused" :class="cn('inline-flex flex-col gap-1', props.class)" > <span v-if="label" data-slot="countdown-label" class="text-muted-foreground text-xs font-medium tracking-wide uppercase" > {{ label }} </span> <div data-slot="countdown-display" class="flex items-baseline gap-1 font-mono tabular-nums"> <slot name="default" :days="parts.days" :hours="parts.hours" :minutes="parts.minutes" :seconds="parts.seconds" :display="display" :finished="finished" > <slot name="days" :days="parts.days"> <span v-if="format.includes('DD')" data-slot="countdown-days" class="text-foreground text-2xl font-semibold"> {{ pad2(parts.days) }} </span> </slot> <span v-if="format.includes('DD') && format.includes('HH')" class="text-muted-foreground text-2xl">{{ separator }}</span> <slot name="hours" :hours="parts.hours"> <span v-if="format.includes('HH')" data-slot="countdown-hours" class="text-foreground text-2xl font-semibold"> {{ pad2(parts.hours) }} </span> </slot> <span v-if="format.includes('HH') && format.includes('MM')" class="text-muted-foreground text-2xl">{{ separator }}</span> <slot name="minutes" :minutes="parts.minutes"> <span v-if="format.includes('MM')" data-slot="countdown-minutes" class="text-foreground text-2xl font-semibold" > {{ pad2(parts.minutes) }} </span> </slot> <span v-if="format.includes('MM') && format.includes('SS')" class="text-muted-foreground text-2xl">{{ separator }}</span> <slot name="seconds" :seconds="parts.seconds"> <span v-if="format.includes('SS')" data-slot="countdown-seconds" class="text-foreground text-2xl font-semibold" > {{ pad2(parts.seconds) }} </span> </slot> </slot> </div> </div> </template> -
app/components/ui/countdown/index.ts 0.1 kB
export { default as Countdown } from './Countdown.vue'
Raw manifest: https://uipkge.dev/r/vue/countdown.json