2026.05.27 リリースしました

確認する

Calendar

Calendarは、日付の表示や選択ができるコンポーネントです。

プロパティ

Calendarは、以下のプロパティをサポートしています。

名前 型 デフォルト値 説明
value string '' 日付(YYYY-MM-DD)を渡すことができます。
min string 0001-01-01 選択できる最小値を設定できます。
max string 9999-12-31 選択できる最大値を設定できます。
start string '' 範囲選択の開始日を指定できます。
end string '' 範囲選択の終了日を指定できます。
months number 1 表示する月数を指定できます。
enableRange boolean false 範囲選択が可能になります。
onChange (value: string) => void 日付が選択されたときのコールバック関数です。
onRangeChange (value: CalendarRange) => void 日付が範囲選択されたときのコールバック関数です。
autoFocusDate boolean false 日付に自動でフォーカスします。

CalendarDate

CalendarDateは、1日分の情報を表すオブジェクトです。

名前 型 デフォルト値 説明
value number 日付(数値)です。
outOfMonth boolean 表示範囲外の月かどうか。
disabled boolean 選択できるかどうか。
formatDate string フォーマット済みの日付文字列です。

CalendarRange

CalendarRangeは、範囲選択された日付の情報を表すオブジェクトです。

名前 型 デフォルト値 説明
start string '' 開始日です。
end string '' 終了日です。

インストールの手順

以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。

modules/Calendar.svelte
        <!--
@component
## 概要
- Calendar は、日付の表示や選択ができるコンポーネントです

## 機能
- 使用側から値(YYYY-MM-DD)を渡すと該当する日付が選択される
- 選択した日付を渡せる

## Props
- value: 指定の日付を渡すことができます
- min: 最小値を指定すると、最小の範囲を設定できます
- max: 最大値を指定すると、最大の範囲を設定できます
- start: 範囲選択の最小値を指定できます
- end: 範囲選択の最大値を指定できます
- enableRange: 範囲選択ができます
- onChange: 日付が選択されたときのコールバック関数
- onRangeChange: 日付が範囲選択されたときのコールバック関数
- autoFocusDate: 日付に自動でフォーカスします

## Usage
```svelte
  <Calendar />
```
-->
<script module lang="ts">
  import type { ClassValue } from 'svelte/elements';
  import { cva, type VariantProps } from 'class-variance-authority';

  const MIN_DATE = '0001-01-01';
  const MAX_DATE = '9999-12-31';
  const DISPLAY_MONTH_NUM = 12;
  const DISPLAY_YEAR_NUM = 12;
  const MONTHS_PER_ROW = 3;
  const YEARS_PER_ROW = 3;
  const WEEK_DAYS = ['日', '月', '火', '水', '木', '金', '土'];

  export const calendarVariants = cva('grid min-w-69.5 gap-3 p-3 bg-surface border border-border rounded-md shadow-md origin-top', {
    variants: {
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 pointer-events-none'],
        false: [],
      },
    },
  });

  export const calendarArrowButtonVariants = cva('p-1.5 bg-surface rounded-md text-muted-foreground outline-ring transition cursor-pointer hover:bg-accent/90 focus-visible:rounded-xs hover:text-accent-foreground outline-offset-2 focus-visible:outline-2', {
    variants: {
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 text-muted-foreground pointer-events-none'],
        false: [],
      },
    },
  });

  export const calendarMonthYearButtonVariants = cva('px-0.5 py-1.25 rounded-md font-medium leading-none outline-ring outline-offset-2 focus-visible:outline-2 transition cursor-pointer hover:bg-accent/90', {
    variants: {
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 text-muted-foreground pointer-events-none'],
        false: [],
      },
    },
  });

  export const calendarDayOfWeekVariants = cva('place-content-center px-3 py-0.75 font-normal leading-tight text-muted-foreground text-xs');

  export const calendarDateVariants = cva('relative z-0 place-content-center w-9 h-10 text-accent-foreground text-sm overflow-visible mx-auto cursor-pointer focus-visible:before:absolute hover:before:absolute focus-visible:before:inset-x-0 hover:before:inset-x-0 focus-visible:before:inset-y-0.5 hover:before:inset-y-0.5 focus-visible:z-10 focus-visible:before:content-[""] hover:before:content-[""] focus-visible:before:size-9 hover:before:size-9 hover:before:bg-accent/90 focus-visible:before:rounded-md focus-visible:before:outline-2 focus-visible:outline-none focus-visible:before:outline-offset-2 focus-visible:before:outline-ring hover:before:outline-ring hover:before:transition-colors hover:before:!-z-10 focus-visible:before:outline', {
    variants: {
      /** 選択されているかどうか */
      select: {
        true: [
          'text-bright-foreground after:absolute after:inset-x-0 after:inset-y-0.5 after:content-[""] after:size-9 after:bg-primary hover:after:bg-primary/90 after:rounded-md after:transition-colors hover:after:-z-10 after:-z-20',
        ],
        false: ['hover:before:rounded-md'],
      },
      /** 現在の日付かどうか */
      current: {
        true: [
          'after:absolute after:inset-x-0 after:inset-y-0.5 after:content-[""] after:size-9 after:border-2 after:border-border after:!rounded-md after:-z-10',
        ],
        false: [],
      },
      /** 選択されている月じゃないかどうか */
      outOfMonth: {
        true: ['opacity-50 text-muted-foreground'],
        false: [],
      },
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 text-muted-foreground pointer-events-none'],
        false: [],
      },
      /** 指定された日付の範囲かどうか */
      range: {
        true: [
          'relative z-0 before:absolute before:inset-x-0 before:inset-y-0.5 before:content-[""] before:size-9 before:bg-primary/10 hover:before:rounded-none before:-z-30',
        ],
        false: [],
      },
    },
    compoundVariants: [
      {
        select: true,
        current: true,
        class: 'after:border-none after:-z-10 before:-z-20',
      },
    ],
  });

  export const calendarMonthYearVariants = cva('relative z-0 place-content-center min-w-13 w-full px-2 py-1 rounded-full text-center leading-none text-accent-foreground outline-ring outline-offset-2 transition-colors cursor-pointer', {
    variants: {
      /** 現在の年月かどうか */
      current: {
        true: [
          'text-bright-foreground before:absolute before:inset-0 before:content-[""] before:bg-primary group-hover:before:bg-primary/90 hover:before:bg-primary/90 before:rounded-full group-focus-visible:outline-2 before:-z-10',
        ],
        false: [
          'before:absolute before:inset-0 before:content-[""] group-hover:before:bg-accent/90 hover:before:bg-accent/90 before:rounded-full group-focus-visible:outline-2 before:-z-10',
        ],
      },
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 text-muted-foreground pointer-events-none'],
        false: [],
      },
    },
  });

  export type CalendarVariants = VariantProps<typeof calendarVariants>;

  export interface CalendarProps extends CalendarVariants {
    /** 日付 */
    value?: string;
    /** 範囲選択できるどうか */
    enableRange?: boolean;
    /** 範囲選択の最小値 */
    start?: string;
    /** 範囲選択の最大値 */
    end?: string;
    /** 選択できる範囲の最小値 */
    min?: string;
    /** 選択できる範囲の最大値 */
    max?: string;
    /** 表示する月の数 */
    months?: number;
    /** 選択されたときのコールバック関数 */
    onChange?: (value: string) => void;
    /** 選択されたときのコールバック関数 */
    onRangeChange?: (value: CalendarRange) => void;
    /** 初期表示時に現在選択中の日付に自動でフォーカスするか */
    autoFocusDate?: boolean;
    /** クラス */
    class?: ClassValue;
  }

  export interface CalendarDate {
    /** 日付 */
    value: number;
    /** 選択されている月じゃないかどうか */
    outOfMonth?: boolean;
    /** 操作できるかどうか */
    disabled?: boolean;
    /** 選択されている日付 */
    date: Date;
    /** 選択されている日付のフォーマット */
    formattedDate: string;
  }

  export interface CalendarRange {
    /** 範囲の開始日 */
    start: string;
    /** 範囲の最終日 */
    end: string;
  }

  export type CalendarType = 'date' | 'month' | 'year';
</script>

<script lang="ts">
  import ChevronLeft from '@lucide/svelte/icons/chevron-left';
  import ChevronRight from '@lucide/svelte/icons/chevron-right';
  import { tick } from 'svelte';
  import { SvelteDate } from 'svelte/reactivity';
  import { scale } from 'svelte/transition';

  let { value = $bindable(''), enableRange = false, start = $bindable(''), end = $bindable(''), min = MIN_DATE, max = MAX_DATE, autoFocusDate = false, class: className, months = 1, onChange, onRangeChange }: CalendarProps = $props();

  let calendarElement: HTMLDivElement;
  let beforeSelectDate = $state<Date | null>(null);
  let type = $state<CalendarType>('date');

  let rangeStart = $derived(start ? parseLocalDate(start) : null);
  let rangeEnd = $derived(end ? parseLocalDate(end) : null);

  const minDate = $derived(parseLocalDate(min));
  const maxDate = $derived(parseLocalDate(max));

  let selectedDate = $derived(value ? new SvelteDate(parseLocalDate(value)) : new SvelteDate());
  const selectedValue = $derived(toDateString(selectedDate));
  let displayDate = $state(new SvelteDate());

  let calendars = $state<ReturnType<typeof createCalendar>[]>([]);

  $effect(() => {
    // すでに表示中の月範囲にdisplayDateが含まれる場合は再生成しない
    if (isInDisplayMonths()) return;
    updateCalendars();
  });

  let calendarVariantsClass = $derived(calendarVariants({ class: className }));

  const yearRange = $derived.by(() => {
    const start = Math.floor(displayDate.getFullYear() / DISPLAY_YEAR_NUM) * DISPLAY_YEAR_NUM;

    return Array.from({ length: DISPLAY_YEAR_NUM }, (_, i) => start + i);
  });

  const yearLabel = $derived(`${yearRange[0]}-${yearRange[yearRange.length - 1]}年`);

  const calendarStartDate = $derived.by(() => {
    if (!calendars.length) {
      return displayDate;
    }

    const start = calendars[0];
    return new Date(start.year, start.month, 1);
  });

  const prevDisplayDate = $derived.by(() => {
    if (type === 'year') {
      return new Date(calendarStartDate.getFullYear() - DISPLAY_YEAR_NUM, 11, 31);
    }
    else if (type === 'month') {
      return new Date(calendarStartDate.getFullYear() - 1, 11, 31);
    }
    else {
      return new Date(calendarStartDate.getFullYear(), calendarStartDate.getMonth(), 0);
    }
  });

  const nextDisplayDate = $derived.by(() => {
    if (type === 'year') {
      return new Date(calendarStartDate.getFullYear() + DISPLAY_YEAR_NUM, 0, 1);
    }
    else if (type === 'month') {
      return new Date(calendarStartDate.getFullYear() + 1, 0, 1);
    }
    else {
      return new Date(calendarStartDate.getFullYear(), calendarStartDate.getMonth() + 1, 1);
    }
  });

  const prevDisabled = $derived(!isDateInRange(prevDisplayDate));
  const nextDisabled = $derived(!isDateInRange(nextDisplayDate));

  $effect(() => {
    if (!isValidDateString(min)) min = MIN_DATE;
    if (!isValidDateString(max)) max = MAX_DATE;
    if (new Date(min) > new Date(max)) {
      console.warn(`minRange (${min}) は maxRange (${max}) より前の日付である必要があります。`);
    }
  });

  $effect(() => {
    if (enableRange) {
      if (!value) {
        value = end || start;
      }
    }
  });

  $effect(() => {
    if (!autoFocusDate) return;
    if (calendarElement.contains(document.activeElement)) return;
    const date_to_focus = value ? value : toDateString(new Date());
    focusByDate(date_to_focus);
  });

  $effect(() => {
    if (value && !isValidDateString(value)) {
      console.warn(`value (${value}) は有効な日付ではありません。`);
      value = '';
    }
    setDisplayDate(value ? parseLocalDate(value) : min !== MIN_DATE ? parseLocalDate(min) : new Date());
  });

  /** 指定した日付の年月を先頭に、months 分のカレンダーを生成 */
  function updateCalendars() {
    calendars = Array.from({ length: months }, (_, i) => {
      const date = new Date(displayDate.getFullYear(), displayDate.getMonth() + i);
      return createCalendar(date.getFullYear(), date.getMonth());
    });
  }

  function createCalendar(year: number, month: number) {
    const first = new Date(year, month, 1);
    const start = new Date(first);
    start.setDate(start.getDate() - first.getDay());

    const weeks: CalendarDate[][] = [];

    for (let w = 0; w < 6; w++) {
      const week: CalendarDate[] = [];
      for (let d = 0; d < 7; d++) {
        const date = new Date(start);
        date.setDate(start.getDate() + w * 7 + d);

        const formatted = toDateString(date);
        const is_out_of_range = date < minDate || date > maxDate;

        week.push({
          value: date.getDate(),
          date,
          formattedDate: formatted,
          outOfMonth: date.getMonth() !== month,
          disabled: is_out_of_range,
        });
      }
      weeks.push(week);
    }

    return { year, month, weeks };
  }

  function isValidDateString(str: string): boolean {
    const date = new Date(str);
    return !Number.isNaN(date.getTime());
  }

  function toDateString(date: Date): string {
    const y = date.getFullYear();
    const m = String(date.getMonth() + 1).padStart(2, '0');
    const d = String(date.getDate()).padStart(2, '0');
    return `${y}-${m}-${d}`;
  }

  function isInDisplayMonths(): boolean {
    if (!calendars.length) return false;

    const start = calendars[0];
    const end = calendars[calendars.length - 1];
    const target = {
      year: displayDate.getFullYear(),
      month: displayDate.getMonth(),
    };

    function format_month({ year, month }): number {
      return year * 12 + month;
    }

    return format_month(start) <= format_month(target) && format_month(target) <= format_month(end);
  }

  function isSelected(day: CalendarDate) {
    if (!value) return false;
    const target = day.formattedDate;
    if (enableRange) {
      const is_start = rangeStart ? target === toDateString(rangeStart) : false;
      const is_end = rangeEnd ? target === toDateString(rangeEnd) : false;
      return is_start || is_end;
    }
    return day.formattedDate === selectedValue;
  }

  function yearDisabled(year: number) {
    const start = new Date(year, 0, 1);
    const end = new Date(year, 11, 31);

    return end < minDate || start > maxDate;
  }

  function monthDisabled(month: number) {
    const first_day = new Date(displayDate.getFullYear(), month, 1);
    const last_day = new Date(displayDate.getFullYear(), month + 1, 0);

    return last_day < minDate || first_day > maxDate;
  }

  function setDisplayDate(date: Date) {
    displayDate = new SvelteDate(date.getFullYear(), date.getMonth(), date.getDate());
  }

  /** 表示タイプと日付を更新 */
  function updateDisplay(nextType: CalendarType, date: Date) {
    type = nextType;
    setDisplayDate(date);
  }

  async function focusByDate(date: Date | string) {
    type = 'date';
    await tick();

    if (typeof date !== 'string') {
      date = toDateString(date);
    }

    const el = calendarElement?.querySelector(`[data-date="${date}"][data-outofmonth="false"]`);
    if (el instanceof HTMLElement) {
      el.focus({ preventScroll: true });
    }
  }

  /** 1つ前の表示期間へ移動し、日付表示ではカレンダーを即時再生成 */
  function prevCalendar() {
    if (prevDisabled) return;
    setDisplayDate(prevDisplayDate);
    updateCalendars();
  }

  /** 1つ次の表示期間へ移動し、日付表示ではカレンダーを即時再生成 */
  function nextCalendar() {
    if (nextDisabled) return;
    setDisplayDate(nextDisplayDate);
    updateCalendars();
  }

  function isDateInRange(date: Date): boolean {
    return date >= minDate && date <= maxDate;
  }

  function parseLocalDate(str: string): Date {
    const [y, m, d] = str.split('-').map(Number);
    return new Date(y, m - 1, d);
  }

  function setValue(date: Date | string) {
    if (typeof date === 'string') {
      value = date;
    }
    else {
      value = toDateString(date);
    }
    onChange?.(value);
  }

  function setRangeValue(day: CalendarDate) {
    const selected_range_value = day.date;
    value = day.formattedDate;

    if (rangeStart && !rangeEnd) {
      rangeEnd = new Date(selected_range_value);
      if (rangeStart > rangeEnd) [rangeStart, rangeEnd] = [rangeEnd, rangeStart];
    }
    else {
      rangeStart = new Date(selected_range_value);
      rangeEnd = null;
    }

    start = toDateString(rangeStart);
    end = rangeEnd === null ? '' : toDateString(rangeEnd);

    onChange?.(value);
    onRangeChange?.({ start, end });
  }

  function isBeforeSelectDate(day: CalendarDate) {
    if (!rangeStart || rangeEnd || !beforeSelectDate) return false;
    return day.date.getTime() === beforeSelectDate.getTime();
  }

  function isInRangeArea(day: CalendarDate): boolean {
    const target = day.date.getTime();
    let range_start = 0;
    let range_end = 0;
    if (rangeStart && rangeEnd) {
      range_start = rangeStart.getTime();
      range_end = rangeEnd.getTime();
    }
    if (rangeStart && beforeSelectDate && !rangeEnd) {
      range_start = rangeStart.getTime();
      range_end = beforeSelectDate.getTime();
    }

    if (range_start > range_end) [range_start, range_end] = [range_end, range_start];
    return target > range_start && target < range_end;
  }

  function getRoundedClass(day: CalendarDate): string {
    if (day.outOfMonth) return '!bg-transparent';
    if (!enableRange && isSelected(day)) return 'hover:before:!rounded-md';

    const date = day.date;
    const date_time = date.getTime();

    if (!enableRange) return isSelected(day) ? 'rounded-md' : '';

    const start_date = rangeStart ?? null;
    const end_like = rangeEnd ?? beforeSelectDate ?? rangeStart ?? null;
    if (!start_date || !end_like) return '';

    let start_ms = start_date.getTime();
    let end_ms = end_like.getTime();
    if (start_ms > end_ms) [start_ms, end_ms] = [end_ms, start_ms];

    const in_inc = (ms: number) => ms >= start_ms && ms <= end_ms;

    if (!in_inc(date_time)) return '';

    // 基本背景(before)
    const base =
      'before:content-[""] before:absolute before:inset-y-0.5 before:inset-x-0 before:size-9 before:!bg-primary/10 before:-z-50';

    // 隣接日の情報
    const prev = new Date(date);
    prev.setDate(prev.getDate() - 1);
    const next = new Date(date);
    next.setDate(next.getDate() + 1);
    const prev_in = in_inc(prev.getTime());
    const next_in = in_inc(next.getTime());

    // 週端 or 月またぎ(行が切れる箇所)は角丸
    const prev_breaks_week = date.getDay() === 0; // 日曜: 左端
    const next_breaks_week = date.getDay() === 6; // 土曜: 右端
    const prev_breaks_month = prev.getMonth() !== date.getMonth() || prev.getFullYear() !== date.getFullYear();
    const next_breaks_month = next.getMonth() !== date.getMonth() || next.getFullYear() !== date.getFullYear();

    const round_left = !prev_in || prev_breaks_week || prev_breaks_month;
    const round_right = !next_in || next_breaks_week || next_breaks_month;

    // 角丸の決定
    if (round_left && round_right) return `${base} before:rounded-md`;
    if (round_left) return `${base} before:!rounded-l-md`;
    if (round_right) return `${base} before:!rounded-r-md`;

    return '';
  }

  async function selectDate(day: CalendarDate) {
    if (day.disabled) return;

    if (enableRange) {
      setRangeValue(day);
      focusByDate(day.formattedDate);
    }
    else {
      setValue(day.formattedDate);
      focusByDate(day.formattedDate);
    }
  }

  function isToday(day: CalendarDate) {
    return day.formattedDate === toDateString(new Date());
  }

  /** 表示してる年を変更 */
  function updateDisplayYear(year: number) {
    if (yearDisabled(year)) return;
    displayDate.setFullYear(year);
    updateCalendars();
  }

  function changeDisplayYear(year: number) {
    updateDisplayYear(year);
    type = 'date';
  }

  /** 表示してる月を変更 */
  function updateDisplayMonth(month: number) {
    if (monthDisabled(month)) return;
    displayDate.setMonth(month);
    updateCalendars();
  }

  function changeDisplayMonth(month: number) {
    updateDisplayMonth(month);
    type = 'date';
  }

  async function handleKeyDownDay(event: KeyboardEvent, day: CalendarDate) {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      selectDate(day);
      return;
    }

    const direction_map = {
      ArrowLeft: -1,
      ArrowRight: 1,
      ArrowUp: -7,
      ArrowDown: 7,
    };

    const direction = direction_map[event.key] ?? 0;
    if (direction === 0) return;
    const date = new Date(day.date);
    date.setDate(date.getDate() + direction);

    if (!isDateInRange(date)) return;

    event.preventDefault();
    beforeSelectDate = date;
    setDisplayDate(date);
    focusByDate(date);
  }

  async function handleKeyDownMonth(event: KeyboardEvent, m: number) {
    if (monthDisabled(m)) return;
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      changeDisplayMonth(m);
      return;
    }
    const directionMap = {
      ArrowLeft: -1,
      ArrowRight: 1,
      ArrowUp: -MONTHS_PER_ROW,
      ArrowDown: MONTHS_PER_ROW,
    };
    const direction = directionMap[event.key] ?? 0;
    if (direction === 0) return;

    const new_month = m + direction;
    if (monthDisabled(new_month)) return;

    event.preventDefault();
    updateDisplayMonth(new_month);

    await tick();
    const el = calendarElement.querySelector(`[data-month="${displayDate.getMonth()}"]`);
    if (el instanceof HTMLElement) el.focus({ preventScroll: true });
  }

  async function handleKeyDownYear(event: KeyboardEvent, y: number) {
    if (yearDisabled(y)) return;
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      changeDisplayYear(y);
      return;
    }

    const direction_map = {
      ArrowLeft: -1,
      ArrowRight: 1,
      ArrowUp: -YEARS_PER_ROW,
      ArrowDown: YEARS_PER_ROW,
    };
    const direction = direction_map[event.key] ?? 0;
    if (direction === 0) return;

    const new_year = y + direction;
    if (yearDisabled(new_year)) return;
    event.preventDefault();
    updateDisplayYear(new_year);

    await tick();
    const el = calendarElement.querySelector(`[data-year="${displayDate.getFullYear()}"]`);
    if (el instanceof HTMLElement) el.focus({ preventScroll: true });
  }

  /** 年表示モードに切り替え */
  function handleSwitchToYear() {
    type = 'year';
  }

  /** 日付セルにフォーカスしたときの処理 */
  function handleDayFocus(day: CalendarDate) {
    beforeSelectDate = new Date(day.date);
  }

  /** 日付セルからフォーカスが外れたときの処理 */
  function handleDayBlur() {
    beforeSelectDate = null;
  }

  /** 日付セルにマウスが乗ったときの処理 */
  function handleDayMouseEnter(day: CalendarDate) {
    beforeSelectDate = new Date(day.date);
  }

  /** 日付セルからマウスが離れたときの処理 */
  function handleDayMouseLeave() {
    beforeSelectDate = null;
  }

  /** 日付セルの props をまとめて返す */
  function getDayProps(day: CalendarDate) {
    return {
      class: [
        calendarDateVariants({
          select: (!day.outOfMonth && isSelected(day)) || isBeforeSelectDate(day),
          current: isToday(day),
          outOfMonth: day.outOfMonth,
          disabled: day.disabled,
          range: !day.outOfMonth && enableRange && isInRangeArea(day),
        }),
        getRoundedClass(day),
      ],
      tabindex: day.disabled ? -1 : 0,
      role: 'button',
      'data-date': day.formattedDate,
      'data-outofmonth': day.outOfMonth,
      onclick: () => selectDate(day),
      onkeydown: (e: KeyboardEvent) => handleKeyDownDay(e, day),
      onfocus: () => handleDayFocus(day),
      onblur: handleDayBlur,
      onmouseenter: () => handleDayMouseEnter(day),
      onmouseleave: handleDayMouseLeave,
    };
  }
</script>

<div class={calendarVariantsClass} bind:this={calendarElement} transition:scale={{ start: 0.98, opacity: 0, duration: 100 }} data-rabee-ui="calendar">
  <div class="flex items-center justify-between">
    <button class={calendarArrowButtonVariants({ disabled: prevDisabled })} onclick={prevCalendar} disabled={prevDisabled} type="button">
      <ChevronLeft size="1rem" />
    </button>
    <div class="flex items-center justify-around w-full gap-9">
      {#if type === 'date'}
        {#each calendars as calendar}
          {@const year = calendar.year}
          {@const month = calendar.month}
          {@const year_disabled = yearDisabled(year)}
          {@const month_disabled = monthDisabled(month)}

          <div class="flex items-center gap-0.5 text-sm">
            <button class={calendarMonthYearButtonVariants({ disabled: year_disabled })} onclick={() => updateDisplay('year', new Date(year, month, 1))} disabled={year_disabled}>
              {year}年
            </button>
            <button class={calendarMonthYearButtonVariants({ disabled: month_disabled })} onclick={() => updateDisplay('month', new Date(year, month, 1))} disabled={month_disabled}>
              {month + 1}月
            </button>
          </div>
        {/each}
      {:else if type === 'month'}
        {@const year = displayDate.getFullYear()}
        {@const disabled = yearDisabled(year)}
        <button class={calendarMonthYearButtonVariants({ disabled })} onclick={handleSwitchToYear} {disabled}>
          {year}年
        </button>
      {:else}
        <p>{yearLabel}</p>
      {/if}
    </div>

    <button class={calendarArrowButtonVariants({ disabled: nextDisabled })} onclick={nextCalendar} disabled={nextDisabled} type="button">
      <ChevronRight size="1rem" />
    </button>
  </div>

  <div class="min-h-68.25">
    <div class="flex gap-2" class:hidden={type !== 'date'}>
      {#each Array(months) as _}
        <div class="flex">
          {#each WEEK_DAYS as week}
            <div class={calendarDayOfWeekVariants()}>{week}</div>
          {/each}
        </div>
      {/each}
    </div>
    <div>
      {#if type === 'date'}
        <div class="flex gap-2">
          {#each calendars as calendar}
            <div>
              {#each calendar.weeks as week}
                <div class="flex text-center">
                  {#each week as day}
                    <div class="py-0.25">
                      <div {...getDayProps(day)}>
                        {day.value}
                      </div>
                    </div>
                  {/each}
                </div>
              {/each}
            </div>
          {/each}
        </div>
      {:else}
        <div class="grid grid-cols-3 col-span-4 row-span-3 place-items-center min-h-68.25">
          {#if type === 'month'}
            {#each Array(DISPLAY_MONTH_NUM) as _, m}
              {@const isDisabled = monthDisabled(m)}
              <div class={['px-1.5 py-2', !isDisabled && 'group cursor-pointer focus-visible:!outline-none']} tabindex={isDisabled ? -1 : 0} role="button" data-month={m} onclick={() => changeDisplayMonth(m)} onkeydown={(e) => handleKeyDownMonth(e, m)}>
                <p class={calendarMonthYearVariants({ current: selectedDate.getFullYear() === displayDate.getFullYear() && selectedDate.getMonth() === m, disabled: isDisabled })}>
                  {m + 1}月
                </p>
              </div>
            {/each}
          {:else if type === 'year'}
            {#each yearRange as y}
              {@const year_disabled = yearDisabled(y)}
              <div class={['px-1.5 py-2', !year_disabled && 'group cursor-pointer focus-visible:!outline-none']} tabindex={year_disabled ? -1 : 0} role="button" data-year={y} onclick={() => changeDisplayYear(y)} onkeydown={(e) => handleKeyDownYear(e, y)}>
                <p class={calendarMonthYearVariants({ current: selectedDate.getFullYear() === y, disabled: year_disabled })}>
                  {y}
                </p>
              </div>
            {/each}
          {/if}
        </div>
      {/if}
    </div>
  </div>
</div>

      

使い方


サンプル

Default

日付を表示・選択できます。

Min・Max

選択可能な範囲を指定できます。

Date

指定の日付を渡すことができます。

Range

日付を範囲選択することができます。

Month

表示する月数を指定することができます。

OnChange

コールバック関数で値を受け取ることができます。

With Input

InputDateとも組み合わせることができます。