Skip to main content
Nexus Design System
GitHub

DatePicker

DatePicker shows a calendar grid for picking one date, a range or several dates. Install react-day-picker in your app to use it. If your Next.js app uses Cache Components, render DatePicker inside <Suspense> so it can read today's date.

January 2025
'use client';

import { Card, CardContent } from '@/components/nxds/components/card/card';
import { DatePicker } from '@/components/nxds/components/date-picker/date-picker';

const REFERENCE_MONTH = new Date(2025, 0, 1);

export default function DatePickerDefault() {
  return (
    <Card className="nx:w-fit">
      <CardContent className="nx:p-0">
        <DatePicker
          mode="single"
          defaultMonth={REFERENCE_MONTH}
          selected={new Date(2025, 0, 15)}
        />
      </CardContent>
    </Card>
  );
}

This example also needs: Card.

Installation

npx nxds-cli@latest add date-picker

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import { DatePicker } from '@/components/nxds/components/date-picker/date-picker';

Examples

Range

Set mode="range" to pick a start and end date, with the days between highlighted.

January 2025
'use client';

import { Card, CardContent } from '@/components/nxds/components/card/card';
import { DatePicker } from '@/components/nxds/components/date-picker/date-picker';

const REFERENCE_MONTH = new Date(2025, 0, 1);

export default function DatePickerRange() {
  return (
    <Card className="nx:w-fit">
      <CardContent className="nx:p-0">
        <DatePicker
          mode="range"
          defaultMonth={REFERENCE_MONTH}
          selected={{ from: new Date(2025, 0, 8), to: new Date(2025, 0, 16) }}
        />
      </CardContent>
    </Card>
  );
}

Multiple

Set mode="multiple" to pick several separate dates.

January 2025
'use client';

import { Card, CardContent } from '@/components/nxds/components/card/card';
import { DatePicker } from '@/components/nxds/components/date-picker/date-picker';

const REFERENCE_MONTH = new Date(2025, 0, 1);

export default function DatePickerMultiple() {
  return (
    <Card className="nx:w-fit">
      <CardContent className="nx:p-0">
        <DatePicker
          mode="multiple"
          defaultMonth={REFERENCE_MONTH}
          selected={[
            new Date(2025, 0, 6),
            new Date(2025, 0, 13),
            new Date(2025, 0, 20),
          ]}
        />
      </CardContent>
    </Card>
  );
}

Dropdown caption

Set captionLayout="dropdown" to jump between months and years from dropdowns.

January 2025
'use client';

import { Card, CardContent } from '@/components/nxds/components/card/card';
import { DatePicker } from '@/components/nxds/components/date-picker/date-picker';

const REFERENCE_MONTH = new Date(2025, 0, 1);

export default function DatePickerDropdownCaption() {
  return (
    <Card className="nx:w-fit">
      <CardContent className="nx:p-0">
        <DatePicker
          mode="single"
          captionLayout="dropdown"
          defaultMonth={REFERENCE_MONTH}
          selected={new Date(2025, 0, 15)}
        />
      </CardContent>
    </Card>
  );
}

Disabled days

Pass dates to disabled so they show dimmed and cannot be picked.

January 2025
'use client';

import { Card, CardContent } from '@/components/nxds/components/card/card';
import { DatePicker } from '@/components/nxds/components/date-picker/date-picker';

const REFERENCE_MONTH = new Date(2025, 0, 1);

export default function DatePickerDisabledDays() {
  return (
    <Card className="nx:w-fit">
      <CardContent className="nx:p-0">
        <DatePicker
          mode="single"
          defaultMonth={REFERENCE_MONTH}
          disabled={[new Date(2025, 0, 10), new Date(2025, 0, 11)]}
        />
      </CardContent>
    </Card>
  );
}

With presets

Add preset buttons in a Card footer to jump to common dates.

January 2025
'use client';

import * as React from 'react';

import { Button } from '@/components/nxds/components/button/button';
import { Card, CardContent, CardFooter } from '@/components/nxds/components/card/card';
import { DatePicker } from '@/components/nxds/components/date-picker/date-picker';

const REFERENCE_MONTH = new Date(2025, 0, 1);

const REFERENCE_TODAY = new Date(2025, 0, 15);

function addDays(date: Date, days: number) {
  const next = new Date(date.getTime());
  next.setDate(next.getDate() + days);
  return next;
}

const PRESETS = [
  { label: 'Today', days: 0 },
  { label: 'Tomorrow', days: 1 },
  { label: 'In 3 days', days: 3 },
  { label: 'In a week', days: 7 },
  { label: 'In 2 weeks', days: 14 },
];

export default function DatePickerWithPresets() {
  const [date, setDate] = React.useState<Date | undefined>(REFERENCE_TODAY);
  const [month, setMonth] = React.useState<Date>(REFERENCE_MONTH);

  const applyPreset = (days: number) => {
    const next = addDays(REFERENCE_TODAY, days);
    setDate(next);
    setMonth(new Date(next.getFullYear(), next.getMonth(), 1));
  };

  return (
    <Card className="nx:w-fit">
      <CardContent className="nx:p-0">
        <DatePicker
          mode="single"
          selected={date}
          onSelect={setDate}
          month={month}
          onMonthChange={setMonth}
          fixedWeeks
        />
      </CardContent>
      <CardFooter className="nx:grid nx:grid-cols-2 nx:gap-2 nx:border-t-default nx:border-border-default nx:p-4 nx:[&>*:last-child]:col-span-2">
        {PRESETS.map((preset) => (
          <Button
            key={preset.label}
            variant="outline"
            size="sm"
            onClick={() => applyPreset(preset.days)}
          >
            {preset.label}
          </Button>
        ))}
      </CardFooter>
    </Card>
  );
}

Props

DatePicker

DatePicker props
PropTypeDefaultDescription
buttonVariant"link" | "default" | "error" | "destructive" | "outline" | "dashed" | "secondary" | "ghost" | null"ghost"

Button variant for the previous and next month arrows.

cellSizeDatePickerCellSize"default"

Day-cell size: default is 32px, large 48px, xlarge 56px. For another size, set --cell-size through className.

renderDayContent((props: DatePickerDayContentProps) => ReactNode)No default documented

Custom content inside each day button. The button keeps its keyboard behavior.

DatePickerDayButton

DatePickerDayButton adds no props of its own; it accepts the props of the element or primitive it wraps.

DatePicker shows a calendar grid for picking one date, a range or several dates. Install @internationalized/date in your app to create its dates.

<script setup lang="ts">
import { CalendarDate } from '@internationalized/date';
import { ref } from 'vue';

import { Card, CardContent } from '@/components/card';
import { DatePicker } from '@/components/date-picker';

const month = new CalendarDate(2025, 1, 1);
const date = ref(new CalendarDate(2025, 1, 15));
</script>

<template>
  <Card class="nx:w-fit">
    <CardContent class="nx:p-0">
      <DatePicker v-model="date" :default-placeholder="month" />
    </CardContent>
  </Card>
</template>

This example also needs: Card.

Installation

  1. Install these components first:

    Button, ButtonGroup, Spinner.

  2. Install the following dependencies:

    npm install @internationalized/date@^3.5.0
    
  3. Copy and paste the following code into your project.

    components/date-picker/DatePicker.vue
    <script setup lang="ts">
    import {
      type DateValue,
      endOfYear,
      getLocalTimeZone,
      startOfYear,
      today as todayIn,
    } from '@internationalized/date';
    import { reactiveOmit } from '@vueuse/core';
    import {
      CalendarGrid,
      CalendarGridBody,
      CalendarGridHead,
      CalendarGridRow,
      CalendarHeadCell,
      CalendarNext,
      CalendarPrev,
      CalendarRoot,
      type CalendarRootProps,
      type DateRange,
      RangeCalendarGrid,
      RangeCalendarGridBody,
      RangeCalendarGridHead,
      RangeCalendarGridRow,
      RangeCalendarHeadCell,
      RangeCalendarNext,
      RangeCalendarPrev,
      RangeCalendarRoot,
      useForwardPropsEmits,
    } from 'reka-ui';
    import { computed, type HTMLAttributes, provide, toRef, type VNode } from 'vue';
    
    import { IconChevronLeft, IconChevronRight } from '../../lib/icons';
    import { cn } from '../../lib/utils';
    import { type ButtonVariants, buttonVariants } from '../button';
    
    import DatePickerCaption from './DatePickerCaption.vue';
    import DatePickerCell from './DatePickerCell.vue';
    
    import {
      type DatePickerCellSize,
      type DatePickerDayContentProps,
      DatePickerKey,
    } from '.';
    
    export interface DatePickerProps extends Omit<
      CalendarRootProps,
      'modelValue' | 'defaultValue' | 'multiple' | 'weekdayFormat'
    > {
      /** Picks one date, several dates, or a start and end date. */
      mode?: 'single' | 'multiple' | 'range';
      /** The picked date, dates or range, by mode. Bind it with `v-model`. */
      modelValue?: DateValue | DateValue[] | DateRange | null;
      /** The starting date, dates or range without `v-model`. */
      defaultValue?: DateValue | DateValue[] | DateRange;
      /** Static month label or month and year dropdowns. */
      captionLayout?: 'label' | 'dropdown';
      /** Shows days from the previous and next months. */
      showOutsideDays?: boolean;
      /** The date marked as today. Defaults to the current date. */
      today?: DateValue;
      /**
       * Button variant for the previous and next month arrows.
       */
      buttonVariant?: ButtonVariants['variant'];
      /**
       * Day-cell size: `default` is 32px, `large` 48px, `xlarge` 56px. For another
       * size, set `--cell-size` through `className`.
       */
      cellSize?: DatePickerCellSize;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<DatePickerProps>(), {
      mode: 'single',
      captionLayout: 'label',
      showOutsideDays: true,
      buttonVariant: 'ghost',
      cellSize: 'default',
    });
    const emits = defineEmits<{
      'update:modelValue': [value: DateValue | DateValue[] | DateRange | undefined];
      'update:placeholder': [date: DateValue];
    }>();
    const slots = defineSlots<{
      /**
       * Custom content inside each day button. The button keeps its keyboard
       * behavior.
       */
      'day-content'?: (props: DatePickerDayContentProps) => VNode[];
      /** Content under the grid that screen readers announce, such as the picked date. */
      footer?: () => VNode[];
    }>();
    
    const delegated = reactiveOmit(
      props,
      'class',
      'mode',
      'captionLayout',
      'showOutsideDays',
      'today',
      'buttonVariant',
      'cellSize'
    );
    const forwarded = useForwardPropsEmits(delegated, emits);
    // `mode` picks the root and the value shape together, so each root gets its own
    // shape; the type system can't see that across the two roots.
    const rootProps = computed(() => forwarded.value as Record<string, unknown>);
    
    const today = computed(() => props.today ?? todayIn(getLocalTimeZone()));
    
    // The year dropdown runs from a century back to the end of this year, so
    // navigation stops there too unless the app sets its own bounds.
    const minValue = computed(
      () =>
        props.minValue ??
        (props.captionLayout === 'dropdown'
          ? startOfYear(today.value.subtract({ years: 100 }))
          : undefined)
    );
    const maxValue = computed(
      () =>
        props.maxValue ??
        (props.captionLayout === 'dropdown' ? endOfYear(today.value) : undefined)
    );
    
    const cellSizeClassNames: Record<DatePickerCellSize, string> = {
      default:
        'nx:[--cell-size:var(--nx-spacing-8)] nx:[--today-marker-size:var(--nx-spacing-6)]',
      large:
        'nx:[--cell-size:var(--nx-spacing-12)] nx:[--today-marker-size:var(--nx-spacing-10)]',
      xlarge:
        'nx:[--cell-size:var(--nx-spacing-14)] nx:[--today-marker-size:var(--nx-spacing-12)]',
    };
    
    const calendarParts = {
      Root: CalendarRoot,
      Prev: CalendarPrev,
      Next: CalendarNext,
      Grid: CalendarGrid,
      GridHead: CalendarGridHead,
      GridBody: CalendarGridBody,
      GridRow: CalendarGridRow,
      HeadCell: CalendarHeadCell,
    };
    const rangeParts = {
      Root: RangeCalendarRoot,
      Prev: RangeCalendarPrev,
      Next: RangeCalendarNext,
      Grid: RangeCalendarGrid,
      GridHead: RangeCalendarGridHead,
      GridBody: RangeCalendarGridBody,
      GridRow: RangeCalendarGridRow,
      HeadCell: RangeCalendarHeadCell,
    };
    const parts = computed(() =>
      props.mode === 'range' ? rangeParts : calendarParts
    );
    
    provide(DatePickerKey, {
      mode: toRef(props, 'mode'),
      today,
      showOutsideDays: toRef(props, 'showOutsideDays'),
      captionLayout: toRef(props, 'captionLayout'),
      dayContent: () => slots['day-content'],
    });
    
    const navButtonClass = computed(() =>
      cn(
        buttonVariants({ variant: props.buttonVariant, size: 'icon' }),
        'nx:size-(--cell-size) nx:p-0 nx:select-none nx:disabled:text-disabled-foreground nx:aria-disabled:text-disabled-foreground'
      )
    );
    </script>
    
    <template>
      <component
        :is="parts.Root"
        v-slot="{ grid, weekDays }"
        v-bind="rootProps"
        :min-value="minValue"
        :max-value="maxValue"
        :multiple="mode === 'multiple' ? true : undefined"
        weekday-format="short"
        data-slot="date-picker"
        :data-mode="mode"
        :data-cell-size="cellSize"
        :class="
          cn(
            'nx:w-fit',
            'nx:group/date-picker nx:bg-background nx:p-3',
            cellSizeClassNames[cellSize],
            'nx:in-data-[slot=card-content]:bg-transparent nx:in-data-[slot=popover-content]:bg-transparent',
            props.class
          )
        "
      >
        <div class="nx:relative nx:flex nx:flex-row nx:gap-4">
          <nav
            aria-label="Navigation bar"
            class="nx:absolute nx:inset-x-0 nx:top-0 nx:flex nx:w-full nx:items-center nx:justify-between nx:gap-1"
          >
            <component
              :is="parts.Prev"
              aria-label="Go to the Previous Month"
              :class="navButtonClass"
            >
              <IconChevronLeft class="nx:size-4 nx:text-muted-foreground" />
            </component>
            <component
              :is="parts.Next"
              aria-label="Go to the Next Month"
              :class="navButtonClass"
            >
              <IconChevronRight class="nx:size-4 nx:text-muted-foreground" />
            </component>
          </nav>
          <div
            v-for="month in grid"
            :key="month.value.toString()"
            class="nx:flex nx:w-full nx:flex-col nx:gap-4"
          >
            <DatePickerCaption :month="month.value" />
            <component :is="parts.Grid" class="nx:w-full nx:border-collapse">
              <component :is="parts.GridHead">
                <component :is="parts.GridRow" class="nx:flex">
                  <component
                    :is="parts.HeadCell"
                    v-for="day in weekDays"
                    :key="day"
                    class="nx:flex-1 nx:rounded-md nx:typography-body-default nx:text-muted-foreground-subtle nx:select-none"
                  >
                    {{ day.slice(0, 2) }}
                  </component>
                </component>
              </component>
              <component :is="parts.GridBody">
                <component
                  :is="parts.GridRow"
                  v-for="(week, index) in month.rows"
                  :key="index"
                  class="nx:mt-2 nx:flex nx:w-full"
                >
                  <DatePickerCell
                    v-for="day in week"
                    :key="day.toString()"
                    :day="day"
                    :month="month.value"
                  />
                </component>
              </component>
            </component>
          </div>
        </div>
        <div v-if="slots.footer" role="status" aria-live="polite">
          <slot name="footer" />
        </div>
      </component>
    </template>
    
    components/date-picker/DatePickerCaption.vue
    <script setup lang="ts">
    import { type DateValue, getLocalTimeZone } from '@internationalized/date';
    import {
      injectCalendarRootContext,
      injectRangeCalendarRootContext,
    } from 'reka-ui';
    import { computed } from 'vue';
    
    import { IconChevronDown } from '../../lib/icons';
    import { cn } from '../../lib/utils';
    
    import { injectDatePicker } from '.';
    
    const props = defineProps<{ month: DateValue }>();
    
    const datePicker = injectDatePicker();
    const root =
      injectRangeCalendarRootContext(null) ?? injectCalendarRootContext(null);
    
    const locale = computed(() => root?.locale.value);
    const date = computed(() => props.month.toDate(getLocalTimeZone()));
    
    const label = computed(() =>
      date.value.toLocaleString(locale.value, { month: 'long', year: 'numeric' })
    );
    const months = computed(() =>
      Array.from({ length: 12 }, (_, index) => ({
        value: index + 1,
        label: new Date(2000, index, 1).toLocaleString(locale.value, {
          month: 'short',
        }),
      }))
    );
    // The years run between the bounds the DatePicker sets in dropdown layout.
    const years = computed(() => {
      const first = root?.minValue.value?.year ?? datePicker.today.value.year;
      const last = root?.maxValue.value?.year ?? datePicker.today.value.year;
      return Array.from({ length: last - first + 1 }, (_, index) => first + index);
    });
    
    function showMonth(event: Event) {
      const month = Number((event.target as HTMLSelectElement).value);
      root?.onPlaceholderChange(props.month.set({ month }));
    }
    
    function showYear(event: Event) {
      const year = Number((event.target as HTMLSelectElement).value);
      root?.onPlaceholderChange(props.month.set({ year }));
    }
    
    const dropdownRootClass =
      'nx:relative nx:rounded-md nx:border-default nx:border-border-default nx:shadow-xs nx:has-focus:border-border-focus';
    const dropdownClass = 'nx:absolute nx:inset-0 nx:bg-popover nx:opacity-0';
    const dropdownLabelClass = cn(
      'nx:typography-label-default nx:select-none',
      'nx:flex nx:h-8 nx:items-center nx:gap-1 nx:rounded-md nx:pr-1 nx:pl-2 nx:[&>svg]:size-3.5 nx:[&>svg]:text-muted-foreground'
    );
    </script>
    
    <template>
      <div
        class="nx:flex nx:h-(--cell-size) nx:w-full nx:items-center nx:justify-center nx:px-(--cell-size)"
      >
        <div
          v-if="datePicker.captionLayout.value === 'dropdown'"
          class="nx:flex nx:h-(--cell-size) nx:w-full nx:items-center nx:justify-center nx:gap-1.5 nx:typography-label-default"
        >
          <span :class="dropdownRootClass">
            <select
              aria-label="Choose the Month"
              :class="dropdownClass"
              :value="month.month"
              @change="showMonth"
            >
              <option
                v-for="option in months"
                :key="option.value"
                :value="option.value"
              >
                {{ option.label }}
              </option>
            </select>
            <span :class="dropdownLabelClass" aria-hidden="true">
              {{ months[month.month - 1]?.label }}
              <IconChevronDown class="nx:size-4" />
            </span>
          </span>
          <span :class="dropdownRootClass">
            <select
              aria-label="Choose the Year"
              :class="dropdownClass"
              :value="month.year"
              @change="showYear"
            >
              <option v-for="year in years" :key="year" :value="year">
                {{ year }}
              </option>
            </select>
            <span :class="dropdownLabelClass" aria-hidden="true">
              {{ month.year }}
              <IconChevronDown class="nx:size-4" />
            </span>
          </span>
        </div>
        <span v-else class="nx:typography-label-default nx:select-none">
          {{ label }}
        </span>
      </div>
    </template>
    
    components/date-picker/DatePickerCell.vue
    <script setup lang="ts">
    import {
      type DateValue,
      isSameDay,
      isSameMonth,
    } from '@internationalized/date';
    import {
      CalendarCell,
      injectCalendarRootContext,
      injectRangeCalendarRootContext,
      RangeCalendarCell,
    } from 'reka-ui';
    import { computed, ref } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import DatePickerDayButton from './DatePickerDayButton.vue';
    
    import { type DatePickerDayModifiers, injectDatePicker } from '.';
    
    const props = defineProps<{ day: DateValue; month: DateValue }>();
    
    const datePicker = injectDatePicker();
    const calendar = injectCalendarRootContext(null);
    const range = injectRangeCalendarRootContext(null);
    const focused = ref(false);
    
    function selection(day: DateValue) {
      if (!range) {
        return {
          selected: calendar?.isDateSelected(day) ?? false,
          rangeStart: false,
          rangeMiddle: false,
          rangeEnd: false,
          preview: false,
        };
      }
      const inRange = range.isSelected(day);
      const rangeStart = range.isSelectionStart(day);
      const rangeEnd = range.isSelectionEnd(day);
      const highlighted = range.highlightedRange.value;
      return {
        selected: inRange,
        rangeStart,
        rangeMiddle: inRange && !rangeStart && !rangeEnd,
        rangeEnd,
        preview:
          !inRange &&
          !!highlighted &&
          day.compare(highlighted.start) >= 0 &&
          day.compare(highlighted.end) <= 0,
      };
    }
    
    // Reka ignores a first press that would span a blocked day. Marking it as
    // already pressed makes Reka restart the range there, as react-day-picker does.
    // Reka's highlightedRange follows the hovered day, so it is pointed at the
    // pressed day first.
    function restartBlockedRange() {
      if (!range?.startValue.value || range.endValue.value) return;
      if (modifiers.value.disabled || modifiers.value.unavailable) return;
      range.focusedValue.value = props.day.copy();
      if (range.highlightedRange.value) return;
      range.lastPressedDateValue.value = props.day.copy();
    }
    
    const modifiers = computed<DatePickerDayModifiers>(() => {
      const root = range ?? calendar;
      return {
        ...selection(props.day),
        today: isSameDay(props.day, datePicker.today.value),
        outside: !isSameMonth(props.day, props.month),
        disabled: root?.isDateDisabled(props.day) ?? false,
        unavailable: root?.isDateUnavailable?.(props.day) ?? false,
      };
    });
    </script>
    
    <template>
      <component
        :is="range ? RangeCalendarCell : CalendarCell"
        :date="day"
        :data-selected="modifiers.selected || undefined"
        :data-focused="focused || undefined"
        :class="
          cn(
            'nx:group/day nx:relative nx:flex nx:aspect-square nx:h-full nx:w-full nx:items-center nx:justify-center nx:p-0 nx:text-center nx:select-none nx:[&:last-child[data-selected=true]_button]:rounded-r-md',
            'nx:[&:first-child[data-selected=true]_button]:rounded-l-md',
            modifiers.rangeStart && 'nx:rounded-l-md nx:bg-primary-subtle',
            modifiers.rangeMiddle && 'nx:rounded-none nx:bg-primary-subtle',
            modifiers.rangeEnd && 'nx:rounded-r-md nx:bg-primary-subtle',
            modifiers.outside &&
              'nx:text-muted-foreground-subtle nx:aria-selected:text-muted-foreground-subtle',
            modifiers.disabled && 'nx:text-disabled-foreground',
            modifiers.unavailable && 'nx:text-muted-foreground nx:line-through',
            modifiers.preview && 'nx:bg-container-hover'
          )
        "
        @click.capture="restartBlockedRange"
        @keydown.enter.capture="restartBlockedRange"
        @keydown.space.capture="restartBlockedRange"
        @focusin="focused = true"
        @focusout="focused = false"
      >
        <DatePickerDayButton
          v-if="!modifiers.outside || datePicker.showOutsideDays.value"
          :day="day"
          :month="month"
          :modifiers="modifiers"
        />
      </component>
    </template>
    
    components/date-picker/DatePickerDayButton.vue
    <script setup lang="ts">
    import {
      CalendarCellTrigger,
      type CalendarCellTriggerProps,
      RangeCalendarCellTrigger,
    } from 'reka-ui';
    import { computed, type FunctionalComponent, type HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import { buttonVariants } from '../button';
    
    import { type DatePickerDayModifiers, injectDatePicker } from '.';
    
    export interface DatePickerDayButtonProps extends Omit<
      CalendarCellTriggerProps,
      'as' | 'asChild'
    > {
      /** The day's state, which the button shows through its `data-*` attributes. */
      modifiers: DatePickerDayModifiers;
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<DatePickerDayButtonProps>();
    
    const datePicker = injectDatePicker();
    
    const isSelected = computed(
      () =>
        props.modifiers.selected ||
        props.modifiers.rangeStart ||
        props.modifiers.rangeMiddle ||
        props.modifiers.rangeEnd
    );
    const isToday = computed(
      () => props.modifiers.today && !isSelected.value && !props.modifiers.disabled
    );
    const isOutside = computed(
      () =>
        props.modifiers.outside &&
        !isSelected.value &&
        !isToday.value &&
        !props.modifiers.disabled
    );
    
    const DayContent: FunctionalComponent = () =>
      datePicker.dayContent()?.({ date: props.day, modifiers: props.modifiers });
    </script>
    
    <template>
      <!-- These attributes win over Reka's own, which mark today by the clock and not the `today` prop. -->
      <component
        :is="
          datePicker.mode.value === 'range'
            ? RangeCalendarCellTrigger
            : CalendarCellTrigger
        "
        :day="day"
        :month="month"
        as="button"
        type="button"
        data-slot="button"
        data-variant="ghost"
        data-size="icon"
        data-icon-only="true"
        :disabled="modifiers.disabled || undefined"
        :data-day="day.toString()"
        :data-selected-single="
          modifiers.selected &&
          !modifiers.rangeStart &&
          !modifiers.rangeEnd &&
          !modifiers.rangeMiddle
        "
        :data-range-start="modifiers.rangeStart"
        :data-range-end="modifiers.rangeEnd"
        :data-range-middle="modifiers.rangeMiddle"
        :data-today="isToday"
        :data-outside="isOutside || undefined"
        :class="
          cn(
            buttonVariants({ variant: 'ghost', size: 'icon' }),
            'nx:flex nx:aspect-square nx:size-(--cell-size) nx:min-w-(--cell-size) nx:typography-body-default',
            'nx:disabled:text-disabled-foreground nx:aria-disabled:text-disabled-foreground',
            isOutside && 'nx:text-muted-foreground-subtle',
            'nx:group-data-[focused=true]/day:relative nx:group-data-[focused=true]/day:z-10 nx:group-data-[focused=true]/day:outline-2 nx:group-data-[focused=true]/day:outline-focus-default',
            'nx:data-[today=true]:relative nx:data-[today=true]:bg-transparent nx:data-[today=true]:typography-label-default nx:data-[today=true]:text-error-foreground nx:data-[today=true]:hover:bg-transparent nx:data-[today=true]:hover:text-error-foreground',
            'nx:data-[today=true]:before:absolute nx:data-[today=true]:before:top-1/2 nx:data-[today=true]:before:left-1/2 nx:data-[today=true]:before:size-(--today-marker-size) nx:data-[today=true]:before:-translate-x-1/2 nx:data-[today=true]:before:-translate-y-1/2 nx:data-[today=true]:before:rounded-full nx:data-[today=true]:before:bg-error-background nx:data-[today=true]:hover:before:bg-error-background-hover',
            `nx:data-[today=true]:before:content-['']`,
            'nx:data-[selected-single=true]:bg-primary-background nx:data-[selected-single=true]:text-primary-foreground',
            'nx:data-[range-start=true]:rounded-md nx:data-[range-start=true]:bg-primary-background nx:data-[range-start=true]:text-primary-foreground',
            'nx:data-[range-end=true]:rounded-md nx:data-[range-end=true]:bg-primary-background nx:data-[range-end=true]:text-primary-foreground',
            'nx:data-[range-middle=true]:rounded-none nx:data-[range-middle=true]:bg-primary-subtle nx:data-[range-middle=true]:text-primary-subtle-foreground',
            props.class
          )
        "
      >
        <span
          data-slot="date-picker-day-content"
          class="nx:relative nx:z-10 nx:flex nx:flex-col nx:items-center nx:justify-center nx:gap-1"
        >
          <DayContent v-if="datePicker.dayContent()" />
          <template v-else>{{ day.day }}</template>
        </span>
      </component>
    </template>
    
    components/date-picker/index.ts
    import type { DateValue } from '@internationalized/date';
    import {
      type ComputedRef,
      inject,
      type InjectionKey,
      type Ref,
      type VNode,
    } from 'vue';
    
    export { default as DatePicker, type DatePickerProps } from './DatePicker.vue';
    export {
      default as DatePickerDayButton,
      type DatePickerDayButtonProps,
    } from './DatePickerDayButton.vue';
    
    export type DatePickerCellSize = 'default' | 'large' | 'xlarge';
    
    /** A day's state, as each day button and the `day-content` slot see it. */
    export interface DatePickerDayModifiers {
      selected: boolean;
      rangeStart: boolean;
      rangeMiddle: boolean;
      rangeEnd: boolean;
      /** Between the range start and the hovered day while a range is half-picked. */
      preview: boolean;
      today: boolean;
      outside: boolean;
      disabled: boolean;
      unavailable: boolean;
    }
    
    export interface DatePickerDayContentProps {
      date: DateValue;
      modifiers: DatePickerDayModifiers;
    }
    
    export const DatePickerKey: InjectionKey<{
      mode: Ref<'single' | 'multiple' | 'range'>;
      today: ComputedRef<DateValue>;
      showOutsideDays: Ref<boolean>;
      captionLayout: Ref<'label' | 'dropdown'>;
      /** The `day-content` slot. Call it while rendering. */
      dayContent: () => ((props: DatePickerDayContentProps) => VNode[]) | undefined;
    }> = Symbol('date-picker');
    
    /** The options the DatePicker provides to its days and captions. */
    export function injectDatePicker() {
      const datePicker = inject(DatePickerKey);
      if (!datePicker) {
        throw new Error('DatePicker parts must be used within a <DatePicker />');
      }
      return datePicker;
    }
    
  4. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { DatePicker } from '@/components/date-picker';

Examples

Range

Set mode="range" to pick a start and end date, with the days between highlighted.

<script setup lang="ts">
import { CalendarDate } from '@internationalized/date';
import { ref } from 'vue';

import { Card, CardContent } from '@/components/card';
import { DatePicker } from '@/components/date-picker';

const range = ref({
  start: new CalendarDate(2025, 1, 8),
  end: new CalendarDate(2025, 1, 16),
});
</script>

<template>
  <Card class="nx:w-fit">
    <CardContent class="nx:p-0">
      <DatePicker v-model="range" mode="range" />
    </CardContent>
  </Card>
</template>

Multiple

Set mode="multiple" to pick several separate dates.

<script setup lang="ts">
import { CalendarDate } from '@internationalized/date';
import { ref } from 'vue';

import { Card, CardContent } from '@/components/card';
import { DatePicker } from '@/components/date-picker';

const dates = ref([
  new CalendarDate(2025, 1, 6),
  new CalendarDate(2025, 1, 13),
  new CalendarDate(2025, 1, 20),
]);
</script>

<template>
  <Card class="nx:w-fit">
    <CardContent class="nx:p-0">
      <DatePicker v-model="dates" mode="multiple" />
    </CardContent>
  </Card>
</template>

Dropdown caption

Set captionLayout="dropdown" to jump between months and years from dropdowns.

<script setup lang="ts">
import { CalendarDate } from '@internationalized/date';
import { ref } from 'vue';

import { Card, CardContent } from '@/components/card';
import { DatePicker } from '@/components/date-picker';

const date = ref(new CalendarDate(2025, 1, 15));
</script>

<template>
  <Card class="nx:w-fit">
    <CardContent class="nx:p-0">
      <DatePicker v-model="date" caption-layout="dropdown" />
    </CardContent>
  </Card>
</template>

Disabled days

Pass isDateDisabled so matching dates show dimmed and cannot be picked.

<script setup lang="ts">
import { CalendarDate } from '@internationalized/date';

import { Card, CardContent } from '@/components/card';
import { DatePicker } from '@/components/date-picker';

const month = new CalendarDate(2025, 1, 1);
const disabledDays = ['2025-01-10', '2025-01-11'];
</script>

<template>
  <Card class="nx:w-fit">
    <CardContent class="nx:p-0">
      <DatePicker
        :default-placeholder="month"
        :is-date-disabled="(date) => disabledDays.includes(date.toString())"
      />
    </CardContent>
  </Card>
</template>

With presets

Add preset buttons in a Card footer to jump to common dates.

<script setup lang="ts">
import { CalendarDate } from '@internationalized/date';
import { ref } from 'vue';

import { Button } from '@/components/button';
import { Card, CardContent, CardFooter } from '@/components/card';
import { DatePicker } from '@/components/date-picker';

const start = new CalendarDate(2025, 1, 15);
const date = ref(new CalendarDate(2025, 1, 15));
const month = ref(new CalendarDate(2025, 1, 1));
const presets = [
  { label: 'Today', days: 0 },
  { label: 'Tomorrow', days: 1 },
  { label: 'In 3 days', days: 3 },
  { label: 'In a week', days: 7 },
  { label: 'In 2 weeks', days: 14 },
];
</script>

<template>
  <Card class="nx:w-fit">
    <CardContent class="nx:p-0">
      <DatePicker v-model="date" v-model:placeholder="month" fixed-weeks />
    </CardContent>
    <CardFooter
      class="nx:grid nx:grid-cols-2 nx:gap-2 nx:border-t-default nx:border-border-default nx:p-4 nx:[&>*:last-child]:col-span-2"
    >
      <Button
        v-for="preset in presets"
        :key="preset.label"
        variant="outline"
        size="sm"
        @click="
          date = start.add({ days: preset.days });
          month = date.set({ day: 1 });
        "
        >{{ preset.label }}</Button
      >
    </CardFooter>
  </Card>
</template>

This example also needs: Button.

Props

DatePicker

DatePicker props
PropTypeDefaultDescription
buttonVariant"default" | "destructive" | "error" | "outline" | "dashed" | "secondary" | "ghost" | "link" | null"ghost"

Button variant for the previous and next month arrows.

captionLayout"label" | "dropdown""label"

Static month label or month and year dropdowns.

cellSizeDatePickerCellSize"default"

Day-cell size: default is 32px, large 48px, xlarge 56px. For another size, set --cell-size through className.

defaultValueDateValue | DateValue[] | DateRangeNo default documented

The starting date, dates or range without v-model.

mode"single" | "multiple" | "range""single"

Picks one date, several dates, or a start and end date.

modelValueDateValue | DateValue[] | DateRange | nullNo default documented

The picked date, dates or range, by mode. Bind it with v-model.

showOutsideDaysbooleantrue

Shows days from the previous and next months.

todayDateValueNo default documented

The date marked as today. Defaults to the current date.

DatePickerDayButton

DatePickerDayButton props
PropTypeDefaultDescription
modifiersRequiredDatePickerDayModifiersNo default documented

The day's state, which the button shows through its data-* attributes.