単一の日付、複数の日付、または日付範囲を選択するカレンダーコンポーネント。

使用法

v-modelディレクティブを使用して選択した日付を制御します。

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
1
2
3
4
5
6
7
8
9
10
11
12
Event Date, February 2022
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 2, 3))
</script>

<template>
  <UCalendar v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 2, 3))
</script>

<template>
  <UCalendar v-model="value" />
</template>

状態を制御する必要がない場合は、default-valueプロパティを使用して初期値を設定します。

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
1
2
3
4
5
6
7
8
9
10
11
12
Event Date, February 2022
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const defaultValue = shallowRef(new CalendarDate(2022, 2, 6))
</script>

<template>
  <UCalendar :default-value="defaultValue" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const defaultValue = shallowRef(new CalendarDate(2022, 2, 6))
</script>

<template>
  <UCalendar :default-value="defaultValue" />
</template>
このコンポーネントは、ロケールに対応した書式設定のために@internationalized/dateパッケージを使用します。日付フォーマットは、Appコンポーネントのlocaleプロパティによって決定されます。
このコンポーネントは、ロケールに対応した書式設定のために@internationalized/dateパッケージを使用します。日付フォーマットは、Appコンポーネントのlocaleプロパティによって決定されます。

タイプbadge{label="4.9+" class="align-text-top"}

typeプロパティを使用して、カレンダーが選択するものを変更します。デフォルトはdateです。

dateを使用する場合は、見出しをクリックして日表示から月表示、年表示に切り替え、ドリルダウンして日付を選択します。

Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
Month Picker, 2022
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 2, 1))
</script>

<template>
  <UCalendar type="month" v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 2, 1))
</script>

<template>
  <UCalendar type="month" v-model="value" />
</template>

type="year"を使用してスタンドアロンの年ピッカーをレンダリングします。

2020 - 2031
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
Year Picker, 2020 - 2031
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 1, 1))
</script>

<template>
  <UCalendar type="year" v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef(new CalendarDate(2022, 1, 1))
</script>

<template>
  <UCalendar type="year" v-model="value" />
</template>

複数

multipleプロパティを使用して複数選択できます。

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
1
2
3
4
5
6
7
8
9
10
11
12
Event Date, February 2022
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef([
  new CalendarDate(2022, 2, 4),
  new CalendarDate(2022, 2, 6),
  new CalendarDate(2022, 2, 8)
])
</script>

<template>
  <UCalendar multiple v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef([
  new CalendarDate(2022, 2, 4),
  new CalendarDate(2022, 2, 6),
  new CalendarDate(2022, 2, 8)
])
</script>

<template>
  <UCalendar multiple v-model="value" />
</template>

Range

rangeプロパティを使用して、日付の範囲を選択します。

Event Date, February 2022
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
1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef({
  start: new CalendarDate(2022, 2, 3),
  end: new CalendarDate(2022, 2, 20)
})
</script>

<template>
  <UCalendar range v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef({
  start: new CalendarDate(2022, 2, 3),
  end: new CalendarDate(2022, 2, 20)
})
</script>

<template>
  <UCalendar range v-model="value" />
</template>

rangeプロパティはtype="month"とtype="year"でも動作し、月または年の範囲を選択できます。

Month Picker, 2022
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const value = shallowRef({ start: new CalendarDate(2022, 2, 1), end: new CalendarDate(2022, 6, 1) })
</script>

<template>
  <UCalendar type="month" range v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef({ start: new CalendarDate(2022, 2, 1), end: new CalendarDate(2022, 6, 1) })
</script>

<template>
  <UCalendar type="month" range v-model="value" />
</template>

月数

numberOfMonthsプロパティを使用して、カレンダーの月数を変更します。

27
28
29
30
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
31
1
2
3
4
5
6
7
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
4
5
6
7
8
9
10
11
12
29
30
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
31
1
2
3
4
5
6
7
8
9
Event Date, October - December 2026
<template>
  <UCalendar :number-of-months="3" />
</template>

Monthコントロール

month-controlsプロパティを使用して月コントロールを表示します。デフォルトはtrueです。

27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar :month-controls="false" />
</template>

prev-monthとnext-monthの小道具を使用して月ボタンを上書きします。

27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar
    :prev-month="{
      color: 'primary',
      variant: 'soft'
    }"
    :next-month="{
      color: 'primary',
      variant: 'soft'
    }"
  />
</template>

Yearコントロール

year-controlsプロパティを使用して年コントロールを表示します。デフォルトはtrueです。

27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar :year-controls="false" />
</template>

prev-yearとnext-yearの小道具を使用して年ボタンを上書きします。

27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar
    :prev-year="{
      color: 'primary',
      variant: 'soft'
    }"
    :next-year="{
      color: 'primary',
      variant: 'soft'
    }"
  />
</template>

Viewコントロールbadge{label="4.9+" class="align-text-top"}

view-controlプロパティを使用して、見出しを日、月、年ビューを切り替えるボタンにします。デフォルトはtrueです。

October 2026
27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar :view-control="false" />
</template>

見出しボタンをオーバーライドするオブジェクトにview-controlプロパティを設定します。

27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar
    :view-control="{
      color: 'primary',
      variant: 'soft'
    }"
  />
</template>

固定週

fixed-weeksプロパティを使用して、固定週のカレンダーを表示します。

27
28
29
30
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
31
Event Date, October 2026
<template>
  <UCalendar :fixed-weeks="false" />
</template>

週番号badge{label="4.4+" class="align-text-top"}

week-numbersプロパティを使用して、カレンダーに週番号を表示します。

40
27
28
29
30
1
2
3
41
4
5
6
7
8
9
10
42
11
12
13
14
15
16
17
43
18
19
20
21
22
23
24
44
25
26
27
28
29
30
31
45
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar week-numbers />
</template>

Color

colorプロパティを使用してカレンダーの色を変更します。

Event Date, February 2022
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
1
2
3
4
5
6
7
8
9
10
11
12
<template>
  <UCalendar color="neutral" />
</template>

Variant

variantプロパティを使用して、カレンダーのバリアントを変更します。

Event Date, February 2022
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
1
2
3
4
5
6
7
8
9
10
11
12
<template>
  <UCalendar variant="subtle" />
</template>

サイズ

sizeプロパティを使用してカレンダーのサイズを変更します。

27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar size="xl" />
</template>

無効

disabledプロパティを使用してカレンダーを無効にします。

27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<template>
  <UCalendar disabled />
</template>

例

Withチップイベント

Chipコンポーネントを使用して、特定の日にイベントを追加します。

26
27
28
29
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
31
1
2
3
4
5
Event Date, January 2022
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))

function getColorByDate(date: Date) {
  const isWeekend = date.getDay() % 6 == 0
  const isDayMeeting = date.getDay() % 3 == 0

  if (isWeekend) {
    return undefined
  }

  if (isDayMeeting) {
    return 'error'
  }

  return 'success'
}
</script>

<template>
  <UCalendar v-model="modelValue">
    <template #day="{ day }">
      <UChip :show="!!getColorByDate(day.toDate('UTC'))" :color="getColorByDate(day.toDate('UTC'))" size="2xs">
        {{ day.day }}
      </UChip>
    </template>
  </UCalendar>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))

function getColorByDate(date: Date) {
  const isWeekend = date.getDay() % 6 == 0
  const isDayMeeting = date.getDay() % 3 == 0

  if (isWeekend) {
    return undefined
  }

  if (isDayMeeting) {
    return 'error'
  }

  return 'success'
}
</script>

<template>
  <UCalendar v-model="modelValue">
    <template #day="{ day }">
      <UChip :show="!!getColorByDate(day.toDate('UTC'))" :color="getColorByDate(day.toDate('UTC'))" size="2xs">
        {{ day.day }}
      </UChip>
    </template>
  </UCalendar>
</template>

無効な日付付き

特定の日付を無効にする関数でis-date-disabledプロパティを使用します。type="month"またはtype="year"を使用する場合は、代わりにis-month-disabledまたはis-year-disabledプロパティを使用します。

Event Date, January 2022
26
27
28
29
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
31
1
2
3
4
5
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateDisabled = (date: DateValue) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UCalendar v-model="modelValue" :is-date-disabled="isDateDisabled" range />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateDisabled = (date: DateValue) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UCalendar v-model="modelValue" :is-date-disabled="isDateDisabled" range />
</template>

利用できない日付

特定の日付を利用できないとマークする関数を使ってis-date-unavailableプロパティを使用します。type="month"またはtype="year"を使用する場合は、代わりにis-month-unavailableまたはis-year-unavailableプロパティを使用します。

Event Date, January 2022
26
27
28
29
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
31
1
2
3
4
5
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateUnavailable = (date: DateValue) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UCalendar v-model="modelValue" :is-date-unavailable="isDateUnavailable" range />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef({
  start: new CalendarDate(2022, 1, 1),
  end: new CalendarDate(2022, 1, 9)
})

const isDateUnavailable = (date: DateValue) => {
  return date.day >= 10 && date.day <= 16
}
</script>

<template>
  <UCalendar v-model="modelValue" :is-date-unavailable="isDateUnavailable" range />
</template>

最小/最大日付付き

min-valueとmax-valueの小道具を使用して日付を制限します。

27
28
29
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
4
5
6
7
Event Date, September 2023
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2023, 9, 10))
const minDate = new CalendarDate(2023, 9, 1)
const maxDate = new CalendarDate(2023, 9, 30)
</script>

<template>
  <UCalendar v-model="modelValue" :min-value="minDate" :max-value="maxDate" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const modelValue = shallowRef(new CalendarDate(2023, 9, 10))
const minDate = new CalendarDate(2023, 9, 1)
const maxDate = new CalendarDate(2023, 9, 30)
</script>

<template>
  <UCalendar v-model="modelValue" :min-value="minDate" :max-value="maxDate" />
</template>

その他のカレンダーシステム

@internationalized/dateの他のカレンダーを使用して、別のカレンダーシステムを実装できます。

24
25
26
27
28
29
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
4
5
6
Event Date, Tishri 5781
<script lang="ts" setup>
import { CalendarDate, HebrewCalendar } from '@internationalized/date'

const hebrewDate = shallowRef(new CalendarDate(new HebrewCalendar(), 5781, 1, 1))
</script>

<template>
  <UCalendar v-model="hebrewDate" />
</template>
利用可能なカレンダーは@internationalized/dateドキュメントで確認できます。

外部コントロール付き

v-modelで渡された日付を操作することで、外部コントロールでカレンダーを制御できます。

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
4
5
6
7
8
9
10
Event Date, April 2025
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'

const date = shallowRef(new CalendarDate(2025, 4, 2))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" :month-controls="false" :year-controls="false" />

    <div class="flex justify-between gap-4">
      <UButton color="neutral" variant="outline" @click="date = date.subtract({ months: 1 })">
        Prev
      </UButton>

      <UButton color="neutral" variant="outline" @click="date = date.add({ months: 1 })">
        Next
      </UButton>
    </div>
  </div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'

const date = shallowRef(new CalendarDate(2025, 4, 2))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" :month-controls="false" :year-controls="false" />

    <div class="flex justify-between gap-4">
      <UButton color="neutral" variant="outline" @click="date = date.subtract({ months: 1 })">
        Prev
      </UButton>

      <UButton color="neutral" variant="outline" @click="date = date.add({ months: 1 })">
        Next
      </UButton>
    </div>
  </div>
</template>

今日の日付付き

@internationalized/dateとgetLocalTimeZoneのtoday関数を使用して、値を現在の日付に設定します。

27
28
29
30
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
31
1
2
3
4
5
6
7
Event Date, October 2026
<script setup lang="ts">
import { getLocalTimeZone, today } from '@internationalized/date'

const date = shallowRef(today(getLocalTimeZone()))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" />

    <UButton color="neutral" variant="outline" class="justify-center" @click="date = today(getLocalTimeZone())">
      Today
    </UButton>
  </div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { getLocalTimeZone, today } from '@internationalized/date'

const date = shallowRef(today(getLocalTimeZone()))
</script>

<template>
  <div class="flex flex-col gap-4">
    <UCalendar v-model="date" />

    <UButton color="neutral" variant="outline" class="justify-center" @click="date = today(getLocalTimeZone())">
      Today
    </UButton>
  </div>
</template>

日付ピッカーとして

日付ピッカーを作成するには、ButtonとPopoverコンポーネントを使用します。

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

const df = new DateFormatter('en-US', {
  dateStyle: 'medium'
})

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
</script>

<template>
  <UPopover>
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      {{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
    </UButton>

    <template #content>
      <UCalendar v-model="modelValue" class="p-2" />
    </template>
  </UPopover>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'

const df = new DateFormatter('en-US', {
  dateStyle: 'medium'
})

const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
</script>

<template>
  <UPopover>
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      {{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
    </UButton>

    <template #content>
      <UCalendar v-model="modelValue" class="p-2" />
    </template>
  </UPopover>
</template>

日付範囲ピッカーとして

ButtonコンポーネントとPopoverコンポーネントを使用して、プリセット範囲を持つ日付範囲ピッカーを作成します。

<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date'

const df = new DateFormatter('en-US', { dateStyle: 'medium' })
const tz = getLocalTimeZone()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isDesktop = breakpoints.greaterOrEqual('sm')

const ranges = [
  { label: 'Last 7 days', days: 7 },
  { label: 'Last 14 days', days: 14 },
  { label: 'Last 30 days', days: 30 },
  { label: 'Last 3 months', months: 3 },
  { label: 'Last 6 months', months: 6 },
  { label: 'Last year', years: 1 }
]

const initialEnd = today(tz)
const modelValue = shallowRef({
  start: initialEnd.subtract({ days: 14 }),
  end: initialEnd
})

const label = computed(() => {
  const { start, end } = modelValue.value
  if (!start) return 'Pick a date'
  if (!end) return df.format(start.toDate(tz))
  return `${df.format(start.toDate(tz))} - ${df.format(end.toDate(tz))}`
})

function computeStart(range: typeof ranges[number]) {
  const end = today(tz)
  return { start: end.subtract({ days: range.days, months: range.months, years: range.years }), end }
}

function isRangeSelected(range: typeof ranges[number]) {
  if (!modelValue.value?.start || !modelValue.value?.end) return false
  const { start, end } = computeStart(range)
  return modelValue.value.start.compare(start) === 0 && modelValue.value.end.compare(end) === 0
}

function selectRange(range: typeof ranges[number]) {
  modelValue.value = computeStart(range)
}
</script>

<template>
  <UPopover :content="{ align: 'center' }">
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      {{ label }}
    </UButton>

    <template #content>
      <div class="flex items-stretch divide-x divide-(--ui-border)">
        <div class="hidden sm:flex flex-col justify-center py-2">
          <UButton
            v-for="(range, index) in ranges"
            :key="index"
            :label="range.label"
            color="neutral"
            variant="ghost"
            class="rounded-none px-4"
            :class="[isRangeSelected(range) ? 'bg-elevated' : 'hover:bg-elevated/50']"
            truncate
            @click="selectRange(range)"
          />
        </div>

        <UCalendar v-model="modelValue" class="p-2" :number-of-months="isDesktop ? 2 : 1" range />
      </div>
    </template>
  </UPopover>
</template>
<script setup lang="ts">
import { shallowRef, computed } from 'vue'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date'

const df = new DateFormatter('en-US', { dateStyle: 'medium' })
const tz = getLocalTimeZone()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isDesktop = breakpoints.greaterOrEqual('sm')

const ranges = [
  { label: 'Last 7 days', days: 7 },
  { label: 'Last 14 days', days: 14 },
  { label: 'Last 30 days', days: 30 },
  { label: 'Last 3 months', months: 3 },
  { label: 'Last 6 months', months: 6 },
  { label: 'Last year', years: 1 }
]

const initialEnd = today(tz)
const modelValue = shallowRef({
  start: initialEnd.subtract({ days: 14 }),
  end: initialEnd
})

const label = computed(() => {
  const { start, end } = modelValue.value
  if (!start) return 'Pick a date'
  if (!end) return df.format(start.toDate(tz))
  return `${df.format(start.toDate(tz))} - ${df.format(end.toDate(tz))}`
})

function computeStart(range: typeof ranges[number]) {
  const end = today(tz)
  return { start: end.subtract({ days: range.days, months: range.months, years: range.years }), end }
}

function isRangeSelected(range: typeof ranges[number]) {
  if (!modelValue.value?.start || !modelValue.value?.end) return false
  const { start, end } = computeStart(range)
  return modelValue.value.start.compare(start) === 0 && modelValue.value.end.compare(end) === 0
}

function selectRange(range: typeof ranges[number]) {
  modelValue.value = computeStart(range)
}
</script>

<template>
  <UPopover :content="{ align: 'center' }">
    <UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
      {{ label }}
    </UButton>

    <template #content>
      <div class="flex items-stretch divide-x divide-(--ui-border)">
        <div class="hidden sm:flex flex-col justify-center py-2">
          <UButton
            v-for="(range, index) in ranges"
            :key="index"
            :label="range.label"
            color="neutral"
            variant="ghost"
            class="rounded-none px-4"
            :class="[isRangeSelected(range) ? 'bg-elevated' : 'hover:bg-elevated/50']"
            truncate
            @click="selectRange(range)"
          />
        </div>

        <UCalendar v-model="modelValue" class="p-2" :number-of-months="isDesktop ? 2 : 1" range />
      </div>
    </template>
  </UPopover>
</template>

API

Props

Prop Default Type
as
'div'
any

The element or component this component should render as.

type
'date'
"date" | "month" | "year"

The type of picker.

  • date renders a day calendar whose heading can switch to a month then year view.
  • month renders a standalone month picker.
  • year renders a standalone year picker.
nextYearIcon
appConfig.ui.icons.chevronDoubleRight
any

The icon to use for the next year control.

nextYear
Omit<ButtonProps, LinkPropsKeys>

Configure the next year button. { color: 'neutral', variant: 'ghost' }

nextMonthIcon
appConfig.ui.icons.chevronRight
any

The icon to use for the next month control.

nextMonth
Omit<ButtonProps, LinkPropsKeys>

Configure the next month button. { color: 'neutral', variant: 'ghost' }

prevYearIcon
appConfig.ui.icons.chevronDoubleLeft
any

The icon to use for the previous year control.

prevYear
Omit<ButtonProps, LinkPropsKeys>

Configure the prev year button. { color: 'neutral', variant: 'ghost' }

prevMonthIcon
appConfig.ui.icons.chevronLeft
any

The icon to use for the previous month control.

prevMonth
Omit<ButtonProps, LinkPropsKeys>

Configure the prev month button. { color: 'neutral', variant: 'ghost' }

viewControl
true
boolean | Omit<ButtonProps, LinkPropsKeys>

Whether to make the heading a button that switches between the day, month and year views. Has no effect when type is year. Can be an object to override the button props. { color: 'neutral', variant: 'ghost', block: true }

color
'primary'
"primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"
variant
'solid'
"solid" | "outline" | "soft" | "subtle"
size
'md'
"xs" | "sm" | "md" | "lg" | "xl"
range
R

Whether or not a range of dates can be selected

multiple
M

Whether or not multiple dates can be selected

monthControls
true
boolean

Show month controls

yearControls
true
boolean

Show year controls

defaultValue
CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[]
modelValue
null | CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[]
weekNumbers
boolean
defaultPlaceholder
CalendarDate | CalendarDateTime | ZonedDateTime
placeholder
CalendarDate | CalendarDateTime | ZonedDateTime
allowNonContiguousRanges
boolean

When combined with isDateUnavailable, determines whether non-contiguous ranges, i.e. ranges containing unavailable dates, may be selected.

pagedNavigation
boolean

This property causes the previous and next buttons to navigate by the number of months displayed at once, rather than one month

preventDeselect
boolean

Whether or not to prevent the user from deselecting a date without selecting another date first

maximumDays
number

The maximum number of days that can be selected in a range

weekStartsOn
0 | 1 | 2 | 4 | 5 | 3 | 6

The day of the week to start the calendar on

weekdayFormat
"narrow" | "short" | "long"

The format to use for the weekday strings provided via the weekdays slot prop

fixedWeeks
true
boolean

Whether or not to always display 6 weeks in the calendar

maxValue
CalendarDate | CalendarDateTime | ZonedDateTime
minValue
CalendarDate | CalendarDateTime | ZonedDateTime
locale
string

The locale to use for formatting dates

numberOfMonths
number

The number of months to display at once

disabled
boolean

Whether or not the calendar is disabled

readonly
boolean

Whether or not the calendar is readonly

initialFocus
boolean

If true, the calendar will focus the selected day, today, or the first day of the month depending on what is visible when the calendar is mounted

isDateDisabled
(date: DateValue): boolean

A function that returns whether or not a date is disabled

isDateUnavailable
(date: DateValue): boolean

A function that returns whether or not a date is unavailable

isDateHighlightable
(date: DateValue): boolean

A function that returns whether or not a date is hightable

nextPage
(placeholder: DateValue): DateValue

A function that returns the next page of the calendar. It receives the current placeholder as an argument inside the component.

prevPage
(placeholder: DateValue): DateValue

A function that returns the previous page of the calendar. It receives the current placeholder as an argument inside the component.

disableDaysOutsideCurrentView
boolean

Whether or not to disable days outside the current view.

fixedDate
"start" | "end"

Which part of the range should be fixed

isMonthDisabled
(date: DateValue): boolean

A function that returns whether or not a month is disabled

isMonthUnavailable
(date: DateValue): boolean

A function that returns whether or not a month is unavailable

isYearDisabled
(date: DateValue): boolean

A function that returns whether or not a year is disabled

isYearUnavailable
(date: DateValue): boolean

A function that returns whether or not a year is unavailable

ui
{ root?: SlotClass; header?: SlotClass; body?: SlotClass; heading?: SlotClass; headingLabel?: SlotClass; grid?: SlotClass; gridRow?: SlotClass; gridWeekDaysRow?: SlotClass; gridBody?: SlotClass; headCell?: SlotClass; headCellWeek?: SlotClass; cell?: SlotClass; cellTrigger?: SlotClass; cellWeek?: SlotClass; }

スロット

Slot Type
heading
{ value: string; view: CalendarView; date: DateValue; setView: (view: CalendarView) => void; setPlaceholder: (date: DateValue) => void; }
day
Pick<CalendarCellTriggerProps, "day">
week-day
{ day: string; }
month-cell
{ month: DateValue; selected: boolean; disabled: boolean; }
year-cell
{ year: DateValue; selected: boolean; disabled: boolean; }

Emits

Event Type
update:modelValue
[value: CalendarModelValue<R, M>]
update:placeholder
[date: DateValue]
update:validModelValue
[date: DateRange]
update:startValue
[date: DateValue | undefined]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    calendar: {
      slots: {
        root: '',
        header: 'flex items-center justify-between',
        body: 'flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0',
        heading: 'flex-1 min-w-0 text-center',
        headingLabel: 'font-medium block truncate p-1.5',
        grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
        gridRow: 'grid',
        gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
        gridBody: 'grid',
        headCell: 'rounded-md',
        headCellWeek: 'rounded-md text-muted',
        cell: 'relative text-center',
        cellTrigger: [
          'm-0.5 relative flex items-center justify-center whitespace-nowrap focus-visible:outline-3 data-disabled:text-muted data-unavailable:line-through data-unavailable:text-muted data-unavailable:pointer-events-none data-today:font-semibold',
          'transition'
        ],
        cellWeek: 'relative text-center text-muted'
      },
      variants: {
        color: {
          primary: {
            headCell: 'text-primary',
            cellTrigger: 'outline-primary/25'
          },
          secondary: {
            headCell: 'text-secondary',
            cellTrigger: 'outline-secondary/25'
          },
          success: {
            headCell: 'text-success',
            cellTrigger: 'outline-success/25'
          },
          info: {
            headCell: 'text-info',
            cellTrigger: 'outline-info/25'
          },
          warning: {
            headCell: 'text-warning',
            cellTrigger: 'outline-warning/25'
          },
          error: {
            headCell: 'text-error',
            cellTrigger: 'outline-error/25'
          },
          neutral: {
            headCell: 'text-highlighted',
            cellTrigger: 'outline-inverted/25'
          }
        },
        variant: {
          solid: '',
          outline: '',
          soft: '',
          subtle: ''
        },
        size: {
          xs: {
            headingLabel: 'text-xs',
            cell: 'text-xs',
            cellWeek: 'text-xs',
            headCell: 'text-[10px]',
            headCellWeek: 'text-[10px]',
            body: 'space-y-2 pt-2'
          },
          sm: {
            headingLabel: 'text-xs',
            headCell: 'text-xs',
            headCellWeek: 'text-xs',
            cellWeek: 'text-xs',
            cell: 'text-xs'
          },
          md: {
            headingLabel: 'text-sm',
            headCell: 'text-xs',
            headCellWeek: 'text-xs',
            cellWeek: 'text-xs',
            cell: 'text-sm'
          },
          lg: {
            headingLabel: 'text-base',
            headCell: 'text-base',
            headCellWeek: 'text-base',
            cellWeek: 'text-base',
            cell: 'text-base'
          },
          xl: {
            headingLabel: 'text-lg',
            headCell: 'text-lg',
            headCellWeek: 'text-lg',
            cellWeek: 'text-lg',
            cell: 'text-lg'
          }
        },
        view: {
          day: {
            gridRow: 'grid-cols-7 place-items-center',
            cellTrigger: 'rounded-full data-outside-view:text-muted'
          },
          month: {
            gridRow: 'grid-cols-4',
            cellTrigger: 'rounded-md'
          },
          year: {
            gridRow: 'grid-cols-4',
            cellTrigger: 'rounded-md'
          }
        },
        weekNumbers: {
          true: ''
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: 'solid',
          class: {
            cellTrigger: 'data-selected:bg-primary data-selected:text-inverted data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
          }
        },
        {
          color: 'primary',
          variant: 'outline',
          class: {
            cellTrigger: 'data-selected:ring data-selected:ring-inset data-selected:ring-primary/50 data-selected:text-primary data-selected:focus-visible:ring-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/10 hover:not-data-selected:bg-primary/10'
          }
        },
        {
          color: 'primary',
          variant: 'soft',
          class: {
            cellTrigger: 'data-selected:bg-primary/10 data-selected:text-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
          }
        },
        {
          color: 'primary',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-selected:bg-primary/10 data-selected:text-primary data-selected:ring data-selected:ring-inset data-selected:ring-primary/25 data-selected:focus-visible:ring-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
          }
        },
        {
          color: 'neutral',
          variant: 'solid',
          class: {
            cellTrigger: 'data-selected:bg-inverted data-selected:text-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'outline',
          class: {
            cellTrigger: 'data-selected:ring data-selected:ring-inset data-selected:ring-accented data-selected:text-default data-selected:bg-default data-selected:focus-visible:ring-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/10 hover:not-data-selected:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'soft',
          class: {
            cellTrigger: 'data-selected:bg-elevated data-selected:text-default data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
          }
        },
        {
          color: 'neutral',
          variant: 'subtle',
          class: {
            cellTrigger: 'data-selected:bg-elevated data-selected:text-default data-selected:ring data-selected:ring-inset data-selected:ring-accented data-selected:focus-visible:ring-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
          }
        },
        {
          size: 'xs',
          view: 'day',
          class: {
            cellTrigger: 'size-6'
          }
        },
        {
          size: 'sm',
          view: 'day',
          class: {
            cellTrigger: 'size-7'
          }
        },
        {
          size: 'md',
          view: 'day',
          class: {
            cellTrigger: 'size-8'
          }
        },
        {
          size: 'lg',
          view: 'day',
          class: {
            cellTrigger: 'size-9'
          }
        },
        {
          size: 'xl',
          view: 'day',
          class: {
            cellTrigger: 'size-10'
          }
        },
        {
          size: 'xs',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-6 px-2'
          }
        },
        {
          size: 'sm',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-7 px-2'
          }
        },
        {
          size: 'md',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-8 px-3'
          }
        },
        {
          size: 'lg',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-9 px-4'
          }
        },
        {
          size: 'xl',
          view: [
            'month',
            'year'
          ],
          class: {
            cellTrigger: 'h-10 px-5'
          }
        },
        {
          view: 'day',
          weekNumbers: true,
          class: {
            gridRow: 'grid-cols-8',
            gridWeekDaysRow: 'grid-cols-8 [&>*:first-child]:col-start-2'
          }
        }
      ],
      defaultVariants: {
        size: 'md',
        color: 'primary',
        variant: 'solid',
        view: 'day'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        calendar: {
          slots: {
            root: '',
            header: 'flex items-center justify-between',
            body: 'flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0',
            heading: 'flex-1 min-w-0 text-center',
            headingLabel: 'font-medium block truncate p-1.5',
            grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
            gridRow: 'grid',
            gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
            gridBody: 'grid',
            headCell: 'rounded-md',
            headCellWeek: 'rounded-md text-muted',
            cell: 'relative text-center',
            cellTrigger: [
              'm-0.5 relative flex items-center justify-center whitespace-nowrap focus-visible:outline-3 data-disabled:text-muted data-unavailable:line-through data-unavailable:text-muted data-unavailable:pointer-events-none data-today:font-semibold',
              'transition'
            ],
            cellWeek: 'relative text-center text-muted'
          },
          variants: {
            color: {
              primary: {
                headCell: 'text-primary',
                cellTrigger: 'outline-primary/25'
              },
              secondary: {
                headCell: 'text-secondary',
                cellTrigger: 'outline-secondary/25'
              },
              success: {
                headCell: 'text-success',
                cellTrigger: 'outline-success/25'
              },
              info: {
                headCell: 'text-info',
                cellTrigger: 'outline-info/25'
              },
              warning: {
                headCell: 'text-warning',
                cellTrigger: 'outline-warning/25'
              },
              error: {
                headCell: 'text-error',
                cellTrigger: 'outline-error/25'
              },
              neutral: {
                headCell: 'text-highlighted',
                cellTrigger: 'outline-inverted/25'
              }
            },
            variant: {
              solid: '',
              outline: '',
              soft: '',
              subtle: ''
            },
            size: {
              xs: {
                headingLabel: 'text-xs',
                cell: 'text-xs',
                cellWeek: 'text-xs',
                headCell: 'text-[10px]',
                headCellWeek: 'text-[10px]',
                body: 'space-y-2 pt-2'
              },
              sm: {
                headingLabel: 'text-xs',
                headCell: 'text-xs',
                headCellWeek: 'text-xs',
                cellWeek: 'text-xs',
                cell: 'text-xs'
              },
              md: {
                headingLabel: 'text-sm',
                headCell: 'text-xs',
                headCellWeek: 'text-xs',
                cellWeek: 'text-xs',
                cell: 'text-sm'
              },
              lg: {
                headingLabel: 'text-base',
                headCell: 'text-base',
                headCellWeek: 'text-base',
                cellWeek: 'text-base',
                cell: 'text-base'
              },
              xl: {
                headingLabel: 'text-lg',
                headCell: 'text-lg',
                headCellWeek: 'text-lg',
                cellWeek: 'text-lg',
                cell: 'text-lg'
              }
            },
            view: {
              day: {
                gridRow: 'grid-cols-7 place-items-center',
                cellTrigger: 'rounded-full data-outside-view:text-muted'
              },
              month: {
                gridRow: 'grid-cols-4',
                cellTrigger: 'rounded-md'
              },
              year: {
                gridRow: 'grid-cols-4',
                cellTrigger: 'rounded-md'
              }
            },
            weekNumbers: {
              true: ''
            }
          },
          compoundVariants: [
            {
              color: 'primary',
              variant: 'solid',
              class: {
                cellTrigger: 'data-selected:bg-primary data-selected:text-inverted data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
              }
            },
            {
              color: 'primary',
              variant: 'outline',
              class: {
                cellTrigger: 'data-selected:ring data-selected:ring-inset data-selected:ring-primary/50 data-selected:text-primary data-selected:focus-visible:ring-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/10 hover:not-data-selected:bg-primary/10'
              }
            },
            {
              color: 'primary',
              variant: 'soft',
              class: {
                cellTrigger: 'data-selected:bg-primary/10 data-selected:text-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
              }
            },
            {
              color: 'primary',
              variant: 'subtle',
              class: {
                cellTrigger: 'data-selected:bg-primary/10 data-selected:text-primary data-selected:ring data-selected:ring-inset data-selected:ring-primary/25 data-selected:focus-visible:ring-primary data-today:not-data-selected:text-primary data-highlighted:bg-primary/20 hover:not-data-selected:bg-primary/20'
              }
            },
            {
              color: 'neutral',
              variant: 'solid',
              class: {
                cellTrigger: 'data-selected:bg-inverted data-selected:text-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
              }
            },
            {
              color: 'neutral',
              variant: 'outline',
              class: {
                cellTrigger: 'data-selected:ring data-selected:ring-inset data-selected:ring-accented data-selected:text-default data-selected:bg-default data-selected:focus-visible:ring-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/10 hover:not-data-selected:bg-inverted/10'
              }
            },
            {
              color: 'neutral',
              variant: 'soft',
              class: {
                cellTrigger: 'data-selected:bg-elevated data-selected:text-default data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
              }
            },
            {
              color: 'neutral',
              variant: 'subtle',
              class: {
                cellTrigger: 'data-selected:bg-elevated data-selected:text-default data-selected:ring data-selected:ring-inset data-selected:ring-accented data-selected:focus-visible:ring-inverted data-today:not-data-selected:text-highlighted data-highlighted:bg-inverted/20 hover:not-data-selected:bg-inverted/10'
              }
            },
            {
              size: 'xs',
              view: 'day',
              class: {
                cellTrigger: 'size-6'
              }
            },
            {
              size: 'sm',
              view: 'day',
              class: {
                cellTrigger: 'size-7'
              }
            },
            {
              size: 'md',
              view: 'day',
              class: {
                cellTrigger: 'size-8'
              }
            },
            {
              size: 'lg',
              view: 'day',
              class: {
                cellTrigger: 'size-9'
              }
            },
            {
              size: 'xl',
              view: 'day',
              class: {
                cellTrigger: 'size-10'
              }
            },
            {
              size: 'xs',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-6 px-2'
              }
            },
            {
              size: 'sm',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-7 px-2'
              }
            },
            {
              size: 'md',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-8 px-3'
              }
            },
            {
              size: 'lg',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-9 px-4'
              }
            },
            {
              size: 'xl',
              view: [
                'month',
                'year'
              ],
              class: {
                cellTrigger: 'h-10 px-5'
              }
            },
            {
              view: 'day',
              weekNumbers: true,
              class: {
                gridRow: 'grid-cols-8',
                gridWeekDaysRow: 'grid-cols-8 [&>*:first-child]:col-start-2'
              }
            }
          ],
          defaultVariants: {
            size: 'md',
            color: 'primary',
            variant: 'solid',
            view: 'day'
          }
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.

Changelog

No recent changes