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.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
'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.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
'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.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
'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.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
'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.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
'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.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
'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
| Prop | Type | Default | Description |
|---|---|---|---|
buttonVariant | "link" | "default" | "error" | "destructive" | "outline" | "dashed" | "secondary" | "ghost" | null | "ghost" | Button variant for the previous and next month arrows. |
cellSize | DatePickerCellSize | "default" | Day-cell size: |
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:
- 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
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
cellSize | DatePickerCellSize | "default" | Day-cell size: |
defaultValue | DateValue | DateValue[] | DateRange | —No default documented | The starting date, dates or range without |
mode | "single" | "multiple" | "range" | "single" | Picks one date, several dates, or a start and end date. |
modelValue | DateValue | DateValue[] | DateRange | null | —No default documented | The picked date, dates or range, by mode. Bind it with |
showOutsideDays | boolean | true | Shows days from the previous and next months. |
today | DateValue | —No default documented | The date marked as today. Defaults to the current date. |
DatePickerDayButton
| Prop | Type | Default | Description |
|---|---|---|---|
modifiersRequired | DatePickerDayModifiers | —No default documented | The day's state, which the button shows through its |