Daterangepicker

An input that opens a calendar popover for picking a date range.
-
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

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.

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 selectionMode="range">
  <DatePickerControl />
  <DatePickerContent>
    <DatePickerCalendar />
  </DatePickerContent>
</DatePickerRoot>
 
Expand
Powered by synthetic caffeine · Deployed by Left4code · Signal traceable on GitHub.