日本の住所入力フォーム設計:都道府県・市区町村・番地・建物名
日本の住所入力フォームを都道府県・市区町村・町域・建物名にどう分けるか、autocomplete 属性の指定、都道府県の入力方式、バリデーションとアクセシビリティの考え方を、動くJSXコード付きで整理します。
執筆:住所APIナビ編集部
住所入力は項目が多く、入力の負担が大きくなりやすい箇所です。ここでは日本の住所をどの単位で分割し、autocomplete をどう指定し、どこまで検証するかを、実装できる形でまとめます。
動作環境
以下の組み合わせを前提に書いています。掲載しているコードは構文チェックを自動で行っていますが、 断片であるため完全な型チェックは通していません。ご自身の環境で確認してください。
- Node.js 24.x
- Next.js 16(App Router)
- TypeScript 5.9(strict)
- React 19
郵便番号から住所を引く部分は Next.jsで郵便番号から住所を自動入力する方法 で作った /api/postal-code を呼び出す前提です。まだ用意していない場合も、そのエンドポイントを差し替えればこのフォームはそのまま動きます。
1. 住所欄をどう分割するか
最低限、次の4つに分けます。
| 欄 | 例 | 自動補完 | 必須 |
|---|---|---|---|
| 都道府県 | 東京都 | する | 必須 |
| 市区町村 | 千代田区 | する | 必須 |
| 町域・番地 | 丸の内1-1-1 | 町域まで | 必須 |
| 建物名・部屋番号 | 〇〇ビル 5F | しない | 任意 |
1行の自由入力だけにしない理由は、後工程が困るからです。
- 配送業者の伝票は都道府県と市区町村を分けて持つことが多く、1行の文字列から機械的に切り出すと「京都府京都市」「広島県広島市」のような重複表記や、政令市の区の扱いで崩れます。
- 建物名が混ざると、町域の照合が通りません。「〇〇ビル」は住所データベースに存在しないため、実在チェックをかけると弾かれます。
- 集計や配送エリア判定が都道府県単位でできなくなります。後から文字列を分解し直すことになり、そこで表記ゆれの問題を引き受けることになります。
逆に、分けすぎるのも良くありません。「町域」と「番地」を別欄にすると、郵便番号から町域だけを埋めた後、ユーザーは番地欄にカーソルを移して続きを打つ必要があります。日本の住所は町域と番地の境目が曖昧なケースが多いため、MVP では「町域・番地」を1欄にまとめ、町域までを自動で埋めて、続きをユーザーに打ってもらう形が扱いやすいです。
2. autocomplete 属性を正しく指定する
ブラウザの自動入力は autocomplete のトークンを見て動きます。**APIを叩くより速く、通信も発生しません。**APIを入れる前に、まずここを正しく埋めることをおすすめします。
MDN のトークン定義に従うと、日本の住所欄は次のように対応します。
| 欄 | トークン | MDN の定義(要約) |
|---|---|---|
| 郵便番号 | postal-code | 郵便番号 |
| 都道府県 | address-level1 | 住所の第1行政レベル。国によって州・県・カントンなど |
| 市区町村 | address-level2 | 第2行政レベル。市・町・村などの自治体 |
| 町域・番地 | address-line1 | 街区住所の1行目 |
| 建物名・部屋番号 | address-line2 | 街区住所の2行目 |
| 国 | country-name | 国・地域名 |
address-level1 は「州・県」に相当するので都道府県、address-level2 は「市・町・村」なので市区町村です。この2つを逆にすると、ブラウザが保存している住所と噛み合わず、自動入力が黙って外れます。
name 属性もトークンと揃えておくと、ブラウザによっては推定の精度が上がります。どちらか一方しか書けない場合は autocomplete を優先します。
3. 都道府県は <select> か <input> か
どちらにも欠点があります。
<select> の場合
- 良い点:入力ミスが構造的に起きない。値が47通りに固定されるので、後段の分岐やバリデーションが単純になる。
- 悪い点:モバイルでは47件のホイールやリストから探すことになり、スクロール量が多い。「宮城県」と「宮崎県」のような似た名前を選び間違える事故も起きる。
<input> の場合
- 良い点:キーボードで数文字打てば終わる。ブラウザの自動入力とも相性が良い。
- 悪い点:「東京」「東京都」「とうきょうと」が混ざる。サーバー側で正規化する処理が必ず必要になる。
折衷案は <input> + <datalist> です。候補は出しつつ、自由入力も許します。
// 候補は出すが、入力自体は自由。値の正規化はサーバー側で行う。
<label htmlFor="prefecture">都道府県</label>
<input
id="prefecture"
name="address-level1"
autoComplete="address-level1"
list="prefecture-options"
value={prefecture}
onChange={(event) => setPrefecture(event.target.value)}
/>
<datalist id="prefecture-options">
{PREFECTURES.map((name) => (
<option key={name} value={name} />
))}
</datalist><datalist> はブラウザによって表示や絞り込みの挙動が異なります。候補が出ない環境でも入力できることが前提なので、サーバー側での正規化は必ず用意してください。
判断の目安としては、配送先として都道府県を厳密に持ちたいなら <select>、入力速度を優先するなら <input> + <datalist> です。この記事のコード例では、値が固定される <select> を採用しています。
47件の一覧は定数として切り出しておきます。
// src/lib/prefectures.ts
/** 都道府県コード順(JIS X 0401)。表示順もこの並びを使う。 */
export const PREFECTURES = [
'北海道',
'青森県',
'岩手県',
'宮城県',
'秋田県',
'山形県',
'福島県',
'茨城県',
'栃木県',
'群馬県',
'埼玉県',
'千葉県',
'東京都',
'神奈川県',
'新潟県',
'富山県',
'石川県',
'福井県',
'山梨県',
'長野県',
'岐阜県',
'静岡県',
'愛知県',
'三重県',
'滋賀県',
'京都府',
'大阪府',
'兵庫県',
'奈良県',
'和歌山県',
'鳥取県',
'島根県',
'岡山県',
'広島県',
'山口県',
'徳島県',
'香川県',
'愛媛県',
'高知県',
'福岡県',
'佐賀県',
'長崎県',
'熊本県',
'大分県',
'宮崎県',
'鹿児島県',
'沖縄県',
] as const
export type Prefecture = (typeof PREFECTURES)[number]4. 番地・建物名は自動補完しない
郵便番号APIが返すのは町域までです。番地や建物名を返すAPIではありません。
やってはいけないのは、町域を埋めたついでに番地欄にも何かを入れてしまう実装です。ユーザーは埋まっている欄を読み飛ばすため、間違った番地のまま送信されます。空欄なら気づけますが、もっともらしく間違っている値には気づけません。
- 町域まで埋めたら、カーソルは番地の続きに置くか、番地欄を空のまま残してユーザーに委ねます。
- 建物名・部屋番号の欄は常に空にします。自動入力の対象にもしません。
- 補完後に埋まった欄を読み取り専用にしないでください。APIのデータが古い場合、ユーザーが直せなくなります。
5. バリデーションは緩めにする
**厳しすぎる正規表現は、実在する住所を弾きます。**日本の住所には、汎用的なパターンから外れる書き方が普通に存在します。
- 京都の通り名:「京都市中京区烏丸通御池上る」のように、「通」「上る」「下る」「西入」といった語が入ります。数字とハイフンだけを想定した検証は通りません。
- 北海道の条丁目:「札幌市中央区北一条西二丁目」のように、町域自体に「条」「丁目」が含まれます。「丁目」を番地側のものと決め打ちすると壊れます。
- 沖縄の「字」:「中頭郡北谷町字美浜」のように「字(あざ)」「大字」が入ります。「字」を不正文字として弾く実装を時々見かけます。
- そのほか、「一ノ関」「三ヶ日」のような漢数字・「ノ」「ヶ」の混在、ローマ字表記の建物名、アルファベット入りの部屋番号なども普通にあります。
実務的な落としどころは次のとおりです。
// src/lib/address-validation.ts
export type FieldError = { field: string; message: string }
const MAX_LENGTHS = {
addressLine1: 100,
addressLine2: 100,
} as const
/**
* 住所欄の検証。「実在するか」は判定しない。
* 弾くのは「空である」「長すぎる」「制御文字が混ざっている」の3つだけ。
*/
export function validateAddress(input: {
prefecture: string
city: string
addressLine1: string
addressLine2: string
}): FieldError[] {
const errors: FieldError[] = []
// 制御文字(改行・タブ・NULLなど)はコピペで紛れ込む。表示崩れの原因になる。
// eslint-disable-next-line no-control-regex
const hasControlChar = (value: string) => /[\u0000-\u001F\u007F]/u.test(value)
if (input.prefecture.trim() === '') {
errors.push({
field: 'prefecture',
message: '都道府県を選択してください。',
})
}
if (input.city.trim() === '') {
errors.push({ field: 'city', message: '市区町村を入力してください。' })
}
if (input.addressLine1.trim() === '') {
errors.push({
field: 'addressLine1',
message: '町域・番地を入力してください。',
})
} else if (input.addressLine1.length > MAX_LENGTHS.addressLine1) {
errors.push({
field: 'addressLine1',
message: `町域・番地は${MAX_LENGTHS.addressLine1}文字以内で入力してください。`,
})
}
// 建物名は任意。入力されたときだけ長さを見る。
if (input.addressLine2.length > MAX_LENGTHS.addressLine2) {
errors.push({
field: 'addressLine2',
message: `建物名・部屋番号は${MAX_LENGTHS.addressLine2}文字以内で入力してください。`,
})
}
for (const [field, value] of Object.entries(input)) {
if (hasControlChar(value)) {
errors.push({ field, message: '使用できない文字が含まれています。' })
}
}
return errors
}**「番地が含まれていないようです」は、エラーではなく警告として出します。**送信をブロックしてはいけません。番地のない住所(大字だけで届く地域、私書箱など)は実在します。
全角・半角やハイフンの種類を揃えたい場合は、検証ではなく正規化の仕事です。挙動は住所整形ツールで確認できます。文字列を都道府県・市区町村・町名・番地に分解したい場合は Geolonia normalize-japanese-addresses のようなライブラリが使えます。
6. アクセシビリティ
<label> を必ず関連付ける。 W3C WAI の Labeling Controls は、label の for 属性が対象コントロールの id と正確に一致する必要があると述べています。プレースホルダは入力が始まると消えるため、ラベルの代わりにはなりません。ラベルを視覚的に隠す場合も、コード上には残します。
エラーは色だけで伝えない。 W3C WAI の User Notifications は、色分けだけに頼らず、ラベルやテキストを併用するよう求めています。赤枠に加えて、必ずテキストのメッセージを出してください。
エラーメッセージは対象の欄に紐づける。 aria-describedby でメッセージ要素を指し、aria-invalid を立てます。メッセージには「何が問題か」と「どう直すか」を書きます。「入力が不正です」だけでは直せません。
非同期の結果は aria-live で通知する。 郵便番号から住所を埋める処理は、画面が勝手に変わる操作です。緊急でない通知なので aria-live="polite"、送信をブロックするエラーには role="alert" を使い分けます。
キーボードだけで完了できるようにする。 補完が終わった瞬間にフォーカスを別の欄へ飛ばすのは避けてください。スクリーンリーダー利用者は、自分がどこにいるか分からなくなります。フォーカスは動かさず、aria-live で「住所を入力しました」と伝えるだけにします。
7. フォーム全体のコード例
Server Component ではなく、通常のクライアントコンポーネントです。入力状態を持つため 'use client' が必要になります。
// src/components/AddressForm.tsx
'use client'
import { useId, useState } from 'react'
import { PREFECTURES } from '@/lib/prefectures'
type Candidate = { prefecture: string; city: string; town: string }
type LookupStatus = 'idle' | 'loading' | 'empty' | 'error'
const LOOKUP_TIMEOUT_MS = 5000
const STATUS_MESSAGES: Record<LookupStatus, string> = {
idle: '',
loading: '住所を検索しています。',
empty: '該当する住所が見つかりませんでした。都道府県から入力してください。',
error: '住所を取得できませんでした。そのまま手入力で先に進めます。',
}
export function AddressForm() {
// 同じページに複数フォームが載っても id が衝突しないようにする。
const uid = useId()
const [prefecture, setPrefecture] = useState('')
const [city, setCity] = useState('')
const [addressLine1, setAddressLine1] = useState('')
const [status, setStatus] = useState<LookupStatus>('idle')
async function lookup(rawPostalCode: string) {
// 全角数字とハイフンを落として7桁にそろう場合だけ問い合わせる。
const digits = rawPostalCode.normalize('NFKC').replace(/[^0-9]/g, '')
if (digits.length !== 7) {
return
}
setStatus('loading')
try {
const response = await fetch(`/api/postal-code?zipcode=${digits}`, {
// 応答が返らないまま待ち続けないよう必ず打ち切る。
signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS),
})
if (!response.ok) {
setStatus('error')
return
}
const data = (await response.json()) as { candidates?: Candidate[] }
const first = data.candidates?.[0]
// 該当なしはエラーではない。手入力に切り替えてもらう。
if (!first) {
setStatus('empty')
return
}
setPrefecture(first.prefecture)
setCity(first.city)
// 町域までしか埋めない。番地の続きはユーザーが打つ。
setAddressLine1(first.town)
setStatus('idle')
} catch {
// タイムアウト(TimeoutError)も通信断もここに来る。どちらも手入力で回復できる。
setStatus('error')
}
}
return (
<form action="/checkout/confirm" method="post">
<div>
<label htmlFor={`${uid}-postal`}>郵便番号</label>
<input
id={`${uid}-postal`}
name="postal-code"
type="text"
inputMode="numeric"
autoComplete="postal-code"
maxLength={8}
aria-describedby={`${uid}-postal-hint`}
// 1文字ごとに叩かない。欄を離れたときに1回だけ。
onBlur={(event) => void lookup(event.target.value)}
/>
<p id={`${uid}-postal-hint`}>
ハイフンはあってもなくてもかまいません。
</p>
</div>
<div>
<label htmlFor={`${uid}-prefecture`}>都道府県</label>
<select
id={`${uid}-prefecture`}
name="address-level1"
autoComplete="address-level1"
required
value={prefecture}
onChange={(event) => setPrefecture(event.target.value)}
>
<option value="">選択してください</option>
{PREFECTURES.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</div>
<div>
<label htmlFor={`${uid}-city`}>市区町村</label>
<input
id={`${uid}-city`}
name="address-level2"
autoComplete="address-level2"
required
value={city}
onChange={(event) => setCity(event.target.value)}
/>
</div>
<div>
<label htmlFor={`${uid}-line1`}>町域・番地</label>
<input
id={`${uid}-line1`}
name="address-line1"
autoComplete="address-line1"
required
value={addressLine1}
onChange={(event) => setAddressLine1(event.target.value)}
/>
</div>
<div>
{/* 建物名は自動補完しないので state を持つ必要もない。 */}
<label htmlFor={`${uid}-line2`}>建物名・部屋番号(任意)</label>
<input
id={`${uid}-line2`}
name="address-line2"
autoComplete="address-line2"
/>
</div>
{/* 補完の結果は色ではなくテキストで伝え、フォーカスは動かさない。 */}
<p role="status" aria-live="polite">
{STATUS_MESSAGES[status]}
</p>
<button type="submit">確認画面へ</button>
</form>
)
}見落としやすい点
複数候補を1件目で決め打ちしない。 同じ郵便番号に複数の町域が対応することがあります。candidates が2件以上なら選択UIを出します。
補完が失敗してもフォームは止めない。 住所補完は補助機能です。APIが落ちている間も注文を受けられる状態にしておきます。
送信直前に整形しない。 ユーザーの見ている値と送信される値が食い違うと、確認画面の意味がなくなります。整形するなら入力直後か、確認画面で「この内容で送信します」と見せてからにします。
海外住所を扱うなら早めに決める。 都道府県の <select> は日本前提の設計です。国の選択を入れる予定があるなら、住所欄の構造を国ごとに切り替える必要が出てきます。
関連ページ
- Next.jsで郵便番号から住所を自動入力する方法 — 補完APIの実装
- 日本の郵便番号検索API比較 — 使うAPIの選び方
- 日本住所表記ゆれチェッカー — 正規化の挙動を試す
出典
- MDN: HTML autocomplete 属性(トークン一覧)(新しいタブで開く)確認日 2026-07-29
- W3C WAI Tutorials: Labeling Controls(新しいタブで開く)確認日 2026-07-29
- W3C WAI Tutorials: User Notifications(新しいタブで開く)確認日 2026-07-29