shadcn/ui 05 フォーム(Field・React Hook Form・Zod)

更新日:

実務で最も作るのがフォーム。shadcn/ui では Field 部品 + React Hook Form(状態管理)+ Zod(検証) が公式の定番。
(公式には他に TanStack Form 版、Formisch 版、Next.js の Server Actions で作る版のガイドもある)

構成要素

部品役割
Field1項目の入れ物。data-invalid でエラー表示の状態
FieldLabelラベル(htmlFor で入力と紐づけ)
FieldDescription補足説明
FieldErrorエラーメッセージ
FieldGroup / FieldSet / FieldLegend項目のまとまり
Input / Textarea / Select / Checkbox など入力部品
InputGroup入力欄+アイコン・文字数などの付属品

導入

npx shadcn@latest add field input textarea button card sonner input-group
npm install react-hook-form @hookform/resolvers zod

実装例(公式デモを簡略化)

'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { Controller, useForm } from 'react-hook-form'
import * as z from 'zod'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
 
// ① スキーマ(ルールとエラーメッセージ)
const formSchema = z.object({
  title: z.string().min(5, 'タイトルは5文字以上').max(32, 'タイトルは32文字以内'),
  description: z.string().min(20, '説明は20文字以上').max(100, '説明は100文字以内'),
})
type FormValues = z.infer<typeof formSchema>   // 型もスキーマから自動生成
 
export function BugReportForm() {
  // ② フォームの状態
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { title: '', description: '' },
  })
 
  // ③ 送信(検証が通った時だけ呼ばれる)
  async function onSubmit(data: FormValues) {
    // Server Action / Server Function / API を呼ぶ
    toast.success('送信しました')
    form.reset()
  }
 
  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <FieldGroup>
        <Controller
          name="title"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor="title">タイトル</FieldLabel>
              <Input {...field} id="title" aria-invalid={fieldState.invalid} autoComplete="off" />
              {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
            </Field>
          )}
        />
        <Controller
          name="description"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor="description">説明</FieldLabel>
              <Textarea {...field} id="description" rows={6} aria-invalid={fieldState.invalid} />
              <FieldDescription>再現手順・期待する動作・実際の動作</FieldDescription>
              {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
            </Field>
          )}
        />
      </FieldGroup>
      <Button type="submit" disabled={form.formState.isSubmitting}>送信</Button>
    </form>
  )
}

ポイント

  • Controller で React Hook Form と入力部品をつなぐ(field に value・onChange などが入っている)
  • data-invalid と aria-invalid でエラー時の見た目とアクセシビリティを両立
  • 同じ Zod スキーマをサーバー側の検証にも使う(schemas.ts に置いて共有)→ クライアント検証は UX のため、本当の検証はサーバー
  • 送信中はボタンを無効化(二重送信防止)
  • 成功・失敗は Sonner で通知
  • ブラウザ標準の検証(required、type="email")も併用すると良い(公式注記)

フレームワーク別のつなぎ方

フレームワーク送信先
Next.jsServer Action(onSubmit 内で呼ぶ、または <form action> + useActionState)→ Next.js_05_データ更新(Server Actions・フォーム)
TanStack StartcreateServerFn({ method: 'POST' }).validator(formSchema) + useMutation → TanStack Start_03_実行モデルとServer Functions
Vite SPAAPI(Cloudflare Worker など)へ fetch

よくある項目の部品

項目部品
メール・パスワードInput type="email" / type="password"
認証コードInput OTP
生年月日・予約日Date Picker(Calendar + Popover)
都道府県Select または Native Select(スマホで使いやすい)
同意Checkbox
ON/OFF 設定Switch
検索して選ぶCombobox

関連