Calendar

An always-visible date grid, for picking a date without a popover.
September 2026
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
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.

Install the following dependencies:

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

Calendar, Datepicker, and Daterangepicker are the same date-picker.tsx 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. Copy and paste the following code into your project.

components/ui/date-picker.tsx
Expand

import { createContext, useContext, useId } from "react";
import { twMerge } from "tailwind-merge";
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, X } from "lucide-react";
import { useMachine, normalizeProps } from "@zag-js/react";
import * as datePickerMachine from "@zag-js/date-picker";
import { Portal } from "@/components/ui/portal";

const DatePickerContext = createContext<ReturnType<typeof datePickerMachine.connect> | null>(
  null
);

function useDatePickerContext() {
  const api = useContext(DatePickerContext);
  if (!api) throw new Error("DatePicker parts must be used within <DatePickerRoot>");
  return api;
}

function DatePickerRoot({
  children,
  ...rest
}: React.PropsWithChildren<Partial<datePickerMachine.Props>>) {
  const service = useMachine(datePickerMachine.machine, { id: useId(), ...rest });
  const api = datePickerMachine.connect(service, normalizeProps);

  return <DatePickerContext.Provider value={api}>{children}</DatePickerContext.Provider>;
}

// The input+trigger row used by the popover variants (Datepicker,
// Daterangepicker). A plain inline <DatePickerCalendar> (Calendar) has no
// use for this - there's no text value to type or popover to open.
function DatePickerControl({ className }: { className?: string }) {
  const api = useDatePickerContext();

  return (
    <div
      {...api.getControlProps()}
      className={twMerge([
        "flex items-center gap-2 border border-primary/30 bg-primary/10 px-3 py-2 w-fit",
        className,
      ])}
    >
      <input
        {...api.getInputProps({ index: 0 })}
        placeholder="Pick a date"
        className="outline-none bg-transparent w-28"
      />
      {api.selectionMode === "range" && (
        <>
          <span className="opacity-50">-</span>
          <input
            {...api.getInputProps({ index: 1 })}
            placeholder="Pick a date"
            className="outline-none bg-transparent w-28"
          />
        </>
      )}
      <button
        {...api.getClearTriggerProps()}
        className="opacity-70 hover:opacity-100 cursor-pointer"
      >
        <X className="size-3.5" />
      </button>
      <button
        {...api.getTriggerProps()}
        className="opacity-70 hover:opacity-100 cursor-pointer"
      >
        <CalendarIcon className="size-4" />
      </button>
    </div>
  );
}

// Wraps <DatePickerCalendar> in a floating popover. getContentProps() already
// manages its own `hidden` state, so unlike Dialog/Combobox this doesn't need
// a separate presence hook for an exit animation.
function DatePickerContent({
  children,
  className,
}: React.PropsWithChildren<{ className?: string }>) {
  const api = useDatePickerContext();
  const positionerProps = api.getPositionerProps();

  return (
    <Portal>
      <div {...positionerProps} style={{ ...positionerProps.style, zIndex: 70 }}>
        <div
          {...api.getContentProps()}
          className={twMerge(["border border-primary/30 bg-background outline-none", className])}
        >
          {children}
        </div>
      </div>
    </Portal>
  );
}

// The day grid, with a prev/next month header. This is the one part shared
// verbatim across Calendar (rendered inline, no popover), Datepicker (single
// selection, inside a popover), and Daterangepicker (range selection, inside
// a popover) - only the selectionMode/inline props passed to DatePickerRoot
// differ between the three.
//
// Scoped to the day view only: Zag's machine also supports drilling into
// month/year picker views (view/minView/maxView, getMonthsGrid, etc.), left
// out here to keep this placeholder implementation a manageable size.
function DatePickerCalendar({ className }: { className?: string }) {
  const api = useDatePickerContext();

  return (
    <div className={twMerge(["p-3", className])}>
      <div className="flex items-center justify-between mb-3">
        <button
          {...api.getPrevTriggerProps()}
          className="p-1 hover:bg-primary/10 disabled:opacity-30 disabled:pointer-events-none cursor-pointer"
        >
          <ChevronLeft className="size-4" />
        </button>
        <span className="font-medium">{api.visibleRangeText.formatted}</span>
        <button
          {...api.getNextTriggerProps()}
          className="p-1 hover:bg-primary/10 disabled:opacity-30 disabled:pointer-events-none cursor-pointer"
        >
          <ChevronRight className="size-4" />
        </button>
      </div>
      <table {...api.getTableProps({ view: "day" })} className="border-collapse">
        <thead {...api.getTableHeadProps()}>
          <tr {...api.getTableRowProps()}>
            {api.weekDays.map((day, i) => (
              <th key={i} scope="col" className="text-xs opacity-50 font-normal pb-2 size-8">
                {day.narrow}
              </th>
            ))}
          </tr>
        </thead>
        <tbody {...api.getTableBodyProps()}>
          {api.weeks.map((week, i) => (
            <tr key={i} {...api.getTableRowProps()}>
              {week.map((value, j) => (
                <td key={j} {...api.getDayTableCellProps({ value })} className="text-center p-0.5">
                  <div
                    {...api.getDayTableCellTriggerProps({ value })}
                    className={twMerge([
                      "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>
  );
}

export { DatePickerRoot, DatePickerControl, DatePickerContent, DatePickerCalendar };
 
Expand

Update the import paths to match your project setup.

Usage
Expand

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

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