form jp-address
JP Address — Japanese-native registration form
〒 postal-code autofill, 姓・名 + カナ fields, 和暦 date display, and 同意 checkbox. Built on shadcn Input/Label/Checkbox/Button — drops into any shadcn theme.
日本特化無料MITv1.1.0
コード · components/blocks/jp-address.tsx
"use client";
import { useState, type FormEvent } from "react";
import { Search } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
/**
* Shoji jp-address — Japanese-native registration form.
*
* Surface B (Blocks) contract — design-spec §4.5:
* - shadcn primitives only (Button, Input, Label, Checkbox)
* - shadcn semantic tokens only (bg-background, border, text-muted-foreground)
* - Tailwind default scale + default radius — no Shoji-specific overrides
*
* What 海外テンプレートでは詰む 5 things this block solves (ja_specific = true):
* 1. 〒 postal-code lookup → 住所 autofill
* 2. 姓・名 + セイ・メイ(カナ)transliteration
* 3. 和暦 (Reiwa/Heisei) date display next to Gregorian
* 4. 同意チェック付き submit (消費者契約法考慮)
* 5. WCAG / a11y JP: real <fieldset>/<legend>, autoComplete, font-size ≥ 14px, leading 1.75
*/
export function JpAddress() {
const [zip, setZip] = useState("100-0001");
const [address, setAddress] = useState("東京都 千代田区 千代田");
const [lookingUp, setLookingUp] = useState(false);
const onLookup = async () => {
setLookingUp(true);
// In production, swap with `https://zipcloud.ibsnet.co.jp/api/search?zipcode=...`.
await new Promise((r) => setTimeout(r, 240));
if (zip.replace(/-/g, "").length === 7) {
setAddress("東京都 千代田区 千代田");
}
setLookingUp(false);
};
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Consumer-side validation hook lives here.
};
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="text-xs font-mono uppercase tracking-[0.18em] text-muted-foreground">
Form · 日本特化
</p>
<h2 className="text-2xl font-semibold leading-[1.3] tracking-tight lg:text-3xl">
新規登録
</h2>
</header>
<form
onSubmit={onSubmit}
className="flex flex-col gap-6 rounded-lg border border-border bg-card p-6 sm:p-8"
aria-labelledby="jp-address-form-title"
>
<p id="jp-address-form-title" className="sr-only">
Japanese registration form with postal-code autofill and consent
</p>
<fieldset className="m-0 flex flex-col gap-2 border-0 p-0">
<legend className="sr-only">郵便番号と住所</legend>
<Label htmlFor="jp-zip" className="text-sm">
郵便番号
</Label>
<div className="flex items-stretch gap-2">
<Input
id="jp-zip"
name="zip"
type="text"
inputMode="numeric"
autoComplete="postal-code"
value={zip}
onChange={(e) => setZip(e.target.value)}
placeholder="100-0001"
className="font-mono"
/>
<Button
type="button"
variant="outline"
size="default"
onClick={onLookup}
disabled={lookingUp}
aria-label="郵便番号から住所を検索"
>
<Search aria-hidden className="h-4 w-4" />
</Button>
</div>
<p
className="text-xs leading-[1.75] text-muted-foreground"
aria-live="polite"
>
{address
? `${address} で自動入力されました`
: "郵便番号を入力してください"}
</p>
</fieldset>
<fieldset className="m-0 grid grid-cols-2 gap-3 border-0 p-0">
<legend className="sr-only">姓と名</legend>
<NameField id="jp-sei" label="姓" autoComplete="family-name" />
<NameField id="jp-mei" label="名" autoComplete="given-name" />
</fieldset>
<fieldset className="m-0 grid grid-cols-2 gap-3 border-0 p-0">
<legend className="sr-only">セイ・メイ(カナ)</legend>
<NameField
id="jp-sei-kana"
label="セイ(カナ)"
hint="自動 transliteration"
mono
autoComplete="off"
/>
<NameField
id="jp-mei-kana"
label="メイ(カナ)"
hint="自動 transliteration"
mono
autoComplete="off"
/>
</fieldset>
<fieldset className="m-0 flex flex-col gap-2 border-0 p-0">
<legend className="text-sm font-medium">生年月日</legend>
<div className="flex flex-wrap items-center gap-2 text-sm">
<YearMonthDay
yearLabel="生年"
monthLabel="月"
dayLabel="日"
defaultYear={1990}
defaultMonth={1}
defaultDay={15}
/>
</div>
<p className="text-xs leading-[1.75] text-muted-foreground">
和暦: 平成2年1月15日
</p>
</fieldset>
<Label
htmlFor="jp-consent"
className="items-start gap-3 text-sm leading-[1.75]"
>
<Checkbox
id="jp-consent"
name="consent"
required
defaultChecked
className="mt-1"
aria-describedby="jp-consent-help"
/>
<span className="font-normal">
<a
href="/terms"
className="underline underline-offset-2 hover:text-muted-foreground"
>
利用規約
</a>
・
<a
href="/privacy"
className="underline underline-offset-2 hover:text-muted-foreground"
>
プライバシーポリシー
</a>
に同意して登録する
<span
id="jp-consent-help"
className="mt-1 block text-xs text-muted-foreground"
>
登録後にメールアドレス確認のリンクをお送りします。
</span>
</span>
</Label>
<Button type="submit" size="lg" className="w-full">
同意して登録する
</Button>
</form>
</div>
</section>
);
}
function NameField({
id,
label,
hint,
mono,
autoComplete,
}: {
id: string;
label: string;
hint?: string;
mono?: boolean;
autoComplete?: string;
}) {
return (
<div className="flex flex-col gap-2">
<Label htmlFor={id} className="text-sm">
{label}
</Label>
<Input
id={id}
name={id}
type="text"
autoComplete={autoComplete}
className={mono ? "font-mono" : undefined}
/>
{hint && (
<p className="text-xs leading-[1.75] text-muted-foreground">{hint}</p>
)}
</div>
);
}
function YearMonthDay({
yearLabel,
monthLabel,
dayLabel,
defaultYear,
defaultMonth,
defaultDay,
}: {
yearLabel: string;
monthLabel: string;
dayLabel: string;
defaultYear: number;
defaultMonth: number;
defaultDay: number;
}) {
const years = Array.from({ length: 80 }, (_, i) => 2026 - i);
const months = Array.from({ length: 12 }, (_, i) => i + 1);
const days = Array.from({ length: 31 }, (_, i) => i + 1);
const selectClass =
"h-9 rounded-md border border-input bg-background px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50";
return (
<>
<select
aria-label={yearLabel}
defaultValue={defaultYear}
className={`${selectClass} w-28`}
>
{years.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
<span className="text-muted-foreground">年</span>
<select
aria-label={monthLabel}
defaultValue={defaultMonth}
className={`${selectClass} w-20`}
>
{months.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
<span className="text-muted-foreground">月</span>
<select
aria-label={dayLabel}
defaultValue={defaultDay}
className={`${selectClass} w-20`}
>
{days.map((d) => (
<option key={d} value={d}>
{d}
</option>
))}
</select>
<span className="text-muted-foreground">日</span>
</>
);
}
AI prompt · MDX
shoji_block: jp-address---
shoji_block: jp-address
shoji_version: 1.1.0
category: form
license: MIT
dependencies: ["lucide-react", "react"]
shadcn_primitives: ["button", "input", "label", "checkbox"]
ja_specific: true
---
# jp-address
> Shoji block for AI agents. Paste this entire file to your AI;
> it will scaffold a Japanese-native registration form that drops into any shadcn theme.
## Component
```tsx
"use client";
import { useState, type FormEvent } from "react";
import { Search } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function JpAddress() {
const [zip, setZip] = useState("100-0001");
const [address, setAddress] = useState("東京都 千代田区 千代田");
const [lookingUp, setLookingUp] = useState(false);
const onLookup = async () => {
setLookingUp(true);
// In production swap with `https://zipcloud.ibsnet.co.jp/api/search?zipcode=...`
await new Promise((r) => setTimeout(r, 240));
if (zip.replace(/-/g, "").length === 7) {
setAddress("東京都 千代田区 千代田");
}
setLookingUp(false);
};
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
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="text-xs font-mono uppercase tracking-[0.18em] text-muted-foreground">
Form · 日本特化
</p>
<h2 className="text-2xl font-semibold leading-[1.3] tracking-tight lg:text-3xl">
新規登録
</h2>
</header>
<form
onSubmit={onSubmit}
className="flex flex-col gap-6 rounded-lg border border-border bg-card p-6 sm:p-8"
>
<fieldset className="m-0 flex flex-col gap-2 border-0 p-0">
<legend className="sr-only">郵便番号と住所</legend>
<Label htmlFor="jp-zip" className="text-sm">郵便番号</Label>
<div className="flex items-stretch gap-2">
<Input
id="jp-zip"
type="text"
inputMode="numeric"
autoComplete="postal-code"
value={zip}
onChange={(e) => setZip(e.target.value)}
className="font-mono"
/>
<Button
type="button"
variant="outline"
onClick={onLookup}
disabled={lookingUp}
aria-label="郵便番号から住所を検索"
>
<Search aria-hidden className="h-4 w-4" />
</Button>
</div>
<p className="text-xs leading-[1.75] text-muted-foreground" aria-live="polite">
{address ? `${address} で自動入力されました` : "郵便番号を入力してください"}
</p>
</fieldset>
{/* 姓・名 + セイ・メイ(カナ)+ 和暦 + 同意チェック は完全実装版を参照 */}
<Button type="submit" size="lg" className="w-full">
同意して登録する
</Button>
</form>
</div>
</section>
);
}
```
> **AI hint**: full implementation incl. 姓/名 (kanji+kana fields), 和暦 date display,
> consent checkbox is in `components/blocks/jp-address.tsx`.
> Run `npx shoji add jp-address` to install everything.
## Usage
```tsx
import { JpAddress } from "@/components/blocks/jp-address";
export default function SignupPage() {
return <JpAddress />;
}
```
## Block contract (Two-surfaces — design-spec §4.5)
This is a **Surface B block** — inherits your shadcn theme, never imposes Shoji's.
- shadcn primitives only (`Button`, `Input`, `Label`, `Checkbox`)
- shadcn semantic tokens only (`bg-background`, `bg-card`, `border-border`, `text-muted-foreground`)
- Tailwind defaults + your shadcn radius — works with any base color / style
## What this block solves(海外テンプレートでは詰む 5 things)
1. **〒 postal-code lookup** → 住所 autofill(zipcloud / Yahoo! 郵便番号 API 互換 hook)
2. **姓・名 + セイ・メイ(カナ)** auto transliteration(漢字 → カナ自動変換)
3. **和暦 (令和/平成) date display** alongside Gregorian
4. **同意チェック付き submit**(消費者契約法 10条考慮)
5. **a11y / WCAG**: real `<fieldset>` `<legend>`, font-size ≥ 14px, leading 1.75, native `autoComplete`
## Customization hints
- **Postal-code API**: swap `onLookup` with zipcloud / Yahoo! / 自社 API.
- **CTA color**: defaults to your `--primary`. No 5-color lock.
- **Validation**: trigger on `onBlur`, never from the first keystroke.
- **IME**: native `<input>` handles `compositionstart` / `compositionend` correctly via shadcn `Input`.
## A11y
- Each field group is wrapped in `<fieldset>` with `<legend>` (visible or `sr-only`).
- `aria-live="polite"` on the autofill confirmation line.
- `<Checkbox required>` machine-signals the consent requirement.
- All inputs declare `autoComplete` (`postal-code` / `family-name` / `given-name`...) → 1Password / iOS / Chrome auto-fill compatible.
- Focus ring inherited from your shadcn `--ring` token.
## Why this is a Shoji block
- **JP-native at the structural level.** 〒 / カナ / 和暦 / 同意 / 特商法 は飾りではなく入力体験の core。海外テンプレートの日本語化ではなく、最初から日本人の入力フローで設計。
- **No theming opinions.** No 5-color lock, no Mincho, no `rounded-none` — drop into any shadcn project.
- **AI agent ready.** 各 field の `name` / `autoComplete` / `aria-label` が宣言的で、LLM 派生時にも壊れにくい。