@aiquants/period-slider
v0.12.0
Published
Customizable period slider and year/month/date selectors for React
Readme
@aiquants/period-slider
時系列分析・期間指定 UI のための React スライダーおよび日付セレクタコンポーネントライブラリ。
概要
@aiquants/period-slider は、年・月・日単位での期間範囲選択 (Range) および単一時点選択 (Point) に対応した PeriodSlider、および年・年月・年月日のステッパー付き入力コンポーネント (YearSelector, MonthSelector, DateSelector) を提供します。
外部依存ライブラリ (nouislider 等) を排した純粋な React / SVG 実装であり、滑らかなドラッグ操作、数学的幾何計算による近接ラベルの重なり防止自動斜めエフェクト、上限/下限交差時のアクティブハイライト自動追従、および WAI-ARIA に準拠したキーボードアクセシビリティを備えています。
必須の入力 (定義域境界・初期値) が欠落または不正な場合、本パッケージは既定値や壁時計時刻で代替せず、すべて RangeError で即座に失敗します。詳細は 実行時バリデーション を参照してください。
組み込みの表示文字列はロケール中立です。アクセシブル名の既定値はすべて英語 ("Range start" 等)、日付ラベル・選択肢テキストの既定値はロケール非依存の ISO 8601 表記 (2025 / 2025-01 / 2025-01-15) で、locale (BCP 47 言語タグ) を渡した時のみ Intl.DateTimeFormat 書式へ切り替わります。詳細は ロケールと表示書式 を参照してください。
特徴
- 柔軟な時間単位:
year,month,dayの 3 つの単位をサポート。 - 暦年以外の年 (
yearStartMonth): 1 年の始まりを任意の暦月へ置けます (3なら 4 月〜翌 3 月の会計年度、9なら 10 月〜翌 9 月のでん粉年度)。年はその年の先頭月が属する暦年で名乗り、レールの目盛・ノギスのマクロ窓・年解像度の離散候補がまとめて同じ境界に従います。四半期はresolveQuarter/formatQuarterLabelが暦四半期・年度四半期の双方を返します (スライダーの Props とは独立した引数で選びます)。 - 3 つの操作モード:
range: 開始時期と終了時期の 2 つのつまみと連結バーで範囲を選択。point: 単一のつまみで時点を選択。vernier: ノギス (Vernier Caliper) 型 2 段スケール一体型モード。全体期間からマクロ表示窓を上側で指定し、その拡大スケールを下側で精密選択。上下反転、台形プロジェクション光、2 段スケールの目盛線、精密ヘアライン針を完備。- ミクロレールは期間と時点の 2 形態:
microRange/defaultMicroRange/onChangeMicroRangeを渡せば 2 つまみの期間選択、microValue/defaultMicroValue/onChangeMicroPointを渡せば 1 つまみの時点選択になります。2 系統の同時指定は型で禁じられ、実行時もRangeErrorで拒否します。時点形態ではミクロ側の接続バーを描かず、つまみはrole="slider"が 1 つだけ立ちます (全体で 3 つ)。 - 半円直径 (±8px) 完全充填幾何配置: 半円ノブ底面 (16px) を完全に満たす一体型選択ゲージ。
- 上下段つまみ交差反転スワップ (Crossover): マクロ・ミクロ双方で上限下限の追い越し反転をサポート。
- 弾性復元 (Push-and-Restore): マクロ縮小時の一時クランプ後、拡大時に記憶された初期ミクロ選択範囲へ自動復元。
- マクロ・ミクロ完全独立設定: マクロ (年) × ミクロ (日) やマクロ (年) × ミクロ (月) などの異単位構成、個別離散スナップ (
macroIsDiscrete/microIsDiscreteと併用)、個別カスタムフォーマッタに対応。 - 中央 1 px スリット空隙: 上段 3.5 px、中央空隙 1.0 px、下段 3.5 px (
calc(50% - 0.5px)) の独立レール設計による高い視認性と数学的均等分割。 - SVG ベクター幾何学半円ドーム (
VernierKnobSvg): 数学的真円弧コマンド (shapeRendering="geometricPrecision") による、環境非依存で一切のガタつき・階段状ジャギーのない滑らかな真円弧。 - 上下反転時のセマンティック配色直交分離: 配置位置 (
data-vernier-pos) と配色 (data-handle-type) を CSS セレクタレベルで直交分離し、反転時もマクロ針・ノブグロー・目盛線・台形プロジェクション光が完璧に追従。
- ミクロレールは期間と時点の 2 形態:
- トラック厚み 8px 偶数設計: サブピクセル滲みを完全に解消し、上下対称の半円つまみ配置と 3.5px+1.0px+3.5px マクロ・ミクロ塗り分けを実現。
- 離散日付対応 (
allowedDates): 実データが存在する日付のみをスナップ対象にする離散モードを内蔵。[minDate, maxDate]外の要素は到達不能なため除外され、全要素が範囲外の場合はRangeErrorで失敗。 - ラベル配置と重なり防止自動傾斜:
labelPosition: 上側 ("top", 既定) または下側 ("bottom") のラベル表示位置を選択可能。autoSlantLabels: 2 つのラベルが近接した際に Smootherstep 補間により 0° 〜 90° に滑らかに自動傾斜し、テキストの重なりを数学的に完全防止。回転時もラベル下端がトラック中心から常に 10px (つまみ上端から 2px) の統一された高さを維持。角度は 2 つの値が一致する位置まで連続で、その位置で 90° に達します。- 傾斜は Range の 1 対と、ノギスのマクロ対・ミクロ対の計 3 組へ対ごとに独立して適用されます (対ごとに比率も単位もラベル幅も違うため、計算を共有しません)。マクロ対とミクロ対で角度が異なるのは正常です。
- 垂直レイアウト (
isVertical={true}、垂直ノギスを含む) では傾斜しません。傾斜幾何が X 軸方向にしか分離しないためで、垂直向けの衝突回避は未整備の保留領域です。 - 衝突判定はラベルの実測幅 (
ResizeObserverが返すボーダーボックス幅) のみを用います。単位や書式から推定した定数を使わないため、locale・formatLabel・ホストのフォントを変えても判定はずれません。実測値が得られるまでラベルは傾斜せず直立します。 - 2 つのつまみが同じ値に重なったときは、同一文字列のラベルを 2 枚並べず 1 枚へ畳みます。冗長な側のラベルには
data-duplicate="true"が付いてvisibility: hiddenになり (要素は残るため実測は継続)、残った 1 枚は 90° に傾いたままつまみの中心に立ちます (畳んでも角度は変えず、離す相手が居なくなった水平方向の押し退けだけを 0 にします)。畳まれたかどうかは傾斜の可否を動かしません。値が一致していても、上記のとおり実測が揃うまで・autoSlantLabels={false}・垂直レイアウトでは直立のままです。同じ機構がノギスのマクロ対・ミクロ対にも適用され (判定は各対の符号化値の厳密一致)、可視ラベルは掴んだ・打鍵した・Tabで到達したつまみへ追従します。 - トラック上をホバーしたときに出る追従ラベル (ポインターオーバーレイ) も、指している値がつまみの確定値と一致する間は同じ機構で隠します。つまみのラベルと一字一句同じ文字列が 4px ずれて二重に描かれるのを防ぐためで、判定はラベル併合と同じ符号化値の厳密一致です。ポインター位置そのものを示す点とガイドは表示したままです。
- 操作中 (ドラッグ中)、直近にクリック・キー操作・フォーカスしたラベル・つまみを前面 (
z-index) に強調表示。併合時に見えるのはこの前面側のラベルです。
- ドラッグは指に付いて来ます (目盛りの粗さに関わらず): つまみ・帯・ラベル・針・ノギスの台形プロジェクションは、押している間はポインターそのものの位置に描かれます。年スケールのように 1 目盛りが数十ピクセルあるレールでも、つまみが固まってから跳ぶことはありません。コールバックの発火値と回数、
debounceMsは 1 ビットも変わりません。指を離すと 120ms の短い収め戻しで確定位置へ座ります。キーボード・ホイール・ステッパーによる変化は決して動きません — 収め戻しの窓が開いている最中に起きた変化でも、値を動かす前に窓を閉じます (prefers-reduced-motion: reduceでは収め戻しも動きません)。専用の Props はありません。 - 掴んでいる間に表示される値は、描かれている位置が名指す値です: ラベルの文字・
aria-valuetext/aria-valuenowは、指の下に描かれているつまみが立っている値を示します (位置は連続、値は離散)。debounceMsを渡した制御下の利用側でも、通知が届くまで文字が古い値で固まることはありません。指を離した瞬間に表示の持ち主は利用側へ戻るため、届いた値を畳む・採らない利用側ではその判断が即座に表示へ現れます。キーボード・ホイール・ステッパーは表示の持ち主にならず、利用側が値を返したときにだけ表示が動きます。 - 上限/下限交差 (クロスオーバー) の動的ハイライト追従:
- レンジドラッグ中につまみが相手側アンカーを超えて上限と下限が入れ替わった際、現在ポインターで操作している側のつまみへアクティブ状態 (
[data-active="true"]、スケール縮小+ブルーグロー・波紋) がリアルタイムに自動スワップ。
- レンジドラッグ中につまみが相手側アンカーを超えて上限と下限が入れ替わった際、現在ポインターで操作している側のつまみへアクティブ状態 (
- 充実した日付セレクタ:
YearSelector: ドロップダウンと ◀▶ ステッパーで年を選択。MonthSelector: カレンダー入力 (variant="date", 既定) / ドロップダウン (variant="dropdown") 対応の月セレクタ。連続した境界のほか、実データに存在する月だけを選ばせる離散集合 (allowedMonths) に対応。DateSelector: 日付入力 (variant="date", 既定)、日付範囲全体を仮想化したコンボボックス (variant="dropdown"、100 年分の日でも定数費用)、年月日の 3 分割セレクト (variant="split-dropdown") に対応。離散集合 (allowedDates) にも対応。- 3 セレクタの ◀▶ とスライダーのステッパーは同じ部品を共有し、押せる間だけホバーで強調され、44px の当たり判定とキーボードフォーカスのリングを持ちます。境界に着いたボタンはフォーカスを保ち、タブ順にも残ったまま使用不可 (
aria-disabled="true") になります。どのボタンもtype="button"のため、ホストの<form>の中に置いても押下でフォームを送信しません。
- カラーテーマ・メリハリ対比 & ダークモード:
colorScheme: 6 系統のカラーパレット ("blue-rose"[既定],"indigo-amber","teal-emerald","violet-fuchsia","slate-sky","monochrome") をサポート。- ノギス 2 段スケールにおける広域マクロ (青系) と確定ミクロ (赤系) の明確なメリハリ対比、および境界を結ぶ台形プロジェクション光の動的 2 色グラデーション。
- トラック、ノブ、目盛線、ツールチップ、セレクタ群 (
YearSelector,MonthSelector,DateSelector) を含む Tailwind.darkクラスによるダークモード完全対応。
- CSS カスタムプロパティによるスタイリング API: 配色 11 個に加え、重なり順 (z-index) 9 個、インジケーターラベルの配色・文字寸法・余白・角丸 7 個とノギスのレール離隔 1 個の計 28 変数を
--aqps-接頭辞で公開。対応する Props は持たず、自前の CSS かstyleプロップで上書きします。既定値は現在の描画結果そのもののため、何も与えなければ見た目は一切変わりません。 - 垂直レイアウト対応 (
isVertical):isVertical={true}によりダッシュボードのサイドバーや狭小パネルに最適な垂直指向スライダーを提供。- つまみのラベルはつまみの左側、ホバーガイドのラベルはトラックの右側へ排他配置し、垂直時の重なりを構造的に回避。つまみ左端からラベル右端までの実測離隔は 16px です (スタイルシートの 1 つの宣言
right: calc(100% + var(--ps-vertical-label-gap))が決めます。--ps-vertical-label-gapは内部変数であり、上書きしてよい公開の CSS カスタムプロパティではありません — 名前も値も予告なく変わります)。
- ノギスの均一 8px クリアランス & ツールチップ色分け:
- ノギス (Vernier) モードでは、上下左右いずれのレールでも半円つまみの平面からラベルまでの距離を 8px に統一 (
--aqps-label-rail-gapで変更可)。 - ノギスモードではマクロツールチップ (
--aqps-macro-color) とミクロツールチップ (--aqps-micro-color) がセマンティックに色分けされ、上下段の対応関係が一目で判別可能。
- ノギス (Vernier) モードでは、上下左右いずれのレールでも半円つまみの平面からラベルまでの距離を 8px に統一 (
- ロケール中立な表示文字列: 組み込み文字列に特定の言語を持ちません。アクセシブル名の既定値は英語、日付表記の既定値は ISO 8601 で、
localeを渡せばIntl.DateTimeFormat書式、フォーマッタ Props を渡せば任意書式になります。<input>のvalueと<option value>は常に ISO 8601 (または生の数値) で、localeの影響を受けません。 - 高いアクセシビリティ: WAI-ARIA
sliderロール、キーボード操作 (矢印キー, PageUp/Down, Home/End) 完全準拠 (Vernier ノギスハンドル含む)。 Shift+ ホイールによる平行移動: スライダーの範囲領域 (レールの行・つまみとラベルが描かれる帯 — レール方向にもラベルの外縁まで — ・ステッパー) のどこでもShiftを押しながらホイールを回すと、選択がステッパーと同じ ±1 単位だけ平行移動します。インジケーターの真上である必要はありません。Shift無しの素のホイールはページのスクロールのままで、ページのスクロールを奪うのはShift付きのホイールが使えるデルタを運んでいるときだけです (選択が端に着いていて結果的に何も動かない場合も、操作は範囲領域へ向けられているため奪います)。縦向きレールではレールの正方向が上のため符号が反転します。ノギスではポインターがトラック中心線のどちら側にあるかで段 (マクロレールならマクロ窓、ミクロレールならミクロ選択) が決まり、ステッパーの上ではクリックと同じミクロ選択が動きます。- レールが描く位置だけを名乗り、そこだけを確定させる: 離散ミクロレール (
microIsDiscrete) は候補集合しか描かないため、つまみの位置・読み上げ値・ラベル・そして確定値がすべて同じ 1 規則 (候補があれば最近傍候補、無ければ窓へのクランプ) を読みます。つまみが描かれている場所と利用側が受け取る値が食い違いません。 - マクロ窓の両境界は対: ノギスのミクロレールは、マクロつまみが名乗る期間をちょうど覆います。
macroRangeに期間の途中を指す日付を渡しても、開始側は先頭マクロ期間の先頭へ (getMacroWindowStartDate)、終了側は末尾マクロ期間の末日へ (getMacroWindowEndDate) 揃います (例:2026-09-15〜2026-10-15は2026-09-01〜2026-10-31)。丸めた窓が[minDate, maxDate]をはみ出す場合は定義域で切り落とされ、この交差はresolveMacroWindowが一手に引き受けます — レール・フックの収め込み・ミクロセレクタが同じ窓を読むため、どの経路も定義域外のミクロ日付を確定させません。
インストール
pnpm add @aiquants/period-slider @aiquants/virtualscroll@aiquants/virtualscroll は peer dependency です。DateSelector の variant="dropdown" はポップアップの行をこのパッケージで仮想化して描くため、ホストが自分で導入し、同梱スタイルシート (@aiquants/virtualscroll/styles/virtualscroll.css、非 Tailwind ホストは .standalone.css) も読み込んでください (下の「スタイル読み込み」)。
0.10.x から上げる場合、DateSelector の variant="dropdown" が描くのはネイティブ <select> ではなく role="combobox" の読み取り専用 <input> と仮想化されたポップアップです。<select> / <option> を前提にしたテストや DOM 参照の移行先は「各種セレクタ」の DateSelector の項にあります。
0.9.0 から上げる場合は、同梱の CHANGELOG.md の「利用側の契約が変わる点」を先に読んでください (使用不可のステッパーの属性、矢印の要素、レール方向の予約、セレクタのルート幅、公開エクスポート、ドラッグのポインター捕捉、ノギスのコールバックの重複抑止)。
jsdom でこのコンポーネントを描画するテストは、jsdom が実装していないブラウザ API のシムが要ります (ResizeObserver、window.matchMedia、Element.prototype.setPointerCapture / releasePointerCapture / hasPointerCapture)。createPortal で別文書 (<iframe> やポップアウトのウィンドウ) へ描く場合は、Element.prototype が文書ごとに別であるため、その文書の Element へも同じシムを載せてください (スライダー側は操作のリスナーもホイールの登録も、自分が描かれている文書の window へ張ります)。
使用方法
スタイル読み込み
本パッケージは 2 種類の CSS 成果物を出力します。ホスト環境に応じてどちらか一方のみを読み込んでください。
// A. Tailwind CSS v4 プロジェクトの場合: パッケージ固有のコンポーネントクラスのみを読み込む
import "@aiquants/period-slider/styles/period-slider.css"
import "@aiquants/virtualscroll/styles/virtualscroll.css"
// B. 非 Tailwind (スタンドアロン) 環境の場合: 自己完結 CSS を読み込む
import "@aiquants/period-slider/styles/period-slider.standalone.css"
import "@aiquants/virtualscroll/styles/virtualscroll.standalone.css"peer dependency の @aiquants/virtualscroll のスタイルシートは、本パッケージの成果物には含まれません (2 つのパッケージの事前生成 CSS は連結では再現できないため)。DateSelector の variant="dropdown" を使わないホストでも、読み込みは無害です。
| 成果物 | 含まれるもの | 用途 |
| :--- | :--- | :--- |
| styles/period-slider.css | 手書きコンポーネントクラス (.period-slider-*) のみ。テーマ変数・ユーティリティ・preflight は含まない | ホスト側に Tailwind CSS v4 ビルドがある場合 |
| styles/period-slider.standalone.css | テーマ変数・ユーティリティを同梱した自己完結スタイル | ホストに Tailwind ビルドが無い場合 |
[!IMPORTANT] A を選ぶ場合、ホスト側の Tailwind エントリ CSS に次の 3 行が必須です。
@import "tailwindcss"; @source "../node_modules/@aiquants/period-slider/dist"; @custom-variant dark (&:where(.dark, .dark *));
@source: Tailwind CSS v4 は既定でnode_modulesを走査しないため、これが無いとパッケージが出力するユーティリティクラス (grid,rounded-full,not-aria-disabled:hover:*,dark:*,forced-colors:*等の状態・バリアント付きのものを含む) が一切生成されず、コンポーネントが未装飾のテキストとして描画されます。@custom-variant dark: Tailwind CSS v4 のdark:は既定で@media (prefers-color-scheme: dark)にコンパイルされます。これが無いと.darkクラスでは切り替わらず、手書きクラスで配色を持つスライダー本体だけが暗転し、dark:ユーティリティで配色を持つ 3 セレクタ (YearSelector,MonthSelector,DateSelector) が明色のまま取り残されます。B のスタンドアロン CSS は自己完結のため
@source指定は不要ですが、ホストの Tailwind ビルドと混在させないでください (ユーティリティ層が二重定義になります)。
ロケールと表示書式 (locale)
PeriodSlider・YearSelector・MonthSelector・DateSelector はいずれも locale?: string (BCP 47 言語タグ) を受け取ります。
| locale | 組み込みの日付表記 |
| :--- | :--- |
| 未指定 (既定) | ロケール非依存の ISO 8601 表記。unit="year" は 2025、unit="month" は 2025-01、unit="day" は 2025-01-15 (年は 4 桁ゼロ埋め) |
| 指定 | Intl.DateTimeFormat(locale, options) の出力。options は単位ごとに year: { year: "numeric" }、month: { year: "numeric", month: "2-digit" }、day: { year: "numeric", month: "2-digit", day: "2-digit" } |
ISO 8601 はフォールバックではなく、本パッケージの仕様上の既定値です。英語などの自然言語表記を既定にすると汎用パッケージへ 1 つの言語を焼き付けることになり、Intl の出力は ICU / Node のバージョン間で変化するためです。
- 優先順位: フォーマッタ Props (
formatLabel/formatMacroLabel/formatMicroLabel/formatAriaValueText、DateSelectorのformatYearOption/formatMonthOption/formatDayOption) が最優先、次にlocale、最後に ISO 8601 既定です。四半期表記だけはこの 3 段に乗りません (localeの段を持ちません)。理由と書き方は 四半期表示 を参照してください。 - 地域化されるのは人間が読むテキストのみ:
<input type="date">のvalue/min/maxは常にyyyy-MM-dd、<input type="month">は常にyyyy-MM、DateSelectorのvariant="dropdown"が操作要素に持つdata-valueは常にyyyy-MM-dd、<option value>は常に ISO 文字列 (yyyy-MM) または生の数値 (年セレクト、variant="split-dropdown"の 3 セレクト) です。 - アクセシブル名は
localeでは変わりません: ボタン・セレクト・つまみの名称は英語既定のままです。翻訳するにはlowerAriaLabel/decreaseAriaLabel/selectAriaLabelなどのアクセシブル名 Props を渡してください。 - Fail Fast: 空文字列や
Intl.DateTimeFormatが受理しない値 ("en_US"、非文字列等) を渡した場合、既定の ISO へ黙って戻さずRangeErrorを送出します (メッセージは 実行時バリデーション 参照)。"xx-YY"のように構造として妥当なタグは、対応する翻訳データが無くても受理されます。
// 既定 (locale 省略): ラベルは "2024-03-10"
<PeriodSlider mode="point" unit="day" minDate={minDate} maxDate={maxDate} value={date} onChangePoint={setDate} />
// locale 指定: ラベルは ja-JP ロケールの "2024/03/10"
<PeriodSlider mode="point" unit="day" locale="ja-JP" minDate={minDate} maxDate={maxDate} value={date} onChangePoint={setDate} />日本語 UI での表示
locale="ja-JP" を渡すと、日本語ロケールの書式へ切り替わります。PeriodSlider のラベルは unit に応じて 2024年 / 2024/01 / 2024/01/15、YearSelector の選択肢は 2024年、MonthSelector の選択肢は 2024/01、DateSelector の選択肢は 2024/01/15、DateSelector の variant="split-dropdown" は 2024年 / 01月 / 15日 になります (variant="date" はブラウザ標準の日付入力 UI のため、表示書式はブラウザのロケール設定が決めます)。
2024年01月15日 のような独自表記が必要な場合は、locale ではなくフォーマッタ Props を渡してください。
const formatJapaneseDay = (date: Date): string => `${date.getFullYear()}年${String(date.getMonth() + 1).padStart(2, "0")}月${String(date.getDate()).padStart(2, "0")}日`
<PeriodSlider mode="point" unit="day" formatLabel={formatJapaneseDay} minDate={minDate} maxDate={maxDate} value={date} onChangePoint={setDate} />フック利用時の locale
フック (usePeriodSliderRange 等) へ渡した locale は sliderProps へ転送されますが、SelectorProps は locale を含みません。スライダーとセレクタの書式を揃えるには、セレクタ側へも明示的に渡してください。
<MonthSelector {...startSelectorProps} locale="ja-JP" />四半期表示 (暦四半期と年度四半期)
四半期だけは上の 3 段の優先順位に乗りません。locale の段が存在しないためです。ECMA-402 に四半期のフィールドが無く、Intl.DateTimeFormat は quarter オプションを受け取っても送出せず黙って捨てます (Node v24.16.0 / ICU 78.3 で実測)。
const formatter = new Intl.DateTimeFormat("ja-JP", { quarter: "long", year: "numeric" })
formatter.resolvedOptions()
// { locale: "ja-JP", calendar: "gregory", numberingSystem: "latn", timeZone: "Asia/Tokyo", year: "numeric" }
// -> quarter は解決結果に残らない
formatter.format(new Date(2025, 3, 1)) // "2025年" (四半期はどこにも現れない)
Intl.supportedValuesOf("unit").includes("quarter") // false履行できない引数は置かない方針のため、四半期の 2 関数は locale を受け取りません。
| 関数 | シグネチャ | 返り値 |
| :--- | :--- | :--- |
| resolveQuarter | (d: Date, yearStartMonth: number) => PeriodQuarter | { year, quarter, startDate } の四半期座標 |
| formatQuarterLabel | (d: Date, yearStartMonth: number) => string | "2025-Q1" 形式のロケール非依存文字列 |
yearStartMonthは 2 関数とも必須です。年を扱う 8 つのヘルパー (normalizeDateByUnit等) が= 0を持つのとは意図的に違えています。非暦年の四半期を扱えることがこの API の目的であり、省略を暦四半期として黙って解釈すると、年度四半期のつもりの呼び出しが 1 四半期ずれたまま通ってしまうためです。- この
yearStartMonthは関数自身の引数であり、スライダーの同名 Props を読むものではありません。年度レール (yearStartMonth={3}) 上の暦四半期も、暦年レール上の年度四半期も、どちらも表現できます。 - 返る文字列はロケール非依存ですが ISO 8601 ではありません。ISO 8601 が定めるのは暦日・年間通日・週日付の 3 形式だけで、四半期の表記はありません。
2025年度 第2四半期 のような地域化した表記は、resolveQuarter から利用側が組み立てます。年は必ず year から取ってください。1 月境界を跨ぐ四半期では startDate.getFullYear() と一致しません (yearStartMonth が 3 のとき 2026-01-15 は year: 2025 / startDate: 2026-01-01)。
import { resolveQuarter } from "@aiquants/period-slider"
const FISCAL_YEAR_START_MONTH = 3
const formatFiscalQuarterJa = (date: Date): string => {
const { year, quarter } = resolveQuarter(date, FISCAL_YEAR_START_MONTH)
return `${year}年度 第${quarter}四半期`
}
formatFiscalQuarterJa(new Date(2026, 0, 15)) // "2025年度 第4四半期"年の部分だけは locale に従わせられます。年ラベルと同じ公開経路 (normalizeDateByUnit を通した formatLabel("year", ...)) を使うと、Q の綴りだけがロケール非依存のまま残ります。
import { formatLabel, normalizeDateByUnit, resolveQuarter } from "@aiquants/period-slider"
const formatQuarterWithLocale = (date: Date, locale?: string): string => {
const { quarter } = resolveQuarter(date, FISCAL_YEAR_START_MONTH)
// 年の切り出しを自前で書くと、年ラベルと四半期ラベルが年について食い違い得る
const year = formatLabel("year", normalizeDateByUnit("year", date, FISCAL_YEAR_START_MONTH), undefined, locale)
return `${year} Q${quarter}`
}
formatQuarterWithLocale(new Date(2025, 3, 1)) // "2025 Q1"
formatQuarterWithLocale(new Date(2025, 3, 1), "ja-JP") // "2025年 Q1"PeriodQuarter の各フィールド、yearStartMonth ごとの出力表、例外の契約は 公開ユーティリティ関数 にまとめてあります。
ゼロ・ボイラープレート React フック連携 (usePeriodSliderRange)
ステート管理・単一要素ミューテータ・セレクタ連動を 1 行で完結できます。
import { PeriodSlider, MonthSelector, usePeriodSliderRange } from "@aiquants/period-slider"
export function FastRangeExample() {
const { sliderProps, startSelectorProps, endSelectorProps } = usePeriodSliderRange({
unit: "month",
initialRange: [new Date(2024, 0, 1), new Date(2024, 11, 1)],
minDate: new Date(2020, 0, 1),
maxDate: new Date(2025, 11, 1),
colorScheme: "indigo-amber",
})
return (
<div className="flex items-center gap-8">
<MonthSelector {...startSelectorProps} />
<PeriodSlider {...sliderProps} className="flex-1" />
<MonthSelector {...endSelectorProps} />
</div>
)
}PeriodSlider (Range モード: タプル直接指定)
import { PeriodSlider } from "@aiquants/period-slider"
import { useState } from "react"
export function Example() {
const [range, setRange] = useState<[Date, Date]>([
new Date(2024, 0, 1),
new Date(2024, 11, 1),
])
return (
<PeriodSlider
mode="range"
unit="month"
minDate={new Date(2020, 0, 1)}
maxDate={new Date(2025, 11, 1)}
range={range}
onChangeRange={(lower, upper) => setRange([lower, upper])}
labelPosition="top"
autoSlantLabels={true}
/>
)
}PeriodSlider (Point モード)
import { PeriodSlider } from "@aiquants/period-slider"
import { useState } from "react"
export function PointExample() {
const [date, setDate] = useState<Date>(new Date(2024, 5, 1))
return (
<PeriodSlider
mode="point"
unit="month"
minDate={new Date(2020, 0, 1)}
maxDate={new Date(2025, 11, 1)}
value={date}
labelPosition="top"
onChangePoint={(nextDate) => setDate(nextDate)}
/>
)
}PeriodSlider (Vernier ノギス 2 段スケール一体型モード)
広域期間 (例: 数年〜十数年) の中から極小期間 (例: 数日〜数ヶ月) を精密に選択する場合に適した、ノギス型 2 段スケール一体型スライダーです。上側 (マクロ) で全体期間の表示窓を定め、下側 (ミクロ) でその表示窓を 100% に拡大した精密な選択を行います。
import { PeriodSlider } from "@aiquants/period-slider"
import { useState } from "react"
export function VernierExample() {
// マクロ側の表示窓 (2023 年〜2025 年)
const [macroRange, setMacroRange] = useState<[Date, Date]>([
new Date(2023, 0, 1),
new Date(2025, 0, 1),
])
// ミクロ側の最終確定選択期間 (2023 年 6 月〜11 月)
const [microRange, setMicroRange] = useState<[Date, Date]>([
new Date(2023, 5, 1),
new Date(2023, 10, 1),
])
return (
<PeriodSlider
mode="vernier"
unit="month"
minDate={new Date(2010, 0, 1)}
maxDate={new Date(2026, 11, 31)}
macroRange={macroRange}
onChangeMacroRange={(lower, upper) => setMacroRange([lower, upper])}
microRange={microRange}
onChangeMicroRange={(lower, upper) => setMicroRange([lower, upper])}
vernierOrientation="macro-top"
showVernierProjection={true}
showVernierTicks={true}
showHairline={true}
/>
)
}マクロ・ミクロ両段のラベルと aria-valuetext は、つまみ位置と aria-valuenow を決めるのと同一の符号値から復元した Date を整形します。したがって macroUnit="month" で macroRange に月の途中の日付を渡しても、ラベルは常に aria-valuenow と一致します。対の 2 つの符号値が一致したとき (例: 同一マクロ単位内に収まる macroRange) は、同一文字列のラベルを 2 枚描かず 1 枚へ畳みます (下記 DOM 契約の data-duplicate)。
PeriodSlider (Vernier ノギス × 時点選択)
数年の定義域から 1 か月・1 日を「1 点だけ」指す画面向けに、ノギスのミクロレールを単一つまみにした形態です。マクロ窓で定義域を絞ってからミクロレールで精密に指す操作は期間選択と同じで、違いはミクロ側が期間ではなく時点であることだけです。
ミクロ側に microValue (制御) または defaultMicroValue (非制御) を渡すと、この形態になります。
import { PeriodSlider } from "@aiquants/period-slider"
import { useState } from "react"
export function VernierPointExample() {
// マクロ側の表示窓 (2020 年〜2026 年)
const [macroRange, setMacroRange] = useState<[Date, Date]>([
new Date(2020, 0, 1),
new Date(2026, 0, 1),
])
// ミクロ側の確定時点 (2023 年 6 月)
const [basisMonth, setBasisMonth] = useState<Date>(new Date(2023, 5, 1))
return (
<PeriodSlider
mode="vernier"
unit="month"
macroUnit="year"
microUnit="month"
minDate={new Date(2015, 0, 1)}
maxDate={new Date(2030, 11, 31)}
macroRange={macroRange}
onChangeMacroRange={(lower, upper) => setMacroRange([lower, upper])}
microValue={basisMonth}
onChangeMicroPoint={setBasisMonth}
/>
)
}時点形態の契約は次のとおりです。
- ミクロレールのつまみは 1 つ (
data-handle-type="micro-point") で、role="slider"はマクロ 2 つと合わせて 3 つになります。既定のアクセシブル名は"Micro selected date"で、pointAriaLabelで上書きできます。 - ミクロ側の接続バー (
period-slider-connect-micro) は描きません。幅を持たない選択を帯で表すと「満タンの期間」に見え、掴める帯まで生えてしまうためです。マクロ窓の帯は従来どおり残ります。 aria-valuemin/aria-valuemaxはミクロレールの両端 (マクロ窓で切り出した範囲) で、aria-valuenowは選択時点そのものです。期間形態のように相手のつまみで塞がれることはありません。- キーボードは
mode="point"と同じく左右対称です。←→で ±1 ミクロ単位、PageUp/PageDownで ±5、Home/Endでレールの両端へ届きます。動けない押下はコールバックを発火しません。 - ステッパーは ±1 ミクロ単位で、レールの端に着いた側だけが使用不可 (
aria-disabled="true"、フォーカスは保持) になります。 - マクロ窓を掴んで動かすと時点も同じ暦差分だけ平行移動し、窓の端を動かして時点が窓の外に出る場合は窓の内側へ収まります。
microIsDiscrete/microAllowedDatesは期間形態と同じく併用でき、キーボードもステッパーも候補集合の上だけを歩きます。
PeriodSlider (垂直レイアウト)
isVertical={true} を指定することで、縦向き (下部が最小日付、上部が最大日付) のスライダーとして表示できます。
[!NOTE] ルート要素はグリッドで、トラックの長さはルート要素の高さ − 104px (ステッパーの当たり判定の予約 19px + ボタン 22px + 間隔 11px を上下で)、
showStepper={false}では高さ − 44px (丸つまみの当たり判定のはみ出しをルートが確保する分を上下で) です。ホストが高さを与えなければトラックは 0px に潰れるため、classNameで高さを与えてください。ブロックレベルのルート要素は親の幅いっぱいに広がるため、幅はw-fitなどで内容へ合わせます (下の例のようにflexの子にすれば内容幅になります)。
import { PeriodSlider } from "@aiquants/period-slider"
import { useState } from "react"
export function VerticalExample() {
const [range, setRange] = useState<[Date, Date]>([
new Date(2024, 2, 1),
new Date(2024, 8, 1),
])
return (
<div className="flex items-center justify-center p-4">
<PeriodSlider
mode="range"
unit="month"
isVertical={true}
className="h-60"
minDate={new Date(2024, 0, 1)}
maxDate={new Date(2024, 11, 31)}
range={range}
onChangeRange={(lower, upper) => setRange([lower, upper])}
/>
</div>
)
}PeriodSlider (離散日付スナップ & 目盛表示)
実データが存在する離散日付のみを選択対象とし、トラック上に目盛線を表示します。allowedDates を指定する場合も minDate / maxDate は必須で、範囲外の要素は除外されます。
import { PeriodSlider } from "@aiquants/period-slider"
import { useState } from "react"
const discreteDates = [
new Date(2024, 0, 15),
new Date(2024, 1, 14),
new Date(2024, 2, 18),
new Date(2024, 3, 22),
new Date(2024, 4, 10),
]
export function DiscreteExample() {
const [point, setPoint] = useState<Date>(discreteDates[0])
return (
<PeriodSlider
mode="point"
unit="day"
minDate={new Date(2024, 0, 1)}
maxDate={new Date(2024, 11, 31)}
allowedDates={discreteDates}
showTicks={true}
snapMode="time"
value={point}
onChangePoint={(d) => setPoint(d)}
/>
)
}PeriodSlider (カスタムラベル & 独立 2 点選択)
四半期表示などのカスタムフォーマット (formatLabel) や、2 点間の連結バーを非表示にする hideConnectBar={true} を組み合わせた設定例です。四半期の文字列はパッケージが公開する formatQuarterLabel で作ります (Math.floor(month / 3) を自前で書くと、年度運用へ移したときに年と四半期が食い違います)。
import { formatQuarterLabel, PeriodSlider } from "@aiquants/period-slider"
import { useCallback, useState } from "react"
export function CustomLabelExample() {
const [range, setRange] = useState<[Date, Date]>([
new Date(2024, 0, 1),
new Date(2024, 6, 1),
])
// 暦四半期 (1-3 月が Q1): 2024-01-01 -> "2024-Q1"、2024-07-01 -> "2024-Q3"
const formatCalendarQuarter = useCallback((date: Date) => formatQuarterLabel(date, 0), [])
return (
<PeriodSlider
mode="range"
unit="month"
minDate={new Date(2023, 0, 1)}
maxDate={new Date(2025, 11, 31)}
range={range}
hideConnectBar={true}
formatLabel={formatCalendarQuarter}
tooltipStyle="balloon"
onChangeRange={(lower, upper) => setRange([lower, upper])}
/>
)
}同じレールへ年度四半期 (4 月開始) を載せる場合も、変えるのは formatQuarterLabel の第 2 引数だけです。レールの構成 (unit / minDate / maxDate) は 1 文字も変わりません。
import { formatQuarterLabel, PeriodSlider } from "@aiquants/period-slider"
import { useCallback, useState } from "react"
const FISCAL_YEAR_START_MONTH = 3
export function FiscalQuarterLabelExample() {
const [range, setRange] = useState<[Date, Date]>([
new Date(2024, 0, 1),
new Date(2024, 6, 1),
])
// 年度四半期 (4-6 月が Q1): 2024-01-01 -> "2023-Q4"、2024-07-01 -> "2024-Q2"
const formatFiscalQuarter = useCallback((date: Date) => formatQuarterLabel(date, FISCAL_YEAR_START_MONTH), [])
return (
<PeriodSlider
mode="range"
unit="month"
minDate={new Date(2023, 0, 1)}
maxDate={new Date(2025, 11, 31)}
range={range}
hideConnectBar={true}
formatLabel={formatFiscalQuarter}
tooltipStyle="balloon"
onChangeRange={(lower, upper) => setRange([lower, upper])}
/>
)
}同じ 2 つの日付が、2 つの四半期系で次のように分かれます。
| 日付 | formatQuarterLabel(date, 0) | formatQuarterLabel(date, 3) |
| :--- | :--- | :--- |
| 2024-01-01 | "2024-Q1" | "2023-Q4" |
| 2024-07-01 | "2024-Q3" | "2024-Q2" |
上の 2 例はどちらも yearStartMonth Props を渡していません。unit="month" のレールには動かすべき年の境界が無いためです (渡しても値の検証が走るだけで、つまみの位置もラベルも変わりません)。Props が効くのは年の格子を持つ構成 — unit="year"、ノギスの macroUnit="year"、年解像度の allowedDates — で、そこでもラベルの四半期系を決めるのは関数の第 2 引数のままです。
// 年度レールの上に年度四半期を載せる (抜粋): 定数は 1 つ、渡す先は 2 つ、どちらも明示的
<PeriodSlider
mode="vernier"
unit="month"
macroUnit="year"
microUnit="month"
yearStartMonth={FISCAL_YEAR_START_MONTH}
minDate={new Date(2020, 3, 1)}
maxDate={new Date(2026, 2, 31)}
macroRange={macroRange}
microRange={microRange}
formatMicroLabel={formatFiscalQuarter}
onChangeMacroRange={(lower, upper) => setMacroRange([lower, upper])}
onChangeMicroRange={(lower, upper) => setMicroRange([lower, upper])}
/>四半期フォーマッタを formatMacroLabel ではなく formatMicroLabel へ渡している点が要点です。マクロつまみが macroUnit="year" のとき、フォーマッタが受け取る日付は必ずその年の先頭月なので、四半期は常に Q1 になります。
ポインター操作の約束
- 1 つの操作が押下から解放まで値の持ち主です: 掴んでいる間は、2 本目の指の押下も
Shift+ ホイールも矢印キーもステッパーも値を動かしません (ホイールはページのスクロールのままです)。ネイティブのrange入力と同じく、先に押した指のものです。 - 取り消されたポインターは中断された位置を残します: OS のジェスチャーや第 3 のタッチで
pointercancelが届いても、ドラッグ開始前の値へは戻しません。途上の値は既にonChange*で渡っているためです。 - 押すだけで動きます: トラックを押して指を動かさずに離すと、最寄りの対象がその位置へ寄ります (ノギスのマクロ窓・ミクロ選択も同じ)。ドラッグを要しない単一ポインターの経路です (WCAG 2.2 SC 2.5.7)。
- 使用不可の ◀▶ / ステッパーは押下をホストへ通しません: 境界に着いたボタンは
aria-disabled="true"でフォーカスを保ったまま、押下を捕捉フェーズで飲み込みます (preventDefault+stopPropagation)。mousedown/mouseup/clickは React のルートコンテナより下のもの (ボタン自身と、その間にあるホストの要素) へ 1 つも届かず、押下がフォーカスを奪うこともありません。クリック可能なカード・イベント委譲・外側押下による閉じ処理・クリック計測の中へ置いても、淡く塗られた矢印からホストのハンドラが発火することはありません。ネイティブのdisabledと同じくpointerdown/pointerupは配送されます。
配置の注意 (範囲領域と当たり判定)
範囲領域はホストの押下を奪いません:
Shift+ ホイールを受ける範囲領域 (ルート要素の箱に加えて、レールの上下のレーンと描かれたラベルの帯。レール方向にもラベルの外縁まで届きます) は何も描かず、ヒットテストにも一切答えません。レール方向の伸びは要素の箱ではなく持ち主判定の中だけにあるため、余裕のないスクロールコンテナへスクロールのはみ出しを足すこともありません。帯がホストの要素に重なっても、押下・ホバー・コンテキストメニューは何も変わりません。帯を空けるかどうかは、ラベルが読めるかどうかという見た目の判断です (レーンはレールの上下 22px、ノギスは 28px。ラベルの帯の厚みは描かれるラベルの寸法と傾きで変わり、日単位で接近した対では 90px を超えます)。範囲領域の持ち主は幾何で決まります: スライダーを 2 つ近づけて範囲領域が重なっても、動くのはトラック中心線が近い方だけ (同距離なら先にマウントした方だけ) で、互いを動かすことはありません。描かれていないスライダー —
visibility: hiddenの下、display: noneの下、畳んだアコーディオン (max-height: 0; overflow: hiddenやcontain: paint) の中 — はどの点も持たないため、そこに見えているホストの要素から操作を奪うこともありません。切り取りは CSS の規則どおり包含ブロックの連鎖でだけ効きます (position: fixedのスライダーは、transformなどで包含ブロックを作らない祖先のoverflowには切り取られず、浮いて描かれているとおり自分の領域を持ちます)。祖先のclip-pathとmaskが消した部分だけは扱いません (算出値から復元できないため、その方法で隠したスライダーは自分の点を持ち続けます)。逆に、スライダーの上へ重ねたホストの層 (ドロップダウンなど) の上でもShift+ ホイールはスライダーを動かします。ホストの層を優先させたい場合の手立ては 3 つあります。(a) ルート要素のインライン--ps-label-reach-before/--ps-label-reach-after/--ps-label-reach-mainを読んで帯の厚みだけ避ける (この 3 つは--ps-名前空間で唯一、読み取り専用の出力として名前と意味を保つもので、ホストが読んでかまいません。上書きの口ではありません。ラベルが描かれていない側は変数自体が出ないためvar(--ps-label-reach-after, 0px)の形で読みます)、(b)labelPositionで帯を反対側へ移す、(c)tooltipStyle="hover"でラベルを描かせず、領域をレール周りのレーン (22px / ノギス 28px) へ縮める。いずれもスライダーの機能は落ちません。ドラッグ中はトラックがインライン変数を持ちます: 押している間、トラック要素 (
[data-testid="period-slider-track"]) は--ps-drag-*(位置とラベルの配置) を、指を離した直後の収め戻しの間はdata-drag-snap="true"と--ps-drag-snap-durationを持ちます。いずれも内部のもので、上書き口ではありません。配布スタイルシートは位置と配置の 24 個をトラック自身へinitialとして宣言するため、ホストが同名の変数を宣言してもスライダーの描画には届きません。--ps-drag-snap-durationには宣言を置きません — この変数が無い間は収め戻しの宣言そのものが算出時に無効となるため、既定時間で勝手に動き出すことがないからです (コンポーネントは属性と一緒に必ずインラインで書きます)。当たり判定のはみ出し: スライダーの当たり判定がレール方向へ伸びる分は、ステッパーの表示・非表示のどちらでもルート要素が自分の箱の中へ確保するため、レール方向には離隔が要りません。残るのはセレクタの ◀▶ の四方 11px と、スライダーのレールの交差軸方向 (ステッパーを表示していれば 11px、
showStepper={false}なら 18px。ルートの箱の厚みがボタンの箱 22px からレールの 8px へ薄くなる分だけ、丸つまみの 44px の当たり判定が外へ残ります) です。| 境界 | 空ける量 (ステッパー表示 / 非表示) | | :--- | ---: | | セレクタの矢印 ⇄ 文字などの非操作要素 | 11px | | セレクタ ⇄ セレクタ (縦横とも) | 22px | | セレクタ ⇄ スライダー (レール方向に並べる) | 11px | | セレクタ ⇄ スライダー (レールの交差軸方向に並べる) | 22px / 29px | | スライダー ⇄ スライダー (レール方向) | 0px | | スライダー ⇄ スライダー (交差軸方向) | 22px / 36px | | スライダー ⇄ 文字などの非操作要素 (交差軸方向) | 11px / 18px |
空けないと、はみ出した当たり判定が隣の文字や部品の押下を受けます。押せるステッパーの当たり判定は
--aqps-z-handle-front+ 1 (既定 41) の層に載るため、z-indexが 41 未満のホストの層に重なると、ステッパーの上でその層の押下を奪います。ルート要素のレイアウトはコンポーネントが持つ:
PeriodSliderのルート要素はグリッド (grid/gap/place-items)、セレクタのルート要素は内容幅のinline-flexです。classNameには寸法と余白 (flex-1、h-60、w-fitなど) だけを与えてください。縦向きのスライダー: トラックの長さはルート要素の高さから決まるため、高さを与えてください (与えないとトラックは 0px に潰れます)。幅は
w-fitなどで内容へ合わせます。セレクタだけを使う場合もスタイルシートを読み込む: 44px の当たり判定は、配布スタイルシートがパッケージ自身の要素 (
.period-sliderと 1 単位移動ボタン) に宣言する内部変数を読みます。読み込まないと押せる範囲はボタンの箱 (22px) だけになります。
PeriodSlider Props
全モード共通 Props
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| mode | "range" \| "point" \| "vernier" | (必須) | 操作モード (range: 2 点範囲選択, point: 1 点選択, vernier: ノギス 2 段スケール一体型) |
| unit | "year" \| "month" \| "day" | (必須) | 時間解像度単位 |
| yearStartMonth | number | 0 | unit="year" (ノギスの macroUnit="year" / microUnit="year" を含む) の 1 年が始まる暦月 (0 始まり)。3 は 4 月〜翌 3 月の会計年度、9 は 10 月〜翌 9 月のでん粉年度。年はその年の先頭月が属する暦年で名乗ります (yearStartMonth={3} なら 2025-04-01 〜 2026-03-31 が「2025 年」)。allowedDates / macroAllowedDates の要素は「その日を含む年」として読まれるため、年解像度の候補は new Date(y, yearStartMonth, 1) で渡してください (1 月 1 日を渡すと yearStartMonth={3} では前年の候補になります)。unit="month" / "day" のレールには年の境界が無いため位置は変わりません。0〜11 の整数以外 (12 / -1 / 1.5 / NaN / null) は RangeError、未指定は仕様上の 0 (暦年) |
| minDate | Date | (必須) | 選択可能最小日付。欠落・Invalid Date は RangeError |
| maxDate | Date | (必須) | 選択可能最大日付。minDate より厳密に後であること。欠落・Invalid Date・逆転は RangeError |
| allowedDates | Date[] | undefined | 離散スナップ対象日付配列 (指定時はこの日付のみにスナップ)。[minDate, maxDate] 外の要素は除外され、全要素が範囲外なら RangeError |
| snapMode | "index" \| "time" | "index" | 離散スナップ時の配置方式 ("index": 配列インデックス均等, "time": 実時間比例)。実時間軸やチャートと位置を揃える場合は "time" を明示指定 |
| labelPosition | "top" \| "bottom" | "top" | 日付ラベルの表示位置 (スライダーの上側または下側) |
| autoSlantLabels | boolean | true | 近接時にラベルを斜め〜90° に自動傾斜させて重なりを防止 (Range の 1 対とノギスの 2 対へ対ごとに適用。垂直レイアウトでは適用されない) |
| tooltipStyle | "always" \| "hover" \| "balloon" | "always" | ツールチップの表示スタイル ("always": 常時, "hover": ポインター操作時, "balloon": 吹き出し型)。"balloon" の宣言は !important 付きでユーティリティ層に勝つが、ノギスのツールチップだけは段の配色 (マクロ青 / ミクロ赤) を保ち、角丸・余白・文字サイズ・高さのみ吹き出し型になる |
| isVertical | boolean | false | 垂直スライダー表示の有効化。ホストが className で高さを与える必要があります (トラックの長さはルート要素の高さ − 104px、showStepper={false} では高さ − 44px で、高さが無いと 0px に潰れて何も見えません) |
| hideConnectBar | boolean | false | 範囲選択バー (ハイライト) を非表示にし、独立 2 点選択 UI として利用 |
| showTicks | boolean | false | 離散モード時のトラック上の目盛線の表示有無 |
| showStepper | boolean | true | ステッパーボタンの表示有無 (水平時は左右、垂直時は上下を指す矢印) |
| formatLabel | (date: Date) => string | undefined | 日付ラベル・ツールチップのカスタム表示文字列生成関数。locale より優先される。ノギスでは formatMacroLabel / formatMicroLabel 双方の既定値でもあるため、ここへ四半期フォーマッタを渡すとマクロ側にも同じ関数が流れ、マクロラベルが常に Q1 になる。ノギスで四半期を出すなら formatMicroLabel にだけ渡す |
| locale | string | undefined | 組み込み日付表記のロケール (BCP 47 言語タグ)。未指定はロケール非依存の ISO 8601 (2025 / 2025-01 / 2025-01-15)、指定時は Intl.DateTimeFormat 書式。空文字列・不正タグ・非文字列は RangeError |
| disabled | boolean | false | スライダーの無効化・非活性表示 |
| debounceMs | number | 0 | 変更コールバック呼び出しのデバウンス時間 (ミリ秒)。0 は即時発火 |
| colorScheme | PeriodSliderColorScheme | "blue-rose" | カラーテーマプリセット ("blue-rose", "indigo-amber", "teal-emerald", "violet-fuchsia", "slate-sky", "monochrome") |
| className | string | "" | 最外層コンテナ要素の追加 CSS クラス。ルート要素のレイアウト (grid / gap / place-items) はコンポーネントが持つため、寸法と余白だけを与える |
| style | React.CSSProperties | undefined | 最外層コンテナ要素のインラインスタイル。--aqps- カスタムプロパティを 1 台単位で注入する正式な経路 (「CSS カスタムプロパティ (スタイリング API)」を参照) |
Range モード専用 Props (mode="range")
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| range | [Date, Date] | undefined | 制御 (Controlled) 時の選択期間タプル [開始, 終了]。指定時は本 Props が単一真実源 |
| defaultRange | [Date, Date] | undefined | 非制御 (Uncontrolled) 時の初期選択期間タプル。range 指定時は無視 |
| onChangeRange | (lower: Date, upper: Date) => void | undefined | 選択期間変更時コールバック |
range / defaultRange を共に省略した場合、両つまみは minDate (離散時は候補日付の先頭) に重なり、幅ゼロの期間として描画されます (この状態のラベルは 1 枚へ畳まれます)。意図した期間は必ずどちらかで与えてください。
Point モード専用 Props (mode="point")
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| value | Date | undefined | 制御 (Controlled) 時の選択日付。指定時は本 Props が単一真実源 |
| defaultValue | Date | undefined | 非制御 (Uncontrolled) 時の初期選択日付。value 指定時は無視 |
| onChangePoint | (value: Date) => void | undefined | 選択日付変更時コールバック |
value / defaultValue を共に省略した場合、選択日付は定義域下限 (minDate、離散時は候補日付の先頭) として描画されます。
Vernier モード専用 Props (mode="vernier")
ノギスはミクロレールの形態で 2 つに分かれます。下の表はどちらの形態にも共通する Props で、ミクロ選択そのものは続く 2 表のいずれか一方だけを使います。
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| macroRange | [Date, Date] | (必須) | マクロ側の広域表示窓 [開始, 終了]。制御専用 (非制御版は存在しない)。欠落・2 要素タプル以外・Invalid Date・逆転 (end が start より前) は RangeError。start と end が同値の場合は 1 マクロ単位幅の窓として有効 |
| onChangeMacroRange | (lower: Date, upper: Date) => void | undefined | マクロ窓変更時コールバック |
| macroUnit | "year" \| "month" \| "day" | unit | マクロ側時間解像度単位 (異単位構成用)。macroUnit で minDate と maxDate が同一値に丸められる場合は RangeError |
| microUnit | "year" \| "month" \| "day" | unit | ミクロ側時間解像度単位 (異単位構成用) |
| macroIsDiscrete | boolean | false | マクロ側の離散スナップ有効化フラグ。false の間は macroAllowedDates が参照されない |
| microIsDiscrete | boolean | false | ミクロ側の離散スナップ有効化フラグ。false の間は microAllowedDates が参照されない。true かつ現在のマクロ窓が候補を 1 つ以上含む間、ミクロレールは候補集合しか描かないため、位置・aria-valuenow / aria-valuetext・ラベル・確定値のすべてが最近傍候補へ解決される (ドラッグ・キーボード・ホイール・ステッパー・フックの収め込みで同一規則)。窓が候補を 1 つも含まない場合は窓内の連続値として振る舞う |
| macroAllowedDates | Date[] | allowedDates | マクロ側の離散スナップ対象日付配列。macroIsDiscrete={true} との併用が必須 |
| microAllowedDates | Date[] | allowedDates | ミクロ側の離散スナップ対象日付配列。microIsDiscrete={true} との併用が必須。候補はマクロ窓で切り出され、窓の外の日付へはキーボード・ポインターのいずれからも到達しない。窓内に候補が 1 件も残らない場合、ミクロレールは窓内の連続値として振る舞う |
| formatMacroLabel | (date: Date) => string | formatLabel | マクロ側ラベル・ツールチップのカスタム表示文字列生成関数。引数はつまみ位置・aria-valuenow と同一の符号値から復元した macroUnit 正規化済み Date であり、macroRange の生の値ではない (macroUnit="month" なら常に月初、"year" なら常にその年の先頭月 1 日 (yearStartMonth が 0 なら元日)、"day" は暦日そのままで時刻が 0 時)。macroUnit="year" では引数が必ず年の先頭月であるため、ここへ四半期フォーマッタを渡すと表示は常に Q1 になる (四半期は月・日解像度のミクロ側へ載せる) |
| formatMicroLabel | (date: Date) => string | formatLabel | ミクロ側ラベル・ツールチップのカスタム表示文字列生成関数。引数はつまみ位置と同一の microUnit 正規化済み Date (microUnit="month" なら常に月初)。四半期ラベルはこの microUnit="month" / "day" の側に載せる。formatQuarterLabel へ渡す yearStartMonth はその関数自身の引数であり、スライダーの yearStartMonth Props とは独立に選べる |
| vernierOrientation | "macro-top" \| "macro-bottom" \| "macro-left" \| "macro-right" | 水平時 "macro-top" / isVertical 時 "macro-left" | ノギスのレール配置。水平時は上下 (macro-top / macro-bottom)、垂直時は左右 (macro-left / macro-right) を指定 |
| showVernierProjection | boolean | true | マクロ窓からミクロ全幅へ広がる半透明台形プロジェクション光の表示有無 |
| showVernierTicks | boolean | true | 目盛線の表示有無。マクロレールに 20% 間隔で 4 本、ミクロレールに 10% 間隔で 9 本を等間隔に描画する |
| showHairline | boolean | true | 半円つまみ先端の 1px 精密ヘアライン針の表示有無 |
ミクロが期間の形態 (PeriodSliderVernierRangeProps)
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| microRange | [Date, Date] | undefined | 制御時のミクロ選択期間タプル [開始, 終了]。指定時は本 Props が単一真実源となり、内部ステートは一切参照されない |
| defaultMicroRange | [Date, Date] | undefined | 非制御時の初期ミクロ選択期間タプル。以降のミクロ操作はコンポーネント内部のステートが保持し、つまみは操作に追従する。microRange 指定時は無視 |
| onChangeMicroRange | (lower: Date, upper: Date) => void | undefined | ミクロ選択期間変更時コールバック |
microRange (制御) / defaultMicroRange (非制御) はいずれか一方が必須です。共に省略した場合は描画時に RangeError を送出し、ミクロ選択範囲を発明しません。
ミクロが時点の形態 (PeriodSliderVernierPointProps)
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| microValue | Date | undefined | 制御時のミクロ選択時点。指定時は本 Props が単一真実源となり、内部ステートは一切参照されない |
| defaultMicroValue | Date | undefined | 非制御時の初期ミクロ選択時点。以降のミクロ操作はコンポーネント内部のステートが保持し、つまみは操作に追従する。microValue 指定時は無視 |
| onChangeMicroPoint | (value: Date) => void | undefined | ミクロ選択時点変更時コールバック |
microValue (制御) / defaultMicroValue (非制御) はいずれか一方が必須です。ミクロ選択を 1 つも渡さなければ、期間側・時点側の双方を名指しする RangeError を送出します。
期間側の 3 Props と時点側の 3 Props は相互排他です。型では互いを never として宣言しているため同時指定はコンパイルエラーになり、JavaScript からの同時指定は描画時に RangeError で拒否します (黙ってどちらかを優先すると、渡したはずのコールバックが永久に鳴らない状態になるため)。
アクセシビリティ Props (AccessibleSliderLabels, 全モード共通)
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| lowerAriaLabel | string | "Range start" (vernier 時 "Micro range start") | 下限つまみのアクセシブル名 |
| upperAriaLabel | string | "Range end" (vernier 時 "Micro range end") | 上限つまみのアクセシブル名 |
| pointAriaLabel | string | "Selected date" (vernier 時点形態では "Micro selected date") | 単一選択つまみのアクセシブル名 (mode="point"、およびノギスのミクロ時点形態) |
| macroLowerAriaLabel | string | "Macro range start" | マクロ下限つまみのアクセシブル名 (mode="vernier" のみ) |
| macroUpperAriaLabel | string | "Macro range end" | マクロ上限つまみのアクセシブル名 (mode="vernier" のみ) |
| decreaseAriaLabel | string | "Decrease" | 値を minDate 方向へ 1 単位動かすステッパーボタンのアクセシブル名 (水平時は左端の左向き矢印、垂直時は下端の下向き矢印) |
| increaseAriaLabel | string | "Increase" | 値を maxDate 方向へ 1 単位動かすステッパーボタンのアクセシブル名 (水平時は右端の右向き矢印、垂直時は上端の上向き矢印) |
| formatAriaValueText | (date: Date, unit: PeriodSliderUnit) => string | undefined | aria-valuetext の読み上げ文字列生成関数。未指定時は可視ラベルと同一の文字列 (ISO 8601、locale 指定時は Intl.DateTimeFormat 書式)。ノギスでは可視ラベルと同じく、マクロ側に macroUnit 正規化済み、ミクロ側に microUnit 正規化済みの Date が渡される。指定すると、aria-valuetext を組み立てる 6 か所すべてで可視ラベルの経路 (formatScreenReaderLabel) を短絡します。四半期ラベルを表示しているなら、ここへ渡す文字列も同じ resolveQuarter 呼び出しから組み立ててください (省略すれば可視ラベルと一字一句同じ文字列が読み上げられます) |
ステッパーの 2 つの名称は配置ではなく値への作用で定義されています。同じボタンが水平レイアウトでは左、垂直レイアウトでは下に立つため、位置で命名すると意味が反転するからです。
コールバック一覧
各イベントに対するコールバックは 1 つだけ存在し、別名は提供されません。
| コールバック | 発火モード | シグネチャ |
| :--- | :--- | :--- |
| onChangePoint | point | (value: Date) => void |
| onChangeRange | range | (lower: Date, upper: Date) => void |
| onChangeMacroRange | vernier (両形態) | (lower: Date, upper: Date) => void |
| onChangeMicroRange | vernier (ミクロが期間の形態) | (lower: Date, upper: Date) => void |
| onChangeMicroPoint | vernier (ミクロが時点の形態) | (value: Date) => void |
DOM 契約 (テストフック)
DOM 要素の特定は CSS クラス名ではなく data-* 属性で行います。クラス名はスタイリングの契約であり、予告なく変更されます。
| フック | 対象 |
| :--- | :--- |
| [data-mode="range" \| "point" \| "vernier"] | ルート要素。data-unit / data-orientation / data-label-position / data-color-scheme / data-disabled を併せ持つ。インライン style に --ps-label-reach-before / --ps-label-reach-after / --ps-label-reach-main (範囲領域の厚みとレール方向の伸びに使う実測値) を持つことがある。上書きの口ではないが、ホストが自分の層を避けるために読むことは想定しており、名前と意味は保つ (配置の注意を参照) |
| [role="slider"] | すべてのつまみ。aria-label (既定 "Range start" / "Range end" / "Selected date" / "Macro range start" / "Macro range end" / "Micro range start" / "Micro range end" / "Micro selected date") で個々のつまみを識別でき、data-active / data-front が操作状態を表す。個数はモードと形態で決まり、range は 2、point は 1、ノギスの期間形態は 4、ノギスの時点形態は 3 |
| [data-testid="period-slider-knob"] | range / point モードのつまみ本体 (円形ノブ)。つまみの位置・寸法の測定に用いる |
| [data-handle-type="macro-low" \| "macro-high" \| "micro-low" \| "micro-high" \| "micro-point"] | ノギスモードのつまみのみに付与 (role="slider" との併用)。micro-low / micro-high はミクロが期間の形態、micro-point はミクロが時点の形態でのみ現れ、両者が同時に存在することはない。配置位置は data-vernier-pos (top / bottom / left / right) が持ち、配色を決める data-handle-type と直交する |
| [data-testid="period-slider-track"] | トラック (data-vernier-orientation でノギスのレール配置を判別) |
| [data-range-area] | 範囲領域 (ルート要素の最初の子)。Shift + ホイールを受ける矩形そのもので、何も描かず pointer-events: none のためヒットテストにも答えない |
| button[aria-label] 内の svg[data-arrow="left" \| "right" \| "up" \| "down"] | ステッパー (アクセシブル名は decreaseAriaLabel / increaseAriaLabel、type="button") とその矢印の向き。矢印は色を持たず、ボタンの文字色 (currentColor) で塗られる (パッケージのスタイルシートも、ボタンや祖先のクラスも矢印を選ばない)。ホバーの文字色は不透明で、押せる矢印をホバーで使用不可の淡色へ落とさない。境界で動けない間はボタンが aria-disabled="true" を持ち、フォーカスを保ちタブ順にも残ったまま押下を受け付けない。ネイティブの disabled は disabled Props でスライダー全体を無効にしたときだけ付く |
| [data-testid="period-slider-connect"] | 接続バー。ノギスでは period-slider-connect-macro / period-slider-connect-micro の 2 本 (ミクロが時点の形態では period-slider-connect-micro を描かない) |
| [data-testid="period-slider-tick"] | 目盛線と離散レールの候補点。ノギスモードでは data-tick-scale="macro" \| "micro" が併記され上下段を判別できる。塗りはパッケージのスタイルシートが 1 か所で持つ (レールの境界線と同じインク) |
| [data-testid="period-slider-hairline"] | ノギスの読み取りの針 (showHairline が真のときだけ描かれる)。線の中心がつまみの中心 = 値そのものに揃う |
| [data-testid="period-slider-hover-guide"] | ポインターオーバーレイ (点と追従ラベルを束ねる器)。range / point のみで、ノギスモードでは描かれない |
| [data-testid="period-slider-hover-dot"] | オーバーレイの点 (ポインター位置そのものを示す。重複時も表示され続ける) |
| [data-testid="period-slider-hover-tooltip"] | オーバーレイの追従ラベル。指している値がつまみの確定値と一致する間は data-duplicate="true" が付いて visibility: hidden になる |
| [data-tooltip-style="always" \| "hover" \| "balloon"] | すべてのツールチップ (Range は 2 枚、Point は 1 枚、ノギスはミクロ期間形態が 4 枚・ミクロ時点形態が 3 枚)。tooltipStyle="hover" ではツールチップ要素自体が描かれない。range / point のツールチップは data-label-position / data-front を併せ持つ (ノギスではこの 2 つは親つまみ側にある) |
| [data-slanted="true"] | 近接傾斜中のツールチップ。Range の 2 枚とノギスのミクロ期間形態の 4 枚が対象 (Point モードと、ノギスのミクロ時点形態のミクロラベルには付かない。いずれも離す相手が居ないため)。ノギスは対ごとに独立して傾くため、マクロ対とミクロ対で有無が違うのは正常。垂直レイアウトでは付かない |
| [data-duplicate="true"] | 同じ値が二重に描かれる側を隠す唯一の印。対象は 2 種類で、いずれも符号化値 (aria-valuenow) の厳密一致が条件。(1) 同じ値に重なった対のうち冗長な側のつまみラベル 1 枚 — 対は Range の下限 / 上限、ノギスのマクロ下限 / 上限、ノギスのミクロ期間形態のミクロ下限 / 上限の 3 つで、Point モードとノギスのミクロ時点形態には付かない。(2) 指している値がつまみの確定値と一致したときのオーバーレイの追従ラベル — こちらは Point モードでも付く (tooltipStyle="hover" ではつまみのラベルが描かれず二重にならないため付かない) |
| [data-handle-type="macro-low"] [data-tooltip-style] | 個々のノギスラベル。ノギスのツールチップは親つまみ経由で特定する。水平レール (macro-top / macro-bottom) かつ autoSlantLabels 有効時はインライン style に --ps-tooltip-tx / --ps-tooltip-ty / --ps-tooltip-rot の 3 つを持ち (--ps-tooltip-ty はレール基準からの生のピクセル差分)、垂直レール (macro-left / macro-right)、autoSlantLabels={false}、およびミクロ時点形態のミクロラベルでは style 属性を持たない (時点には離す相手が居ないため傾斜計算そのものを行わない) |
公開ユーティリティ関数
PeriodSlider が内部で使う純粋関数のうち、次のものはパッケージのルート (@aiquants/period-slider) から公開されています。React に依存しないため、レールの外 (取得期間の算出、集計キーの生成など) でもそのまま使えます。
四半期 — resolveQuarter / formatQuarterLabel
export interface PeriodQuarter {
/** その年の先頭月が属する暦年 */
year: number
/** その年の中の四半期序数 (1 始まり) */
quarter: 1 | 2 | 3 | 4
/** 四半期先頭の現地 0 時 */
startDate: Date
}
export declare const resolveQuarter: (d: Date, yearStartMonth: number) => PeriodQuarter
export declare const formatQuarterLabel: (d: Date, yearStartMonth: number) => stringyearStartMonthは 2 関数とも必須で、下の 8 つのヘルパーが持つ= 0とは意図的に違えてあります。省略を暦四半期として黙って解釈すると、年度四半期のつもりの呼び出しが 1 四半期ずれたまま通るためです。yearStartMonthは関数自身の引数であり、スライダーの同名 Props を読むものではありません。年度レール上の暦四半期も、暦年レール上の年度四半期も表現できます。- 序数の型は
PeriodQuarter["quarter"]で参照できるため、地域化テーブルの添字にキャストが要りません (const JA: Record<PeriodQuarter["quarter"], string>)。 yearStartMonthを含む四半期が必ず Q1 です。yearはその年の先頭月が属する暦年で、startDate.getFullYear()と一致するとは限りません (yearStartMonthが3のとき2026-01-15はyear: 2025/startDate: 2026-01-01)。ラベルの年は必ずyearから取ってください。formatQuarterLabelが返す"<year>-Q<n>"はロケール非依存ですが ISO 8601 ではありません (ISO 8601 が定めるのは暦日・年間通日・週日付の 3 形式のみ)。
formatQuarterLabel(date, yearStartMonth) の出力:
| 日付 | yearStartMonth: 0 | yearStartMonth: 3 (4-3 月) | yearStartMonth: 9 (10-9 月) |
| :--- | :--- | :--- | :--- |
| 2025-01-15 | "2025-Q1" | "2024-Q4" | "2024-Q2" |
| 2025-04-01 | "2025-Q2" | "2025-Q1" | "2024-Q3" |
| 2025-12-31 | "2025-Q4" | "2025-Q3" | "2025-Q1" |
| 2026-03-31 | "2026-Q1" | "2025-Q4" | "2025-Q2" |
四半期の境界は yearStartMonth を 3 で割った剰余だけで決まり、番号は yearStartMonth そのもので決まります。 上の 4 日付の resolveQuarter(date, ...).startDate は yearStartMonth が 0 / 3 / 9 のいずれでも同一 (2025-01-01 / 2025-04-01 / 2025-10-01 / 2026-01-01) で、動くのは番号だけです。3 の倍数でない開始月 (1, 2, 4, 5, 7, 8, 10, 11) では境界そのものが動きます (yearStartMonth: 1 は 2 月〜4 月が Q1 で、2025-04-01 は "2025-Q1" / startDate: 2025-02-01)。
月 (1 月〜12 月) から四半期序数への対応:
yearStartMonth=0 1,1,1,2,2,2,3,3,3,4,4,4
yearStartMonth=1 4,1,1,1,2,2,2,3,3,3,4,4
yearStartMonth=3 4,4,4,1,1,1,2,2,2,3,3,3
yearStartMonth=9 2,2,2,3,3,3,4,4,4,1,1,1例外の契約は、パッケージの計算系とラベル系の作法をそのまま踏襲します。
| 入力 | resolveQuarter (計算系) | formatQuarterLabel (ラベル系) |
| :--- | :--- | :--- |
| d が未指定・Invalid Date | RangeError (normalizeDateByUnit と同じ) | "" (formatLabel と同じ) |
| yearStartMonth が 0〜11 の整数以外 | RangeError | RangeError |
| 両方とも不正 | RangeError | "" (無効日付の短絡が yearStartMonth の検査より先) |
メッセージは必ず呼ばれた関数自身を名指しします (formatQuarterLabel: yearStartMonth must be an integer between 0 and 11 (received: 12))。
quarter が 1 の日付では、resolveQuarter(d, ysm).startDate が normalizeDateByUnit("year", d, ysm) と厳密に一致します。四半期の格子とレールの年の格子は同じ 1 つの規則から導かれており、ずれません。
年を扱う 8 つのヘルパーの yearStartMonth (末尾引数・既定 0)
| シグネチャ | 役割 |
| :--- | :--- |
| normalizeDateByUnit(unit, d, yearStartMonth = 0) | 単位先頭への正規化。unit="year" は「その日を含む年」の先頭月 1 日 |
| encode(unit, d, yearStartMonth = 0) | 比較用の整数キー。unit="year" では年番号そのもの |
| decode(unit, val, yearStartMonth = 0) | 整数キーから日付へ。unit="year" は new Date(val, yearStartMonth, 1) |
| getMacroWindowStartDate(macroUnit, microUnit, d, yearStartMonth = 0) | マクロ窓の開始境界 |
| getMacroWindowEndDate(macroUnit, microUnit, d, yearStartMonth = 0) | マクロ窓の終了境界。年の末尾は 12 月ではなく、開始月から数えて 12 番目の月 |
| resolveMacroWindow(macroUnit, microUnit, macroRange, minDate, maxDate, yearStartMonth = 0) | マクロ窓と [minDate, maxDate] の交差 |
| findNearestDateIndex(dates, target, unit, yearStartMonth = 0) | 昇順配列に対する最近傍の二分探索 |
| getSortedAllowedDates(allowedDates, unit, minDate, maxDate, yearStartMonth = 0) | 離散候補の正規化・重複排除・境界絞り込み |
yearStartMonthは必ず末尾の引数です。既存の呼び出しは 1 文字も変えずに済み、0のときの結果は従来と同一です。- 未指定 (
undefined) は仕様上の値である0(暦年) です。nullは既定引数が効かないためRangeErrorになります。12/-1/1.5/NaN/Infinity/"3"も同じく拒否され、近い月へ丸められることはありません。 - 検証は早期 return より前に行われます。空配列を渡した
getSortedAllowedDatesや 1 要素しかないfindNearestDateIndexでも、不正なyearStartMonthが素通りすることはありません。 addUnit(unit, d, amount)は意図的にこの引数を受け取りません。 開始月をずらした年もちょうど 12 か月であり、加算の結果はyearStartMonthに依存しないためです。無視するだけの引数を置けば、「渡せば効く」という嘘の契約を名乗ることになります。
実行時バリデーション (Fail Fast)
本パッケージは欠落値・不正値を既定値や壁時計時刻で代替しません。次の条件では描画時に例外を送出します。
| コンポーネント / 関数 | 条件 | 例外 |
| :--- | :--- | :--- |
| PeriodSlider | minDate が未指定または Invalid Date | RangeError: PeriodSlider: minDate is required and must be a valid Date (received: ...) |
| PeriodSlider | maxDate が未指定または Invalid Date | RangeError: PeriodSlider: maxDate is required and must be a valid Date (received: ...) |
| PeriodSlider | maxDate <= minDate | RangeError: PeriodSlider: maxDate must be strictly later than minDate (...) |
| PeriodSlider (vernier) | macroUnit で minDate と maxDate が同一単位に丸められる | RangeError: PeriodSlider: macroUnit "..." cannot distinguish minDate from maxDate (...) |
| PeriodSlider (vernier) | macroRange の未指定・2 要素タプル以外・Invalid Date・逆転 | RangeError: PeriodSlider: macroRange is required and must be a [start, end] Date tuple (...) / ... macroRange[0] must be a valid Date (...) / ... macroRange must not be inverted (...) |
| PeriodSlider (vernier) | ミクロ選択 Props を 1 つも指定していない | RangeError: PeriodSlider: vernier mode requires a micro selection; supply microRange / defaultMicroRange for a period, or microValue / defaultMicroValue for a point |
| PeriodSlider (vernier) | ミクロ期間系とミクロ時点系の Props を同時に指定 | RangeError: PeriodSlider: vernier mode takes either a micro range (microRange / defaultMicroRange / onChangeMicroRange) or a micro point (microValue / defaultMicroValue / onChangeMicroPoint), never both |
| PeriodSlider (vernier) | 時点系の Props だけを渡し microValue / defaultMicroValue が共に未指定 | RangeError: PeriodSlider: vernier point mode requires a micro selection; supply microValue (controlled) or defaultMicroValue (uncontrolled) |
| PeriodSlider (vernier) | 期間系の Props だけを渡し microRange / defaultMicroRange が共に未指定 | RangeError: PeriodSlider: vernier range mode requires a micro selection; supply microRange (controlled) or defaultMicroRange (uncontrolled) |
| PeriodSlider (vernier) | 指定された microRange / defaultMicroRange が 2 要素タプル以外・Invalid Date・逆転 | RangeError: PeriodSlider: <プロパティ名> ... |
| PeriodSlider (vernier) | 指定された microValue / defaultMicroValue が Invalid Date | RangeError: PeriodSlider: <プロパティ名> must be a valid Date (received: ...) |
| PeriodSlider | 指定された range / defaultRange / value / defaultValue が unit の解像度で [minDate, maxDate] の外 | RangeError: PeriodSlider: <プロパティ名> must lie within [minDate, maxDate] (...) |
| PeriodSlider (vernier) | 指定されたミクロ選択が microUnit の解像度で [minDate, maxDate] の外 | RangeError: PeriodSlider: <プロパティ名> must lie within [minDate, maxDate] (...) (macroRange は定義域をはみ出してよい。レールが定義域と積を取り、通知はクランプ済みの値を配るため) |
| PeriodSlider | allowedDates の全要素が [minDate, maxDate] 外 | RangeError: getSortedAllowedDates: every allowedDates entry falls outside [minDate, maxDate] (...) |
| usePeriodSliderRange / usePeriodSliderPoint / useVernierSliderRange / useVernierSliderPoint | 境界・初期値の欠落、Invalid Date、逆転 | RangeError: <フック名>: ... |
| PeriodSlider | locale が空文字列・構造として不正な BCP 47 タグ・非文字列 | RangeError: PeriodSlider: locale must be a valid BCP 47 language tag (received: ...) |
| DateSelector | 同上 | RangeError: DateSelector: "locale" must be a valid BCP 47 language tag (received ...) |
| MonthSelector / YearSelector | 同上 | RangeError: <コンポーネント名>: `locale` must be a valid BCP 47 language tag, received .... |
| YearSelector / MonthSelector / DateSelector | value と initialDate が共に未指定 | RangeError: <コンポーネント名>: either \value` (controlled) or `initialDate` (uncontrolled) is required; ...|
|YearSelector| 境界の欠落・矛盾、Date / 年 Props の不正値 |RangeError|
|MonthSelector | 境界の矛盾、allowedMonthsを伴わないvariant="dropdown" での境界の欠落、allowedMonthsの要素が Invalid Date またはDateでない (添字付きで報告)、Date / 年 Props の不正値 | ``RangeError: MonthSelector:allowedMonths[1]must be a valid Date, received ....`` 等 |
|DateSelector | Date / 年 Props の不正値、解決後の範囲が空、バリアントが要求する境界の欠落、variant="split-dropdown"へのallowedDates指定 |RangeError` |
定義域の内包は、値が載るレールの解像度で判定します。 レールが描くのは単位まるごと (年・月・日) であるため、minDate が期間の途中を指す場合 (unit="month" に 2026-09-15、unit="day" に時刻を持つ new Date() など) は、その期間まるごと (2026-09-01) がレールの先頭として描かれ、Home キー・ステッパー・ドラッグ・フックはいずれもその日付を確定値として配ります。したがってその日付をそのまま制御 Props (range / value / microRange など) へ戻しても受理されます。1 単位でも外れた日付は従来どおり RangeError です。
DateSelector は構成そのものの矛盾を優先して報告します。variant="split-dropdown" と allowedDates の併用は、value / initialDate の欠落よりも先に判定されます。
カラーテーマ & ダークモード
colorScheme プロパティを指定することで、スライダー全体のベースカラー体系を容易に変更できます。
<PeriodSlider
mode="vernier"
unit="month"
minDate={new Date(2020, 0, 1)}
maxDate={new Date(2026, 11, 31)}
macroRange={macroRange}
colorScheme="indigo-amber" // 🟣 Indigo × 🟠 Amber 配色
/>| プリセット | マクロ色 (--aqps-macro-color) | ミクロ色 (--aqps-micro-color) | アクセント色 (--aqps-accent-color) | 特徴 |
| :--- | :--- | :--- | :--- | :--- |
| "blue-rose" (既定) | Sky (#0284c7) | Rose (#e11d48) | Sky (#0284c7) | 青と赤の鮮明なメリハリ対比 |
| "indigo-amber" | Indigo (#4f46e5) | Amber (#d97706) | Indigo (#4f46e5) | 藍色と琥珀色のモダンなコントラスト |
| "teal-emerald" | Teal (#0d9488) | Emerald (#059669) | Teal (#0d9488) | 自然で落ち着きのあるグリーン・ティール系 |
| "violet-fuchsia" | Violet (#7c3aed) | Fuchsia (#c026d3) | Violet (#7c3aed) | クリエイティブな紫・赤紫系 |
| "slate-sky" | Slate (#475569) | Sky (#0284c7) | Slate (#475569) | エンタープライズ向けクールスレート系 |
| "monochrome" | Zinc (#52525b) | Zinc 950 (#18181b) | Zinc 800 (#27272a) | 無彩色ニュートラル設計 |
アクセント色 (つまみのグロー・波紋) は monochrome を除きマクロ色と同一値です。各プリセットの実値は COLOR_SCHEME_PALETTES からも取得できます。
ダークモードは、祖先要素に .dark クラスを付与するだけで、スライダー (トラック、ノブ、グロー光、目盛線、ツールチップ) および全セレクタ (YearSelector, MonthSelector, DateSelector) が自動でダークパレットに適応します。.dark は文書のルートに限らず任意の部分木に置けます — DateSelector の variant="dropdown" が document.body へポータルするポップアップも、操作要素自身の位置から配色モードを解決するため、部分木に限定した .dark に追従します。
CSS カスタムプロパティ (スタイリング API)
配色・重なり順・インジケーターラベルの見た目と離隔は、すべて --aqps- 接頭辞の CSS カスタムプロパティで差し替えられます。対応する Props はありません。上書き経路は次の 2 つで、どちらも同じ変数名を使います。
// A. style プロップ: そのスライダー 1 台だけに与える (インライン宣言なのでどの CSS にも勝つ)
<PeriodSlider
mode="range"
unit="month"
minDate={new Date(2020, 0, 1)}
maxDate={new Date(2026, 11, 31)}
range={range}
onChangeRange={setRange}
style={{ "--aqps-label-bg": "#1e3a8a", "--aqps-label-radius": "9999px" } as React.CSSProperties}
/>/* B. 自前の CSS: 複数台へまとめて与える */
.dashboard [data-mode] {
--aqps-label-font-size: 0.875rem;
--aqps-label-radius: 9999px;
--aqps-z-label: 60;
}[!IMPORTANT] 配色の 11 変数と、それ以外の 17 変数では、B の書き方が違います。 配色の 11 変数はパッケージ自身が
[data-color-scheme="<preset>"]— スライダーのルート要素そのもの — に宣言しているため、祖先要素 (bodyや:root) に書いた値は届きません (要素自身が持つ宣言が、祖先から継承した値に必ず勝つため)。セレクタがスライダーのルート要素自身に一致する形 (上の例の[data-mode]など) で書いてください。 残る 17 変数はパッケージがどこにも宣言せず、参照側でvar(--aqps-…, <既定値>)のフォールバックとしてのみ既定値を与えます。したがって祖先要素・ルート要素・styleプロップのいずれに書いても届きます。
配色 (colorScheme プリセットが与える 11 変数)
既定値は colorScheme="blue-rose" (既定プリセット) のもので、プリセットを変えると 11 個すべてが入れ替わります。公開エクスポート COLOR_SCHEME_PALETTES から取得できるのは、このうち基調色と明色の 5 つ (macro / macroLight / micro / microLight / accent) だけです。光彩の 3 つと文字色の 2 つは ColorSchemePalette が持たず、スタイルシート側 ([data-color-scheme=…]) だけが値を持ちます。--aqps-macro-color / --aqps-micro-color を上書きする場合は、対になる --aqps-*-on-color も同時に上書きしてコントラスト比 4.5:1 を再確認してください。
| 変数 | 制御対象 | 既定値 |
| :--- | :--- | :--- |
| --aqps-macro-color | マクロバーのグラデーション開始色、マクロ側ラベル背景、マクロ目盛・針 | #0284c7 |
| --aqps-macro-light | マクロバーのグラデーション終了色、マクロ側ラベル枠線、マクロ針 | #38bdf8 |
| --aqps-macro-glow | マクロ側の光彩 (box-shadow / drop-shadow) | rgba(56, 189, 248, 0.4) |
| --aqps-macro-on-color | マクロ側ラベルの文字色 (背景に対し 4.5:1 以上) | #020617 |
| --aqps-micro-color | ミクロバーのグラデーション開始色、ミクロ側ラベル背景 | #e11d48 |
| --aqps-micro-light | ミクロバーのグラデーション終了色、ミクロ側ラベル枠線、ミクロ針 | #fb7185 |
| --aqps-micro-glow | ミクロ側の光彩 | rgba(244, 63, 94, 0.4) |
| --aqps-micro-on-color | ミクロ側ラベルの文字色 (背景に対し 4.5:1 以上) | #ffffff |
| --aqps-accent-color | range / point のつまみ・連結バーの色 | #0284c7 |
| --aqps-accent-light | アクティブ時の光彩色、ダークモードのつまみ塗り、ホバーガイドの点 | #38bdf8 |
| --aqps-accent-glow | アクティブ時の外周グロー、ホバーガイドの点のグロー | rgba(56, 189, 248, 0.4) |
重なり順 (z-index)
既定値は現在の描画順序そのものです (既定のままなら何も動きません)。ホストのヘッダーやモーダルとスライダーの前後関係を調整する場合にだけ触ってください。
| 変数 | 制御対象 | 既定値 |
| :--- | :--- | :--- |
| --aqps-z-handle | 丸つまみ (range / point) の層。前面でも操作中でもない通常状態 | 30 |
| --aqps-z-handle-front | 前面 (data-front="true") / 操作中 (data-active="true") のつまみの層。ノギスの 4 つの半円つまみも含む全つまみが対象 | 40 |
| --aqps-z-label | インジケーターラベルの層 | 31 |
| --aqps-z-label-front | 前面 / 操作中のつまみが持つラベルの層 | 41 |
| --aqps-z-hover-guide | ポインターオーバーレイ (点と追従ラベルを束ねる器) 全体の層。つまみとの前後を決めるのはこちらで、既定では 30 > 20 によりつまみの背面に入る | 20 |
| --aqps-z-hover-tooltip | オーバーレイ内部での追従ラベルと点の前後。器が積層文脈を作るため、この値がつまみへ届くことはない | 25 |
| --aqps-z-handle-wrapper | .period-slider-handle-wrapper の層 | 2 |
| --aqps-z-handle-wrapper-active | .period-slider-handle-wrapper.is-active の層 | 10 |
| --aqps-z-date-popup | DateSelector の variant="dropdown" が開くポップアップの層。document.body へポータルされるため、ホストの層と直接重なるのはこれだけ | 60 |
[!NOTE] ノギスの 4 つの半円つまみは通常状態の層を上下段で変えており (上段 15 / 下段 20)、これはインラインスタイルが持ちます。
--aqps-z-handleはこの 2 値を潰さないよう丸つまみだけに効きます。--aqps-z-handle-wrapper/--aqps-z-handle-wrapper-activeが対象とする.period-slider-handle-wrapperは、現在のコンポーネントが描画する要素ではありません。公開済みのクラス契約として規則だけが残っているため、このクラスを自前のマークアップで使う場合にのみ意味を持ちます。--aqps-z-date-popup以外の 8 つはスライダーのルート要素が作る積層文脈の内側で前後を決めます。ホストのヘッダーやモーダルとの前後を調整する場合、body 直下に立つポップアップだけは別の層にあるため、この 1 つを個別に与えてください。
インジケーターラベルの見た目と離隔
range / point / vernier すべてのつまみが持つラベル (.period-slider-tooltip) が対象です。ポインターオーバーレイの追従ラベルは対象外で、独自の配色・寸法を持ちます。
| 変数 | 制御対象 | 既定値 |
| :--- | :--- | :--- |
| --aqps-label-bg | ラベル背景 (ライト) | oklch(0.208 0.042 265.755) |
| --aqps-label-bg-dark | ラベル背景 (.dark 配下) | oklch(0.279 0.041 260.031) |
| --aqps-label-fg | ラベル文字色 (ライト) | #ffffff |
| --aqps-label-fg-dark | ラベル文字色 (.dark 配下) | #ffffff |
| --aqps-label-font-size | ラベルの文字寸法。行送りは calc(1 / 0.75) の比率で保持するため、値を変えても行箱が追従する | 0.75rem (16px ルートで 12px) |
| --aqps-label-padding | ラベルの内側余白 (padding ショートハンドの任意の形) | 0.125rem 0.5rem |
| --aqps-label-radius | ラベルの角丸 | 0.25rem |
| --aqps-label-rail-gap | ノギスの半円つまみの平面とラベルとの離隔。水平レール (上下) と垂直レール (左右) の 4 方向すべてに効く | 8px |
適用範囲の限界は次のとおりです (いずれも詳細度が高い既存規則が勝つため、意図した優先順位です)。
- ノギスのラベルは配色 2 変数を無視します: マクロ青・ミクロ赤というセマンティック配色 (
--aqps-macro-color/--aqps-micro-color) が--aqps-label-bg/--aqps-label-fgに勝ちます。文字寸法・余白・角丸の 3 変数はノギスのラベルにも効きます。 tooltipStyle="balloon"は見た目 5 項目を固定します: 吹き出し様式は背景・文字色・文字寸法・余白・角丸を自前の値で宣言するため、対応する変数は効きません (文字色は明るい塗りの上で 9.93:1 を確保する暗色です)。--aqps-label-rail-gapは効きます。- ラベルの高さは文字寸法に追従します: 箱の高さは
--aqps-label-font-sizeと同じ比率 (行送りと同一の $4/3$) で書かれているため、文字寸法をどう変えても行箱とボーダーボックスが一致し、文字が箱の外へ出ません (既定の0.75remでは 16px)。箱をさらに大きくしたい場合は--aqps-label-paddingの上下方向を増やしてください (box-sizing: border-boxのため、上下余白と枠線の合計が行箱を超えた分だけ高さが伸びます)。衝突回避の幾何はラベルの実測ボーダーボックスを見ているため、いずれの場合も離隔は破綻しません。
丸つまみとラベルの離隔は変数になっていません
丸つまみ (range / point) とそのラベルの離隔 (水平レイアウトで 8px + 2px、垂直レイアウトで 16px) だけは、上の表に含まれていません。この値は衝突回避の幾何計算の入力であり (傾いたラベルの外接矩形をつまみから逃がす距離を解くのに必要)、CSS には「回転後の外接矩形の半高」を解く手段がないためです。変数にすると JavaScript から getComputedStyle で読み戻すことになり、px 以外の単位が使えない・ホストの CSS を書き換えても次の再描画まで効かない、という他の変数には無い制約を 1 つだけが背負います。設定方法を揃えるため、この経路は公開していません。
離隔を変えたい場合は LabelSlantConfig.anchorGapY を使ってください (公開関数 calculateLabelSlant の引数であり、自前で配置を解く場合の入口です)。ノギスのラベルは幾何計算を通らないため --aqps-label-rail-gap で CSS だけで動かせます。
なぜ !important が付いているのか
ラベルの見た目を決める宣言 (背景・文字色・文字寸法・余白・角丸)、重なり順、data-duplicate の隠蔽、transform 系には !important が付いています。詳細度の問題ではなくカスケードレイヤーの問題です。
パッケージの手書きクラスは @layer components に属し、ホストの Tailwind ユーティリティ (@layer utilities) より前に解決されます。カスケードレイヤーは詳細度より優先されるため、!important の無い宣言は、同じ要素に載るホストのユーティリティへ詳細度に関わらず黙って負けます。過去に tooltipStyle="balloon" の装飾が 2 リリースにわたり半分しか効かなかった原因がこれです (height と border だけが通り、背景も角丸も余白も文字寸法もユーティリティのままだった)。上書きしたい側は、変数へ値を与える (!important は不要) か、style プロップを使ってください — 宣言ごと上書きするのではなく、変数を与えるのが正しい経路です。
各種セレクタ
3 つのセレクタはいずれも value (制御) または initialDate (非制御) で選択値を受け取り、両脇の ◀▶ ステッパーボタンで 1 単位ずつ移動します。
◀▶ は押せる間だけ、ポインターを載せると矢印の色が背景から際立ち (ライトでは濃く、ダークでは明るく) 淡い面が現れ、キーボードフォーカスで青いリングが付きます。押せる範囲は矢印を中心とする 44px 四方で、隣のセレクトボックスや日付入力の縁で止まります。境界に着いて動けない ◀▶ はフォーカスを保ち、タブ順にも残ったまま使用不可 (aria-disabled="true") になり、押しても何も発行しません (キーボードで押し続けて境界に着いても、フォーカスが文書へ落ちません)。ネイティブの disabled が付くのは disabled Props でセレクタ全体を無効にしたときだけで、そのときは全てのバリアントで ◀▶ の両方がセレクトや日付入力と共にタブ順から外れます。
[!NOTE]
variant="date"のネイティブ入力 (<input type="date">/<input type="month">) は、同じ値を示すvariant="dropdown"と並んだときに別物に見えないよう、同じ書体・字送り・面で描かれます。中央寄せだけはユーティリティでは届きません — Tailwind preflight が::-webkit-datetime-editをinline-flexにするためtext-alignが余白を配らないからです。そこで同梱スタイルシートの公開クラス.period-slider-native-date-fieldが、編集領域を全幅化して中央へ寄せ、カレンダー起動子を絶対配置で桁送りから外します。自前のマークアップで同じ揃えが必要な場合はこのクラスを付けてください。preflight を適用しないホストでは編集領域が flex にならず、これらの宣言は無害に無視されます。
import { YearSelector, MonthSelector, DateSelector } from "@aiquants/period-slider"
import { useState } from "react"
const minDate = new Date(2020, 0, 1)
const maxDate = new Date(2030, 11, 31)
export function SelectorsExample() {
const [currentDate, setCurrentDate] = useState<Date>(new Date(2024, 5, 15))
return (
<div className="flex flex-col gap-6">
{/* 年セレクタ (境界必須) */}
<YearSelector
value={currentDate}
minYear={2020}
maxYear={2030}
onChange={(date) => setCurrentDate(date)}
/>
{/* 年月セレクタ (variant="dropdown" は境界必須) */}
<MonthSelector
value={currentDate}
variant="dropdown"
minDate={minDate}
maxDate={maxDate}
onChange={(date) => setCurrentDate(date)}
/>
{/* 年月日セレクタ (既定の variant="date" は境界任意) */}
<DateSelector
value={currentDate}
minDate={minDate}
maxDate={maxDate}
onChange={(date) => setCurrentDate(date)}
/>
</div>
)
}YearSelector Props
年を 1 年刻みで選択するコンポーネント。選択肢は解決後の上限年から下限年までの降順で、境界を跨いで広がることはありません。選択肢のテキストは 4 桁ゼロ埋めの年 (2024) で、locale 指定時は Intl.DateTimeFormat 書式 (ja-JP なら 2024年)。<option value> は常に生の数値です。
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| value | Date | undefined | 制御時の選択日付 (年のみ使用)。initialDate 未指定時は必須 |
| initialDate | Date | undefined | 非制御時の初期選択日付 (年のみ使用)。value 未指定時は必須。value 指定時は無視 |
| minYear | number | undefined | 選択可能な最小年。minDate と併用時は厳しい方 (遅い方) を採用 |
| maxYear | number | undefined | 選択可能な最大年。maxDate と併用時は厳しい方 (早い方) を採用 |
| minDate | Date | undefined | 選択可能な最小日付 (年のみ使用)。minYear と併用時は厳しい方を採用 |
| maxDate | Date | undefined | 選択可能な最大日付 (年のみ使用)。maxYear と併用時は厳しい方を採用 |
| onChange | (date: Date, year: number) => void | undefined | 年変更時コールバック。date は該当年の先頭月 1 日ローカル 0 時 (yearStartMonth が 0 なら 1 月 1 日)、year はその年の番号 |
| disabled | boolean | false | 無効化フラグ。true の間は onChange を発火せず、◀▶ とコンボボックス・セレクト・入力をネイティブに無効化する (タブ順から外れ、ポップアップも開かない) |
| className | string | "" | ルート要素の追加 CSS クラス |
| style | React.CSSProperties | undefined | ルート要素のインラインスタイル |
| children | React.ReactNode | undefined | ルート要素末尾へ追加描画する子要素 |
| prevYearAriaLabel | string | "Previous year" | 前年ボタンのアクセシブル名 |
| nextYearAriaLabel | string | "Next year" | 翌年ボタンのアクセシブル名 |
| selectAriaLabel | string | "Year" | 年セレクトのアクセシブル名 |
| locale | string | undefined | 選択肢テキストのロケール (BCP 47 言語タグ)。未指定は 4 桁ゼロ埋めの年、指定時は Intl.DateTimeFormat 書式。空文字列・不正タグ・非文字列は RangeError |
| unit | — | — | このコンポーネントが宣言していない名前です。フックの SelectorProps をそのままスプレッドできるよう、受け取っても読まれず捨てられます (コンポーネントは必要な Props だけを名前で取り出すため、DOM へも届きません)。型にも宣言が無いので、JSX へ直接書くとコンパイルエラーになります (スプレッドは余剰プロパティ検査を受けないため通ります) |
| yearStartMonth | number | 0 | 1 年が始まる暦月 (0 始まり)。選択肢の年の数え方と onChange が返す日付を決めます (yearStartMonth={3} なら「2025 年」は 2025-04-01 〜 2026-03-31 で、onChange は 2025-04-01 を渡します)。0〜11 の整数以外 (12 / -1 / 1.5 / NaN / null) は RangeError、未指定は仕様上の 0 (暦年) |
value / initialDate / minDate / maxDate は「その日付を含む年」として読みます。yearStartMonth が 0 以外のとき、開始月より前の暦月は 1 つ手前の年に属します (yearStartMonth={3} では 2025-01-15 は「2024 年」で、その年の選択肢テキストも 2024 です)。
下限 (minYear または minDate) と上限 (maxYear または maxDate) は共に必須です。境界外の年を保持している間は、境界へ丸めず・RangeError も送出せず、その年を選択肢へ 1 件加えてそのまま表示します。ステッパーは移動先の年が境界内にある場合だけ有効です (境界の 1 つ外側では戻る側だけが有効、それより外では両方とも使用不可 (aria-disabled="true"、フォーカスは保持))。境界外の年が onChange へ渡ることはありません。
MonthSelector Props
年月を 1 ヶ月刻みで選択するコンポーネント。
| プロパティ | 型 | 既定値 | 説明 |
| :--- | :--- | :--- | :--- |
| value | Date | undefined | 制御時の選択日付 (年月のみ使用)。initialDate 未指定時は必須 |
| initialDate | Date | undefined | 非制御時の初期選択日付 (年月のみ使用)。value 未指定時は必須。value 指定時は無視 |
| variant | `"date" | "dropdo
