Datepicker

An input that opens a calendar popover for picking a single date.
Installation
Expand

npx @left4code/cosmic-ui-cli@latest add date-picker
 
Expand

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:

pnpm
npm
yarn
bun
yarn add @zag-js/date-picker @zag-js/vue

Datepicker, Calendar, and Daterangepicker are the same date-picker primitive (Zag ships one date-picker machine, not three) composed differently: this page adds DatePickerControl (the text input and trigger button) and wraps the grid in DatePickerContent (a floating popover), instead of rendering DatePickerCalendar inline like the Calendar page does. 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.

components/ui/date-picker/context.ts
Expand

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 };
 
Expand
components/ui/date-picker/DatePickerRoot.vue
Expand

<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>
 
Expand
components/ui/date-picker/DatePickerControl.vue
Expand

<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>
 
Expand
components/ui/date-picker/DatePickerContent.vue
Expand

<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>
 
Expand
components/ui/date-picker/DatePickerCalendar.vue
Expand

<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>
 
Expand
components/ui/date-picker/index.ts
Expand

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";
 
Expand

Update the import paths to match your project setup.

Usage
Expand

import {
  DatePickerRoot,
  DatePickerControl,
  DatePickerContent,
  DatePickerCalendar,
} from "@/components/ui/date-picker";
 
Expand
Expand

<DatePickerRoot>
  <DatePickerControl />
  <DatePickerContent>
    <DatePickerCalendar />
  </DatePickerContent>
</DatePickerRoot>
 
Expand
Powered by synthetic caffeine · Deployed by Left4code · Signal traceable on GitHub.