| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 1 | 2 | 3 |
<DatePickerRoot selectionMode="range">
<DatePickerControl />
<DatePickerContent>
<DatePickerCalendar />
</DatePickerContent>
</DatePickerRoot>
npx @left4code/cosmic-ui-cli@latest add date-picker
This writes the component and everything it imports, then installs the packages it needs. Run npx @left4code/cosmic-ui-cli@latest initfirst if you haven't already (it detects Vue automatically).
Install the following dependencies:
Daterangepicker, Calendar, and Datepicker are the same date-picker primitive (Zag ships one date-picker machine, not three) composed differently: this page is DatePickerRoot with selectionMode="range", which is what makes DatePickerControl render a second input for the end date and the grid highlight everything between the two picks. Only the day view is wired up here (no month/year picker view); the plain border is a placeholder for the Frame primitive. Vue SFCs can't export multiple components from one file, so DatePicker is a folder instead of a single file — copy all of it into your project.
import type { InjectionKey, ComputedRef } from "vue";
import type * as datePickerMachine from "@zag-js/date-picker";
type DatePickerApi = ComputedRef<ReturnType<typeof datePickerMachine.connect>>;
const DatePickerApiKey: InjectionKey<DatePickerApi> = Symbol("date-picker-api");
export { DatePickerApiKey };
export type { DatePickerApi };
<script setup lang="ts">
import { computed, provide, useAttrs, useId } from "vue";
import { useMachine, normalizeProps } from "@zag-js/vue";
import * as datePickerMachine from "@zag-js/date-picker";
import { DatePickerApiKey } from "./context";
defineOptions({ inheritAttrs: false });
const rawAttrs = useAttrs() as Partial<datePickerMachine.Props>;
const id = useId();
const service = useMachine(
datePickerMachine.machine,
computed(() => ({ id, ...rawAttrs }))
);
const api = computed(() => datePickerMachine.connect(service, normalizeProps));
provide(DatePickerApiKey, api);
</script>
<template>
<slot />
</template>
<script setup lang="ts">
import { inject } from "vue";
import { Calendar as CalendarIcon, X } from "@lucide/vue";
import { twMerge } from "tailwind-merge";
import { DatePickerApiKey } from "./context";
const { class: className } = defineProps<{ class?: string }>();
const injectedApi = inject(DatePickerApiKey);
if (!injectedApi) throw new Error("DatePicker parts must be used within <DatePickerRoot>");
const api = injectedApi;
</script>
<template>
<div
v-bind="api.getControlProps()"
:class="
twMerge(['flex items-center gap-2 border border-primary/30 bg-primary/10 px-3 py-2 w-fit', className])
"
>
<input
v-bind="api.getInputProps({ index: 0 })"
placeholder="Pick a date"
class="outline-none bg-transparent w-28"
/>
<template v-if="api.selectionMode === 'range'">
<span class="opacity-50">-</span>
<input
v-bind="api.getInputProps({ index: 1 })"
placeholder="Pick a date"
class="outline-none bg-transparent w-28"
/>
</template>
<button v-bind="api.getClearTriggerProps()" class="opacity-70 hover:opacity-100 cursor-pointer">
<X class="size-3.5" />
</button>
<button v-bind="api.getTriggerProps()" class="opacity-70 hover:opacity-100 cursor-pointer">
<CalendarIcon class="size-4" />
</button>
</div>
</template>
<script setup lang="ts">
import { inject } from "vue";
import { twMerge } from "tailwind-merge";
import Portal from "@/components/ui/portal.vue";
import { DatePickerApiKey } from "./context";
const { class: className } = defineProps<{ class?: string }>();
const injectedApi = inject(DatePickerApiKey);
if (!injectedApi) throw new Error("DatePicker parts must be used within <DatePickerRoot>");
const api = injectedApi;
</script>
<template>
<Portal>
<div
v-bind="api.getPositionerProps()"
:style="{ ...api.getPositionerProps().style, zIndex: 70 }"
>
<div
v-bind="api.getContentProps()"
:class="twMerge(['border border-primary/30 bg-background outline-none', className])"
>
<slot />
</div>
</div>
</Portal>
</template>
<script setup lang="ts">
import { inject } from "vue";
import { ChevronLeft, ChevronRight } from "@lucide/vue";
import { twMerge } from "tailwind-merge";
import { DatePickerApiKey } from "./context";
const { class: className } = defineProps<{ class?: string }>();
const injectedApi = inject(DatePickerApiKey);
if (!injectedApi) throw new Error("DatePicker parts must be used within <DatePickerRoot>");
const api = injectedApi;
</script>
<template>
<div :class="twMerge(['p-3', className])">
<div class="flex items-center justify-between mb-3">
<button
v-bind="api.getPrevTriggerProps()"
class="p-1 hover:bg-primary/10 disabled:opacity-30 disabled:pointer-events-none cursor-pointer"
>
<ChevronLeft class="size-4" />
</button>
<span class="font-medium">{{ api.visibleRangeText.formatted }}</span>
<button
v-bind="api.getNextTriggerProps()"
class="p-1 hover:bg-primary/10 disabled:opacity-30 disabled:pointer-events-none cursor-pointer"
>
<ChevronRight class="size-4" />
</button>
</div>
<table v-bind="api.getTableProps({ view: 'day' })" class="border-collapse">
<thead v-bind="api.getTableHeadProps()">
<tr v-bind="api.getTableRowProps()">
<th
v-for="(day, i) in api.weekDays"
:key="i"
scope="col"
class="text-xs opacity-50 font-normal pb-2 size-8"
>
{{ day.narrow }}
</th>
</tr>
</thead>
<tbody v-bind="api.getTableBodyProps()">
<tr v-for="(week, i) in api.weeks" :key="i" v-bind="api.getTableRowProps()">
<td
v-for="(value, j) in week"
:key="j"
v-bind="api.getDayTableCellProps({ value })"
class="text-center p-0.5"
>
<div
v-bind="api.getDayTableCellTriggerProps({ value })"
class="size-8 flex items-center justify-center cursor-pointer border border-transparent hover:border-primary/30 data-[selected]:bg-primary/20 data-[selected]:border-primary data-[today]:border-primary/50 data-[in-range]:bg-primary/10 data-[outside-range]:opacity-30 data-[unavailable]:opacity-20 data-[unavailable]:pointer-events-none data-[disabled]:opacity-20 data-[disabled]:pointer-events-none"
>
{{ value.day }}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
export { default as DatePickerRoot } from "./DatePickerRoot.vue";
export { default as DatePickerControl } from "./DatePickerControl.vue";
export { default as DatePickerContent } from "./DatePickerContent.vue";
export { default as DatePickerCalendar } from "./DatePickerCalendar.vue";
Update the import paths to match your project setup.
import {
DatePickerRoot,
DatePickerControl,
DatePickerContent,
DatePickerCalendar,
} from "@/components/ui/date-picker";
<DatePickerRoot selectionMode="range">
<DatePickerControl />
<DatePickerContent>
<DatePickerCalendar />
</DatePickerContent>
</DatePickerRoot>