有効期限ExpiryBadgeExperimental

期限日を有効、期限間近、失効、未登録の状態に分類して表示する期限バッジです。

プレビュー

30 日以内

適性診断は 22 日後に期限を迎えます。現在の判定は「期限間近」です。

普通二種免許
2029/03/15残991日有効
適性診断(適齢)
2026/07/20残22日期限間近
健康診断
2026/06/1018日超過失効
地理試験合格証
未登録

状態とバリエーション

4つの状態

valid / expiring / expired / missing を、アイコンと文言を含むチップで表示します。

普通二種免許
2029/03/15残991日有効
適性診断(適齢)
2026/07/20残22日期限間近
健康診断
2026/06/1018日超過失効
地理試験合格証
未登録

日付と残日数の制御

showDate と hideRemaining で、一覧密度に合わせて表示量を調整します。

期限間近残22日期限間近2026/07/20

いつから知らせるか

warnWithinDays は「期限間近」に入る日数です。同じ期限日でも、書類ごとに知らせ始める時期は違います。

有効2026/08/05残38日既定(30日前から)
有効2026/08/05残38日健診の運用(14日前から)
期限間近2026/08/05残38日車検の運用(60日前から)

プロパティ

表は横にスクロールできます
プロパティ初期値説明
valuestring | number | Date | null-判定する期限日です。null または未指定は missing になります。
todaystring | number | Datenew Date()比較基準日です。SSR では明示指定します。
warnWithinDaysnumber30この日数以内を expiring と判定します。
showDatebooleantrue状態チップの横に日付を表示します。
hideRemainingbooleanfalse残日数または超過日数を隠します。
formatDate(date: Date) => string-日付表示の整形関数です。
formatRemaining(days: number, state: ExpiryState) => ReactNode-残日数または超過日数の表示文言を整形します。
labelsPartial<Record<ExpiryState, ReactNode>>-状態ラベルを上書きします。
stateClassNamestring-状態チップ部分だけに追加する className です。表や一覧で幅を揃える時に使います。
statePosition"start" | "end""start"状態チップを日付・残日数の前後どちらに置くかを指定します。
classifyExpiry(value, options) => { state, days }-UI なしで同じ期限分類を返す純関数です。

使い方

import * as React from "react";
import { ExpiryBadge, MetadataList, Slider, classifyExpiry } from "@gunjo/ui";

const today = "2026-06-28";
const expiryBadgeAlignClass = "w-[244px] justify-end";
const expiryBadgeStateClass = "w-20 shrink-0 justify-start";
const expiryLabels = {
  valid: "有効",
  expiring: "期限間近",
  expired: "失効",
  missing: "未登録",
} as const;

function formatExpiryRemaining(days: number) {
  if (days < 0) return Math.abs(days) + "日超過";
  if (days === 0) return "本日まで";
  return "残" + days + "日";
}

export function ComplianceExpiryList() {
  const [warnWithinDays, setWarnWithinDays] = React.useState(30);
  const rows = [
    { label: "普通二種免許", value: "2029-03-15" },
    { label: "適性診断(適齢)", value: "2026-07-20" },
    { label: "健康診断", value: "2026-06-10" },
    { label: "地理試験合格証", value: null },
  ];
  const assessment = classifyExpiry("2026-07-20", { today, warnWithinDays });

  return (
    <div className="flex w-full max-w-xl flex-col gap-4 rounded-lg border bg-card p-4">
      <Slider
        min={7}
        max={90}
        step={1}
        value={warnWithinDays}
        onValueChange={setWarnWithinDays}
        aria-label="期限間近の判定日数"
      />
      <p className="text-xs text-muted-foreground">
        適性診断は {assessment.days} 日後に期限を迎えます。現在の判定は「{expiryLabels[assessment.state]}」です。
      </p>
      <MetadataList
        items={rows.map((row) => ({
          label: row.label,
          value: (
            <ExpiryBadge
              value={row.value}
              today={today}
              warnWithinDays={warnWithinDays}
              labels={expiryLabels}
              formatRemaining={formatExpiryRemaining}
              className={expiryBadgeAlignClass}
              stateClassName={expiryBadgeStateClass}
              statePosition="end"
            />
          ),
        }))}
      />
    </div>
  );
}

設計の判断

  • 判定を部品から外に出した。classifyExpiry() は表示を持たない関数で、日付を渡すと「有効・期限間近・失効・未登録」と残り日数を返します。表の並べ替えや件数の集計は、バッジを描かずにこの関数だけで済みます。
  • 色だけに意味を乗せない。4つの状態それぞれに別のアイコンと文字(有効/期限間近/失効/未登録)が付きます。資料も「色だけでステータスを表現しない」を核に挙げています。文字は labels で差し替えられます。
  • 「期限間近」の線は呼ぶ側が引く。warnWithinDays の既定は30日です。車検と資格と保険では警告を出したい時期が違うので、部品の中に固定しませんでした。数のバッジではないので、資料の「0件で非表示」「99件を超えたら99+」の丸めは持ちません。today も props で、サーバーとブラウザで結果がずれないようにしています。
    一般のバッジの設計は UIXHERO の「バッジ」にあります。 UIXHERO: バッジ(Badge)

使用コンポーネント

設計の判断(UIXHERO)

「いつ・なぜ使うか」の判断は、姉妹サイト UIXHERO の記事で解説しています。