Calendar
먼저 보고, 그다음 계약을 읽습니다
단일 날짜 선택 그리드(월 단위) — 외부 dep 0, 순수 날짜 산술 + role="grid".
Component intent의도와 경계 읽기
한 달을 네이티브 <button> 셀 그리드로 그려 날짜 하나(value: Date | null)를 고르는 controlled 컴포넌트. Radix·date 라이브러리 없이 순수 날짜 산술로 동작하고, role="grid"/"row"/"gridcell" + 셀별 aria-selected 로 접근성을 표현한다(서버가 같은 markup 을 계산해 넣으면 maud/jinja 소비자도 동일 모양). weekStartsOn 으로 주 시작 요일을, minDate/maxDate/isDateDisabled 로 선택 범위를 제한한다. 경계 — 단일 날짜 선택만 한다: 기간(range) 선택·시각(시·분) 선택·input 옆 팝오버 DatePicker 합성은 범위 밖(이 컴포넌트를 내부에 합성해 별도 제공).
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { Calendar } from "@axe/ui";import { useState } from "react";function DueDatePicker() { const [due, setDue] = useState<Date | null>(null); return <Calendar value={due} onValueChange={setDue} />;}import { Calendar } from "@axe/ui";<Calendar value={due} onValueChange={setDue} weekStartsOn={1} minDate={new Date(2026, 0, 1)} maxDate={new Date(2026, 11, 31)} isDateDisabled={(d) => d.getDay() === 0}/><div class="axe-calendar"> <div class="axe-calendar__header"> <button type="button" class="axe-calendar__nav" aria-label="이전 달">‹</button> <div class="axe-calendar__title" aria-live="polite">2026년 5월</div> <button type="button" class="axe-calendar__nav" aria-label="다음 달">›</button> </div> <div class="axe-calendar__grid" role="grid" aria-label="달력"> <div class="axe-calendar__weekdays" role="row"> <div class="axe-calendar__weekday" role="columnheader" aria-label="일">일</div> <!-- 월~토 columnheader 반복 --> </div> <div class="axe-calendar__week" role="row"> <div class="axe-calendar__day axe-calendar__day--outside" role="gridcell" aria-disabled="true"></div> <div role="gridcell" aria-selected="true"> <button type="button" class="axe-calendar__day axe-calendar__day--selected" aria-label="14일, 2026년 7월 화요일" tabindex="0">14</button> </div> <div role="gridcell" aria-selected="false"> <button type="button" class="axe-calendar__day axe-calendar__day--today" aria-current="date" tabindex="-1">15</button> </div> <!-- 나머지 날짜 gridcell 반복 --> </div> </div></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
value | Date | null | 필수 | — | 선택된 날짜 또는 미선택(null) — controlled. |
onValueChange | (value: Date) => void | 필수 | — | 날짜 셀 클릭 시 그 Date 로 호출. |
weekStartsOn | Weekday | — | 0 | 주 시작 요일(0=일 … 6=토). 요일 헤더와 리드 빈칸 계산에 반영. |
minDate | Date | — | — | 선택 가능한 최소 날짜(포함). 이전 날짜 셀은 disabled. |
maxDate | Date | — | — | 선택 가능한 최대 날짜(포함). 이후 날짜 셀은 disabled. |
isDateDisabled | (date: Date) => boolean | — | — | 개별 날짜 비활성 술어(min/max 위에 추가 적용). 예: 일요일 비활성. |
weekdayLabels | [string, string, string, string, string, string, string] | — | ["일","월","화","수","목","금","토"] | 요일 라벨(일요일 기준 7개). weekStartsOn 만큼 회전되어 헤더에 표시. |
formatMonth | (year: number, month: number) => string | — | (y, m) => `${y}년 ${m + 1}월` | 헤더 월 표제 포맷(month 는 0-based). |
disabled | boolean | — | false | 전체 비활성 — 모든 셀·내비 버튼 disabled + --disabled 클래스. |
ariaLabel | string | — | "달력" | role="grid" 요소의 aria-label. |
...rest | Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> | — | — | className 병합, 나머지는 루트 div 로 전파. ref 는 루트 div 로 forward. |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-calendar | stable | 루트 div. |
.axe-calendar--disabled | stable | 전체 비활성 상태. |
.axe-calendar__header | stable | 이전/제목/다음 3열 헤더. |
.axe-calendar__title | stable | 월 표제(aria-live="polite"). |
.axe-calendar__nav | stable | 이전/다음 달 버튼. |
.axe-calendar__grid | stable | role="grid" 날짜 그리드 컨테이너. |
.axe-calendar__weekdays | stable | 요일 헤더 row(role="row"). |
.axe-calendar__weekday | stable | 요일 셀(role="columnheader"). |
.axe-calendar__week | stable | 한 주 row(role="row", 7칸). |
.axe-calendar__day | stable | 날짜 셀 <button>. |
.axe-calendar__day--outside | stable | 이달 밖 빈칸(격자 정렬용, aria-disabled). |
.axe-calendar__day--selected | stable | 선택된 날짜(accent 채움). |
.axe-calendar__day--today | stable | 오늘 표시(테두리/링). |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- ←/→는 하루, ↑/↓는 한 주 단위로 roving focus를 이동한다.
- Home/End는 현재 주의 첫날/마지막 날로 이동한다.
- PageUp/PageDown은 같은 날짜를 기준으로 이전/다음 달로 이동한다.
- Enter/Space는 포커스된 날짜를 선택한다.
그리드는 role="grid" + aria-label. 요일 헤더 줄 role="row" 안에 각 요일 role="columnheader"(aria-label). 각 주 role="row", 셀 래퍼 role="gridcell" + 선택 셀에 aria-selected. 날짜 <button>의 접근성 이름은 보이는 일자 숫자를 포함한 전체 연월일·요일이고, 오늘이면 aria-current="date". 이전/다음 버튼은 aria-label "이전 달"/"다음 달", 월 표제는 aria-live="polite" 로 이동 시 낭독. 이달 밖 빈칸은 aria-disabled="true".
날짜 버튼은 roving tabindex로 한 셀만 Tab 순서에 놓인다. 키보드 이동이 다른 달을 넘으면 월 표제와 그리드를 함께 갱신하고 새 날짜로 포커스를 복원한다. min/max/isDateDisabled/disabled로 막힌 날짜는 건너뛰며 native disabled도 유지한다. prefers-reduced-motion은 CSS에서 처리한다.