| 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 :inline="true">
<DatePickerCalendar />
</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:
Calendar, Datepicker, and Daterangepicker are the same date-picker primitive (Zag ships one date-picker machine, not three) composed differently: this page only renders DatePickerCalendar directly with inline, skipping the input/trigger/popover parts entirely - there's nothing to type or open when the grid is always visible. See the Datepicker and Daterangepicker pages for the popover composition. 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, DatePickerCalendar } from "@/components/ui/date-picker";
<DatePickerRoot :inline="true">
<DatePickerCalendar />
</DatePickerRoot>