form mynumber-input
MyNumber Input — 個人番号 12 桁入力(チェックデジット検証)
個人番号法施行令 第 8 条準拠の check digit 計算。4-4-4 区切り表示・default masked・visibility toggle・autoComplete=off で privacy 最優先。
日本特化無料MITv1.1.0
コード · components/blocks/mynumber-input.tsx
"use client";
import { useState } from "react";
import { AlertCircle, Eye, EyeOff, ShieldCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
/**
* Shoji mynumber-input — Japanese individual number (12-digit) input with check-digit validation.
*
* Surface B (Blocks) contract — design-spec §4.5:
* - shadcn primitives only (Button, Input, Label)
* - shadcn semantic tokens only (bg-background, text-foreground, border, text-destructive)
* - Tailwind default scale + default radius — no Shoji-specific overrides
*
* What this block solves (ja_specific = true):
* 1. 12 桁の数字入力 — onChange で数字以外を即時除去
* 2. 4-4-4 区切りの視覚フォーマットを表示時のみ適用、内部値は素の 12 桁
* 3. チェックデジット検証(個人番号法のアルゴリズム — 11 桁 + 1 桁)
* 4. 数値マスキング (●●●● ●●●● ●●●●) を default にし、目アイコンで切り替え
* 5. inputMode="numeric" + autoComplete="off" でモバイル最適化 + autofill 抑止
*
* Reference: 個人番号法施行令 第 8 条 — 12 桁の最後の 1 桁は前 11 桁の重み付け合計から導出。
*/
const WEIGHTS = [6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
function computeCheckDigit(eleven: string): number | null {
if (!/^\d{11}$/.test(eleven)) return null;
const sum = WEIGHTS.reduce(
(acc, w, i) => acc + w * Number(eleven.charAt(i)),
0,
);
const remainder = sum % 11;
return remainder <= 1 ? 0 : 11 - remainder;
}
function isValidMyNumber(twelve: string): boolean {
if (!/^\d{12}$/.test(twelve)) return false;
const expected = computeCheckDigit(twelve.slice(0, 11));
if (expected === null) return false;
return expected === Number(twelve.charAt(11));
}
function formatGrouped(digits: string, masked: boolean): string {
const padded = digits.padEnd(12, "·").slice(0, 12);
const display = masked
? padded.replace(/\d/g, "●").replace(/·/g, "·")
: padded;
return [display.slice(0, 4), display.slice(4, 8), display.slice(8, 12)].join(
" ",
);
}
export function MyNumberInput() {
const [digits, setDigits] = useState("");
const [touched, setTouched] = useState(false);
const [revealed, setRevealed] = useState(false);
const valid = digits.length === 12 && isValidMyNumber(digits);
const showError = touched && digits.length > 0 && !valid;
return (
<section className="bg-background text-foreground">
<div className="mx-auto w-full max-w-xl px-6 py-16 lg:py-24">
<header className="mb-10 flex flex-col gap-2">
<p className="font-mono text-xs uppercase tracking-[0.18em] text-muted-foreground">
Form · 日本特化
</p>
<h2 className="text-2xl font-semibold leading-[1.3] tracking-tight lg:text-3xl">
個人番号(マイナンバー)
</h2>
<p className="text-sm leading-[1.75] text-muted-foreground">
12 桁の数字を入力してください。チェックデジットで自動検証します。番号は規約に基づき
<span className="font-medium"> 暗号化保管 </span>されます。
</p>
</header>
<fieldset className="flex flex-col gap-6 rounded-lg border border-border bg-card p-6 sm:p-8">
<legend className="sr-only">マイナンバー入力</legend>
<div className="flex flex-col gap-2">
<Label htmlFor="mynumber-input" className="flex items-center gap-2">
個人番号
<span className="text-xs text-muted-foreground">必須</span>
</Label>
<div className="flex items-center gap-2">
<Input
id="mynumber-input"
type={revealed ? "text" : "password"}
inputMode="numeric"
pattern="\d{12}"
autoComplete="off"
placeholder="1234 5678 9012"
value={digits}
maxLength={12}
onChange={(e) =>
setDigits(e.target.value.replace(/\D/g, "").slice(0, 12))
}
onBlur={() => setTouched(true)}
aria-describedby={
showError ? "mynumber-input-error" : "mynumber-input-help"
}
aria-invalid={showError ? true : undefined}
className={`tabular-nums tracking-[0.2em] ${
showError
? "border-destructive focus-visible:ring-destructive/40"
: ""
}`}
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setRevealed((r) => !r)}
aria-label={
revealed ? "個人番号を隠す" : "個人番号を表示する"
}
>
{revealed ? (
<EyeOff aria-hidden className="h-4 w-4" />
) : (
<Eye aria-hidden className="h-4 w-4" />
)}
</Button>
</div>
<p
id="mynumber-input-help"
className="font-mono text-xs tabular-nums text-muted-foreground"
aria-live="polite"
>
{formatGrouped(digits, !revealed)}
</p>
{showError ? (
<p
id="mynumber-input-error"
className="flex items-start gap-1.5 text-sm text-destructive"
>
<AlertCircle
aria-hidden
className="mt-0.5 h-3.5 w-3.5 shrink-0"
/>
<span>
{digits.length < 12
? "12 桁すべての数字を入力してください。"
: "チェックデジットが一致しません。番号をご確認ください。"}
</span>
</p>
) : null}
{valid ? (
<p
role="status"
className="flex items-start gap-1.5 text-sm text-emerald-600 dark:text-emerald-400"
>
<ShieldCheck
aria-hidden
className="mt-0.5 h-3.5 w-3.5 shrink-0"
/>
<span>形式・チェックデジットともに有効です。</span>
</p>
) : null}
</div>
</fieldset>
</div>
</section>
);
}
AI prompt · MDX
shoji_block: mynumber-input---
shoji_block: mynumber-input
shoji_version: 1.1.0
category: form
license: MIT
dependencies: ["lucide-react"]
shadcn_primitives: ["button", "input", "label"]
ja_specific: true
---
# mynumber-input
> Shoji block for AI agents. Paste this MDX to your AI; it scaffolds a 12-digit
> 個人番号 (My Number) input with check-digit validation and visibility toggle.
## Component
```tsx
"use client";
import { useState } from "react";
import { AlertCircle, Eye, EyeOff, ShieldCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const WEIGHTS = [6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
function computeCheckDigit(eleven: string): number | null {
if (!/^\d{11}$/.test(eleven)) return null;
const sum = WEIGHTS.reduce(
(acc, w, i) => acc + w * Number(eleven.charAt(i)),
0,
);
const remainder = sum % 11;
return remainder <= 1 ? 0 : 11 - remainder;
}
function isValidMyNumber(twelve: string): boolean {
if (!/^\d{12}$/.test(twelve)) return false;
const expected = computeCheckDigit(twelve.slice(0, 11));
return expected !== null && expected === Number(twelve.charAt(11));
}
```
## Usage
```tsx
import { MyNumberInput } from "@/components/blocks/mynumber-input";
export default function Page() {
return <MyNumberInput />;
}
```
## Block contract (Two-surfaces — design-spec §4.5)
This is a **Surface B block** — it inherits your shadcn theme.
- shadcn primitives only (`Button`, `Input`, `Label`)
- shadcn semantic tokens only (`bg-background`, `text-foreground`, `border`, `text-destructive`)
- Tailwind default scale + your shadcn radius
## What it solves (ja_specific = true)
1. **個人番号法準拠の check digit**: implements 個人番号法施行令 第 8 条 — weighted-sum mod-11 over the first 11 digits. Most JP libraries get this wrong by mishandling the `remainder ≤ 1 → 0` case.
2. **Default-masked display**: `<input type="password">` plus a 4-4-4 visual format (`●●●● ●●●● ●●●●`). Toggle to plaintext via the eye-icon button.
3. **Strict numeric input**: `onChange` strips non-digits and clips at 12, so the parent state is always normalized to either `""` or 12 numeric chars.
4. **3-state feedback**: empty → no error, partial → "12 桁すべての…", complete-but-invalid → "チェックデジットが一致しません。", complete-valid → green success row with `ShieldCheck` icon.
5. **`autoComplete="off"`**: prevents browser autofill from leaking sensitive numbers into the wrong field.
## Customization hints
- **Always-revealed mode**: drop the `Eye` toggle if your form is admin-only and unmasked entry is acceptable.
- **Encryption note**: the help copy currently says "暗号化保管" — replace with your actual data-handling commitment, or wire to a tooltip linking to your privacy notice.
- **Server-side double-check**: this block validates client-side only. Always re-run `isValidMyNumber` server-side before persisting.
## A11y
- The visibility toggle has a context-aware `aria-label` ("個人番号を隠す" / "個人番号を表示する").
- The grouped 4-4-4 display lives in a separate `<p aria-live="polite">` so screen readers announce changes without trapping focus.
- Errors use the 3-point pattern: color (`text-destructive`) + `AlertCircle` icon + descriptive copy.
- Success uses `role="status"` + `ShieldCheck` icon + green text — never green alone.
- `aria-describedby` switches between the help row and the error row depending on state.
## Why this is a Shoji block
- **JP-tuned by default.** The check-digit weights and mod-11 carry-rule are baked in. Form generators usually skip this and silently accept invalid numbers.
- **AI agent ready.** This MDX exposes the entire validation logic + UX rationale as one paste.
- **Privacy-first defaults.** Masked by default, autocomplete off, no analytics hooks. Surface only the digits the consumer typed.
- **No theming opinions.** Pure shadcn primitives + semantic tokens — drop into any shadcn project.