<script setup lang="ts">
import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date";
import { TimeField, TimeFieldInput, TimeFieldSeparator } from "@/components/time-field";
import { Clock10Icon } from "lucide-vue-next";
import { ref } from "vue";
const date = ref(new CalendarDateTime(2026, 3, 16, 12, 30, 10));
function format(date: CalendarDateTime) {
return date.toDate(getLocalTimeZone()).toLocaleString();
}
</script>
<template>
<div class="flex flex-col gap-4">
<TimeField v-model="date" granularity="second" :hour-cycle="12" v-slot="{ hour, minute, second, dayPeriod }" class="w-fit">
<Clock10Icon class="size-4" />
<TimeFieldInput :segment="hour" />
<TimeFieldSeparator />
<TimeFieldInput :segment="minute" />
<TimeFieldSeparator />
<TimeFieldInput :segment="second" />
<TimeFieldInput :segment="dayPeriod" />
</TimeField>
<p class="text-sm text-muted-foreground">
{{ format(date) }}
</p>
</div>
</template>
Features
- 12/24-hour formats — Control display with
hour-cycleprop - Configurable granularity — Show hour only, hour+minute, or include seconds
- Day period — AM/PM display when using 12-hour format
- Composable — Flexible slot-based architecture with
TimeFieldInputandTimeFieldSeparator
Installation
Install from the Vuzeno registry with the shadcn-vue CLI:
bunx --bun shadcn-vue@latest add https://vuzeno.com/r/time-field.json
Dependencies
This component requires @internationalized/date for date/time values.
bun add @internationalized/date
Anatomy
- TimeField — Root component that provides segment data via scoped slots
- TimeFieldInput — Renders a single segment (hour, minute, second, or dayPeriod)
- TimeFieldSeparator — Renders a separator between segments (default:
:)
Examples
Combining with Calendar
<script setup lang="ts">
import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date";
import { TimeField, TimeFieldInput, TimeFieldSeparator } from "@/components/time-field";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { CalendarIcon, Clock10Icon } from "lucide-vue-next";
import { ref } from "vue";
const date = ref(new CalendarDateTime(2026, 3, 16, 12, 30, 0));
function format(date: CalendarDateTime) {
return date.toDate(getLocalTimeZone()).toLocaleString();
}
</script>
<template>
<Popover>
<PopoverTrigger>
<Button variant="outline">
<CalendarIcon class="size-4" />
{{ format(date) }}
</Button>
</PopoverTrigger>
<PopoverContent class="p-2 grid gap-4">
<Calendar v-model="date" class="mx-auto p-0" />
<TimeField v-model="date" granularity="minute" :hour-cycle="12" v-slot="{ hour, minute, dayPeriod }" class="mx-auto dark:border-accent dark:bg-background/50">
<Clock10Icon class="size-4" />
<TimeFieldInput :segment="hour" />
<TimeFieldSeparator />
<TimeFieldInput :segment="minute" />
<TimeFieldInput :segment="dayPeriod" />
</TimeField>
</PopoverContent>
</Popover>
</template>
Hour Cycle
<script setup lang="ts">
import { CalendarDateTime } from "@internationalized/date";
import { TimeField, TimeFieldInput, TimeFieldSeparator } from "@/components/time-field";
import { Clock10Icon } from "lucide-vue-next";
import { ref } from "vue";
const date = ref(new CalendarDateTime(2026, 3, 16, 16, 30, 0));
</script>
<template>
<div class="flex flex-col gap-4">
<TimeField v-model="date" granularity="minute" v-slot="{ hour, minute, dayPeriod }" class="w-fit">
<Clock10Icon class="size-4" />
<TimeFieldInput :segment="hour" />
<TimeFieldSeparator />
<TimeFieldInput :segment="minute" />
<TimeFieldInput :segment="dayPeriod" />
</TimeField>
<TimeField v-model="date" granularity="minute" :hour-cycle="24" v-slot="{ hour, minute }" class="w-fit">
<Clock10Icon class="size-4" />
<TimeFieldInput :segment="hour" />
<TimeFieldSeparator />
<TimeFieldInput :segment="minute" />
</TimeField>
</div>
</template>