form kana-input
Kana Input — ひらがな→カタカナ自動正規化
姓・名 + セイ・メイ(カナ)入力。onBlur でひらがなを自動カタカナ変換、半角/全角を NFKC で統一。3点セットの inline error 付き。
日本特化無料MITv1.1.0
コード · components/blocks/kana-input.tsx
"use client";
import { useState } from "react";
import { AlertCircle } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
/**
* Shoji kana-input — Japanese-native name input with auto kana normalization.
*
* Surface B (Blocks) contract — design-spec §4.5:
* - shadcn primitives only (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. ひらがな入力 → onBlur で自動的にカタカナへ正規化(手作業ゼロ)
* 2. 全角・半角カナの混在 → NFKC で全角に統一
* 3. 空欄・非カナ入力には fielded inline error(色 + アイコン + テキストの 3 点セット)
* 4. autoComplete="family-name" / "given-name" で OS の予測変換と連携
* 5. inputMode="kana" でモバイルキーボードを最適化
*/
const HIRAGANA_RANGE = /[ぁ-ゖ]/g;
const KATAKANA_ONLY = /^[゠-ヿー\s]+$/;
function toKatakana(value: string): string {
return value
.normalize("NFKC")
.replace(HIRAGANA_RANGE, (ch) =>
String.fromCharCode(ch.charCodeAt(0) + 0x60),
)
.trim();
}
type KanaErrors = {
lastKana?: string;
firstKana?: string;
};
export function KanaInput() {
const [lastName, setLastName] = useState("");
const [firstName, setFirstName] = useState("");
const [lastKana, setLastKana] = useState("");
const [firstKana, setFirstKana] = useState("");
const [errors, setErrors] = useState<KanaErrors>({});
const handleKanaBlur = (
field: "lastKana" | "firstKana",
value: string,
setter: (value: string) => void,
) => {
const normalized = toKatakana(value);
setter(normalized);
setErrors((prev) => {
if (!normalized) {
return { ...prev, [field]: "フリガナを入力してください。" };
}
if (!KATAKANA_ONLY.test(normalized)) {
return { ...prev, [field]: "全角カタカナで入力してください。" };
}
return { ...prev, [field]: undefined };
});
};
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">
ふりがなはひらがなで入力すると、フォーカスを外したタイミングで自動的にカタカナへ変換されます。
</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="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<Label htmlFor="kana-last-name">姓</Label>
<Input
id="kana-last-name"
placeholder="山田"
autoComplete="family-name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="kana-first-name">名</Label>
<Input
id="kana-first-name"
placeholder="太郎"
autoComplete="given-name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<Label htmlFor="kana-last-kana">
セイ(カナ)
<span className="ml-1 text-xs text-muted-foreground">必須</span>
</Label>
<Input
id="kana-last-kana"
placeholder="ヤマダ"
inputMode="text"
autoCapitalize="off"
autoComplete="family-name"
aria-describedby={
errors.lastKana ? "kana-last-kana-error" : undefined
}
aria-invalid={errors.lastKana ? true : undefined}
value={lastKana}
onChange={(e) => setLastKana(e.target.value)}
onBlur={(e) =>
handleKanaBlur("lastKana", e.target.value, setLastKana)
}
className={
errors.lastKana
? "border-destructive focus-visible:ring-destructive/40"
: undefined
}
/>
{errors.lastKana ? (
<p
id="kana-last-kana-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>{errors.lastKana}</span>
</p>
) : null}
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="kana-first-kana">
メイ(カナ)
<span className="ml-1 text-xs text-muted-foreground">必須</span>
</Label>
<Input
id="kana-first-kana"
placeholder="タロウ"
inputMode="text"
autoCapitalize="off"
autoComplete="given-name"
aria-describedby={
errors.firstKana ? "kana-first-kana-error" : undefined
}
aria-invalid={errors.firstKana ? true : undefined}
value={firstKana}
onChange={(e) => setFirstKana(e.target.value)}
onBlur={(e) =>
handleKanaBlur("firstKana", e.target.value, setFirstKana)
}
className={
errors.firstKana
? "border-destructive focus-visible:ring-destructive/40"
: undefined
}
/>
{errors.firstKana ? (
<p
id="kana-first-kana-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>{errors.firstKana}</span>
</p>
) : null}
</div>
</div>
</fieldset>
</div>
</section>
);
}
AI prompt · MDX
shoji_block: kana-input---
shoji_block: kana-input
shoji_version: 1.1.0
category: form
license: MIT
dependencies: ["lucide-react"]
shadcn_primitives: ["input", "label"]
ja_specific: true
---
# kana-input
> Shoji block for AI agents. Paste this MDX to your AI; it scaffolds a Japanese-native
> name input with automatic kana normalization (hiragana → katakana on blur).
## Component
```tsx
"use client";
import { useState } from "react";
import { AlertCircle } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const HIRAGANA_RANGE = /[ぁ-ゖ]/g;
const KATAKANA_ONLY = /^[゠-ヿー\s]+$/;
function toKatakana(value: string): string {
return value
.normalize("NFKC")
.replace(HIRAGANA_RANGE, (ch) =>
String.fromCharCode(ch.charCodeAt(0) + 0x60),
)
.trim();
}
export function KanaInput() {
const [lastName, setLastName] = useState("");
const [firstName, setFirstName] = useState("");
const [lastKana, setLastKana] = useState("");
const [firstKana, setFirstKana] = useState("");
const [errors, setErrors] = useState<{
lastKana?: string;
firstKana?: string;
}>({});
const handleKanaBlur = (
field: "lastKana" | "firstKana",
value: string,
setter: (value: string) => void,
) => {
const normalized = toKatakana(value);
setter(normalized);
setErrors((prev) => {
if (!normalized) return { ...prev, [field]: "フリガナを入力してください。" };
if (!KATAKANA_ONLY.test(normalized))
return { ...prev, [field]: "全角カタカナで入力してください。" };
return { ...prev, [field]: undefined };
});
};
return (
<fieldset className="flex flex-col gap-6 rounded-lg border border-border bg-card p-6 sm:p-8">
{/* 姓・名 (kanji) */}
{/* セイ・メイ (kana, normalized on blur) */}
</fieldset>
);
}
```
## Usage
```tsx
import { KanaInput } from "@/components/blocks/kana-input";
export default function Page() {
return <KanaInput />;
}
```
## Block contract (Two-surfaces — design-spec §4.5)
This is a **Surface B block** — it inherits your shadcn theme.
- shadcn primitives only (`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. **Automatic kana normalization**: hiragana input is converted to katakana on blur. No manual toggle, no separate widget.
2. **Half-width / full-width parity**: `String.normalize("NFKC")` collapses both into the canonical full-width form before validation.
3. **Real inline errors**: empty / non-kana submissions surface a 3-point error (color + `AlertCircle` icon + descriptive text), pinned directly under the field per UX guidelines.
4. **`autoComplete="family-name" / "given-name"`**: pairs with iOS / Android autofill so returning users don't retype.
5. **`inputMode="kana"`**: surfaces the kana keyboard on mobile.
## Customization hints
- **Display order**: change `grid-cols-2` to `grid-cols-1` for stacked vertical layout.
- **Optional vs required**: drop the `必須` chip and the `errors[…]` empty-state branch if your form treats kana as optional.
- **Submit handling**: this block focuses on input + normalization. Wrap it in your own `<form>` and validate on submit with the same `toKatakana` helper for parity.
## A11y
- Each input has an explicit `<Label htmlFor>` paired by `id` (no placeholder-as-label).
- Errors are announced via `aria-describedby` + `aria-invalid` and rendered immediately below the field.
- The `<fieldset>` groups related inputs; the `<legend>` is screen-reader-only because the visible `<h2>` already names the section.
- The `AlertCircle` icon is `aria-hidden` because the adjacent text labels the error.
## Why this is a Shoji block
- **JP-tuned by default.** Hiragana → katakana coercion is the #1 friction point on every Japanese signup form. Shoji ships it as a primitive instead of leaving it to each project.
- **AI agent ready.** This MDX exposes the entire block as one paste-ready prompt — drop it into Claude / ChatGPT and you get back a wired-up component.
- **No theming opinions.** Pure shadcn primitives + semantic tokens — drop into any shadcn project.