@k8ordo/form

入力欄の種類

どの種類の入力欄になるかは、スキーマの型で決まります。このページでは、入力欄の種類ごとに、スキーマの書き方とフォームでの描き方を紹介します。

このページの内容

スキーマと入力欄の対応

field()が返すinputには、スキーマに応じて次の属性が入ります。どれも、そのまま入力欄に展開して使います。

スキーマ作られる入力欄
z.string()type="text"
z.email() / z.url()type="email" / type="url"
z.iso.date() / z.iso.time()type="date" / type="time"
z.iso.datetime({ local: true })type="datetime-local"
z.coerce.number()type="number"
z.boolean() / z.literal(true)type="checkbox"
z.stringbool()type="checkbox" value="true"
z.file()type="file"
z.enum([…])typeなし。<select>に展開する
z.array(z.enum([…]))typeなし。選択肢ごとのチェックボックス
それ以外(z.uuid()など)type="text"

文字列を受け取る

z.string()はテキストの入力欄になります。.min()と.max()、.regex()は、それぞれminlengthとmaxlength、patternの属性になります。

ts
z.object({
  handle: z.string().min(3).max(20).regex(/^[a-z0-9_]+$/),
});

// handle.input
// { name: 'handle', type: 'text', required: true,
//   minLength: 3, maxLength: 20, pattern: '^[a-z0-9_]+$' }

z.email()はtype="email"、z.url()はtype="url"の入力欄になり、書式はブラウザが確かめます。

警告

落とし穴

空のテキスト欄は、値が無いのではなく""を送ります。そのため.optional()を付けていても、.min(1)があれば空欄のままでは通りません。

空欄を許したいときは、""を受け付けるスキーマを書きます。

ts
bio: z.string().min(1).optional(),
bio: z.string().max(200),

日付と時刻を受け取る

z.isoの書式は、ブラウザの日付や時刻の入力欄になります。値はISO形式の文字列のまま届きます。

ts
z.object({
  day: z.iso.date(),
  start: z.iso.time(),
  doors: z.iso.datetime({ local: true }),
});

Dateとして受け取りたいときはz.coerce.date()を使います。ただしこの場合はテキストの入力欄になります。z.date()は文字列を受け付けず、どんな値も通らない欄になるので、formFieldsがエラーにします。

数値を受け取る

数値の入力欄はz.coerce.number()で書きます。type="number"の入力欄になり、.min()と.max()はminとmaxの属性になります。

ts
z.object({
  seats: z.coerce.number().int().min(1).max(500),
  price: z.coerce.number().multipleOf(0.01).optional(),
});

// seats.input → { type: 'number', required: true, step: 1, min: 1, max: 500 }
// price.input → { type: 'number', step: 0.01 }

stepは、.int()があれば1に、.multipleOf()があればその値になります。どちらも無ければanyになり、小数も入力できます。

空の数値欄は何も送らず、0にもなりません。空欄を許すなら.optional()を付けます。

警告

落とし穴

z.number()は使えません。フォームが送る値はいつも文字列なので、どんな入力も通らない欄になってしまうからです。formFieldsはこれをエラーにして知らせます。

選択肢から1つ選ばせる

z.enum()のinputは<select>に展開します。先頭に値が空の選択肢を置いておくと、requiredが「どれかを選ぶ」という意味になります。

tsx
const format = form.field('format');

<select {...format.input}>
  <option value="">Choose a format</option>
  <option value="talk">Talk</option>
  <option value="workshop">Workshop</option>
</select>

.optional()を付けると、空の選択肢を選んだままでも送信できます。

ラジオボタンで選ばせる

自分で書くラジオボタンには、inputからnameとrequiredだけを渡します。送信前に選ばれていた値は、state.valuesを見て選択肢ごとに戻します。

tsx
const format = form.field('format');

{['talk', 'workshop'].map((option) => (
  <label key={option}>
    <input
      defaultChecked={state.values?.format === option}
      name={format.input.name}
      required={format.input.required}
      type="radio"
      value={option}
    />
    {option}
  </label>
))}
警告

落とし穴

ラジオボタンにinputをそのまま展開しないでください。送信のあとはinputに前回の値がdefaultValueとして入っていて、各ラジオボタンのvalueとぶつかってしまいます。

情報

メモ

@k8ordo/uiのRadioとRadioCardは、inputをそのまま展開して使えるように作ってあります。

チェックボックスで受け取る

z.boolean()はチェックボックスになります。チェックが無いときはfalseとして届くので、必須の欄にはなりません。

規約への同意のように、チェックを必須にしたいときはz.literal(true)を使います。

ts
z.object({
  newsletter: z.boolean(),
  terms: z.literal(true, 'Agree to the terms to continue'),
});

文字列を送るチェックボックス

z.stringbool()のチェックボックスは、チェックされるとinput.valueの文字列(既定では"true")を送ります。値がURLに残るGETのフォームで使います。

ts
z.object({
  inStock: z.stringbool().default(false),
});

// inStock.input → { name: 'inStock', type: 'checkbox', value: 'true' }

チェックが無いときは何も送らないので、.default(false)か.optional()を付けます。.default(true)はエラーになります。チェックを外してもfalseを送る手段が無いからです。

複数を選ばせる

z.array(z.enum())は、同じnameを持つチェックボックスの集まりになります。チェックされた値が配列で届き、1つもチェックが無ければ[]になります。

tsx
const tags = form.field('tags');
const checked = state.values?.tags;

{['react', 'css', 'a11y'].map((option) => (
  <label key={option}>
    <input
      defaultChecked={Array.isArray(checked) && checked.includes(option)}
      name={tags.input.name}
      type="checkbox"
      value={option}
    />
    {option}
  </label>
))}

.min(2)のような下限はHTMLの属性で表せないので、ブラウザでは確かめません。ブラウザでも確かめたいときは、ルールのminCheckedを宣言します。

ファイルを受け取る

z.file()はファイルの入力欄になり、.mime()はacceptの属性になります。

ts
z.object({
  slides: z.file().mime(['application/pdf']).max(10_000_000),
});

// slides.input → { name: 'slides', type: 'file', required: true,
//                  accept: 'application/pdf' }

acceptはファイルを選ぶときの候補を絞るだけで、ブラウザは選ばれたファイルの種類を確かめません。種類と大きさの検証は、サーバーだけで行います。

送信に失敗しても、選んだファイルは入力欄に戻りません。ブラウザが、ファイルの入力欄に値を戻すことを許していないためです。

パスワードを受け取る

スキーマにinput: "password"のメタ情報を付けると、type="password"の入力欄になります。

ts
password: z.string().min(8).meta({ input: 'password' }),

パスワードの入力欄は、送信に失敗しても入力されていた値を返しません。

情報

メモ

zod/miniには.meta()が無いので、.check(z.meta(…))の形で付けます。

ts
password: z.string().check(z.minLength(8), z.meta({ input: 'password' })),
k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2