【Next.js】お問い合わせフォームの作り方をコード付きで解説

公開日: 2026年9月12日 · 読了目安 11 分

Next.js の LP に置くお問い合わせフォームを、名前・メール・用件の3項目で作る手順です。label 付きの入力欄、欄ごとのエラー表示、Server Actions での受け取り、2カラムの配置までコード付きで解説します。

LP のお問い合わせフォームは何項目にすべきか

Next.js で LP を作り終え、最後にお問い合わせフォームを置く段階になったとします。会社名や電話番号、部署に予算と、聞きたい項目は次々に増えていきます。

けれども LP のお問い合わせフォームは、名前・メール・用件の3項目で十分です。

見た目は1カラムか、フォームの左に短い説明を置く2カラムで足ります。動きは画面に入ったときの入場だけにして、入力の邪魔をしないようにします。

この記事で分かることは次の4つです。

  • 項目を減らすと、フォームが最後まで送られやすくなる理由

  • label 付きの3項目フォームと、欄ごとのエラー表示のコード

  • Server Actions(サーバーで動く関数をフォームから直接呼ぶ仕組み)での受け取り方

  • 2カラムの配置と、公開前に確認したいこと

お問い合わせフォームの項目を減らすべき理由

フォームの途中で人が離れる原因は、たいてい「面倒そう」「この先に何が起きるか分からない」の2つです。どちらも、作る側の工夫で減らせます。

必須の項目が多いと、書く前に閉じられる

欄が7つ並んでいると、読者は入力を始める前に手間を見積もり、閉じてしまいます。

電話番号や会社名は、商談に進んでから聞いても間に合います。本当に必要になった段階で足してください。

ボタンが「送信」だけだと、押したあとが想像できない

「送信」では、営業電話が来るのか、資料が届くのかが分かりません。「デモを予約する」「相談を送る」のように、押した直後に起きることが分かる言葉にします。

ボタンの近くに「2営業日以内に返信します」のような一文を添えると、不安がさらに減ります。

エラーがまとめて出ると、どこを直すか探させる

ボタンの下に「入力内容に誤りがあります」とだけ出すと、読者は欄を1つずつ見直すことになります。エラーは該当する欄のすぐ下に、直し方が分かる言葉で出します。

1カラムのお問い合わせフォームを作るコード

まずは送信先をつながずに、見た目と文言を固めます。送信の処理は次の章でつなぎます。

下のコードは、次の要素をまとめたブラウザで動く部品(クライアントコンポーネント)です。

  • 見出しと1行の説明

  • 3つの入力欄

  • 送信ボタン

ポイントは、すべての入力欄に label を付けることです。

プレースホルダだけで代用すると、入力を始めた瞬間に何の欄か分からなくなります。スクリーンリーダーでも読まれにくくなります。

tsx
// app/contact/ContactForm.tsx
  "use client";
  
  import { useActionState } from "react";
  import { sendContact, type ContactState } from "./actions";
  
  const initialState: ContactState = { ok: false, errors: {}, values: {} };
  
  const inputClass =
    "mt-2 w-full rounded-lg border border-zinc-300 bg-white px-4 py-3 text-base outline-none focus:border-zinc-900 aria-[invalid=true]:border-red-500";
  
  export function ContactForm() {
    const [state, formAction, pending] = useActionState(sendContact, initialState);
  
    if (state.ok) {
      return (
        <p role="status" className="rounded-xl bg-zinc-100 p-6 text-sm">
          送信しました。2営業日以内にメールでご返信します。
        </p>
      );
    }
  
    return (
      <form action={formAction} noValidate className="space-y-5">
        <div>
          <label htmlFor="name" className="text-sm font-medium">お名前</label>
          <input
            id="name"
            name="name"
            autoComplete="name"
            defaultValue={state.values.name}
            aria-invalid={Boolean(state.errors.name)}
            aria-describedby={state.errors.name ? "name-error" : undefined}
            className={inputClass}
          />
          {state.errors.name && (
            <p id="name-error" className="mt-1 text-xs text-red-600">{state.errors.name}</p>
          )}
        </div>
  
        <div>
          <label htmlFor="email" className="text-sm font-medium">メールアドレス</label>
          <input
            id="email"
            name="email"
            type="email"
            autoComplete="email"
            defaultValue={state.values.email}
            aria-invalid={Boolean(state.errors.email)}
            aria-describedby={state.errors.email ? "email-error" : undefined}
            className={inputClass}
          />
          {state.errors.email && (
            <p id="email-error" className="mt-1 text-xs text-red-600">{state.errors.email}</p>
          )}
        </div>
  
        <div>
          <label htmlFor="message" className="text-sm font-medium">ご用件</label>
          <textarea
            id="message"
            name="message"
            rows={5}
            defaultValue={state.values.message}
            aria-invalid={Boolean(state.errors.message)}
            aria-describedby={state.errors.message ? "message-error" : undefined}
            className={inputClass}
          />
          {state.errors.message && (
            <p id="message-error" className="mt-1 text-xs text-red-600">{state.errors.message}</p>
          )}
        </div>
  
        <button
          type="submit"
          disabled={pending}
          className="w-full rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition hover:bg-zinc-700 disabled:opacity-60"
        >
          {pending ? "送信中…" : "相談を送る"}
        </button>
        <p className="text-center text-xs text-zinc-500">2営業日以内にメールでご返信します。</p>
      </form>
    );
  }

お問い合わせセクションとして置くときの外側

フォームだけをページに置くと、何のフォームかが伝わりません。見出しと1行の説明で包み、横幅を max-w-xl 程度に絞ります。

横に広い入力欄は、目線の移動が増えて書きにくくなります。

tsx
// app/contact/page.tsx
  import { ContactForm } from "./ContactForm";
  
  export default function ContactPage() {
    return (
      <section id="contact" className="mx-auto max-w-xl px-6 py-24">
        <h2 className="text-3xl font-semibold tracking-tight">導入のご相談</h2>
        <p className="mt-3 text-sm text-zinc-600">
          3項目だけで送れます。営業電話はしません。
        </p>
        <div className="mt-10">
          <ContactForm />
        </div>
      </section>
    );
  }

お問い合わせフォームを Server Actions で受け取る

入力の受け取りは、サーバーで動く関数をフォームの送信先に渡すだけで作れます。関数のファイルの先頭には "use server"(サーバーで動かす、という印)を書きます。

受け取り専用の API ルート(データを受け取るための URL)を、別に作る必要はありません。

上のフォームで使った useActionState(送信の結果を受け取る仕組み)は React 19 の機能です。Next.js 15 以降の App Router で使えます。

下のコードは3項目を検証し、問題があれば欄ごとのエラーと入力値を返します。メール配信サービスやデータベースへの保存は、コメントの位置に足してください。

ts
// app/contact/actions.ts
  "use server";
  
  export type ContactState = {
    ok: boolean;
    errors: { name?: string; email?: string; message?: string };
    values: { name?: string; email?: string; message?: string };
  };
  
  export async function sendContact(
    _prev: ContactState,
    formData: FormData,
  ): Promise<ContactState> {
    const name = String(formData.get("name") ?? "").trim();
    const email = String(formData.get("email") ?? "").trim();
    const message = String(formData.get("message") ?? "").trim();
    const values = { name, email, message };
  
    const errors: ContactState["errors"] = {};
    if (!name) errors.name = "お名前を入力してください。";
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      errors.email = "メールアドレスの形式を確認してください。";
    }
    if (message.length < 10) errors.message = "ご用件は10文字以上で入力してください。";
  
    if (Object.keys(errors).length > 0) {
      return { ok: false, errors, values };
    }
  
    // ここでメール送信や保存の処理を呼ぶ
    return { ok: true, errors: {}, values: {} };
  }

お問い合わせフォームでエラー後に入力が消えるときの直し方

React 19 で form の action に関数を渡すと、送信のあとに入力欄がリセットされます。エラーで戻したときに、書いた用件まで消えてしまうのはこのためです。

上のコードのように、返り値に入力値を入れて defaultValue に渡すと、書いた内容が残ります。

Next.js 14 以前や、すでに送信先がある場合

useActionState が使えない環境では、react-dom の useFormState がほぼ同じ役割を持ちます。

ただし送信中の値は返さないので、ボタン側で useFormStatus を使います。

すでに受け取り用の URL や外部のフォームサービスがあるなら、新しい受け取り方に移す必要はありません。

その場合は、送信ボタンを押したときの処理(onSubmit)から fetch で送れば十分です。

2カラムのお問い合わせフォームで不安を先に消す

BtoB のデモ予約や見積もりの相談では、フォームの左に「送ったあとに何が起きるか」を書く2カラムが向いています。

次の3つを置くだけで、送る前のためらいが減ります。

  • 所要時間

  • 送信後の流れ

  • プライバシーの一文

モバイルでは説明を上、フォームを下の1列にします。lg 以上で横に並べれば、Tailwind の grid だけで切り替えられます。

tsx
<section className="mx-auto grid max-w-5xl gap-12 px-6 py-24 lg:grid-cols-2">
    <div>
      <h2 className="text-3xl font-semibold tracking-tight">30分のデモを予約する</h2>
      <ul className="mt-6 space-y-3 text-sm text-zinc-600">
        <li>所要時間は30分。画面共有で実際の操作をお見せします。</li>
        <li>送信後、候補日をメールでお送りします。</li>
        <li>いただいた情報は、ご連絡のためだけに使います。</li>
      </ul>
    </div>
    <ContactForm />
  </section>

お問い合わせフォームを公開する前の確認リスト

見た目が整っても、次のどれかが抜けていると問い合わせは届きません。公開の前に一度ずつ試してください。

  • すべての欄に label があり、クリックで入力欄にフォーカスが移る

  • エラーが欄の下に出て、直すと消える

  • スマホで type="email" のキーボードが出る

  • 送信後に完了の文言が出て、二重送信できない

  • 送り先のメールアドレスや秘密のキーを、ブラウザ側のコードに書いていない

  • フォームの近くに、利用目的かプライバシーポリシーへのリンクがある

迷惑な送信が増えてきたら、ボットだけが埋める隠し欄や、送信の回数制限を後から足します。最初から重い対策を入れて、正しい読者の入力を邪魔しないようにします。

完成したお問い合わせセクションをコピーして使う方法

DesignLayer の「映像制作・コンタクト」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「映像制作・コンタクト」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

ここまでのコードで、動くフォームは作れます。ただ、見た目の細部まで整えるには、もう少し時間がかかります。

  • 余白

  • 入力欄の高さ

  • ボタンのホバー

  • 背景写真との重なり

DesignLayer には、写真の上にお問い合わせの案内を置く「映像制作・コンタクト」など、仕上がったお問い合わせのセクションがあります。

ブラウザでライブプレビューを確認し、気に入ったらコードをコピーします。https://design-layer.com/ja/components

コピーしたら、フォームの送信先と各欄の名前だけを、自分で作った受け取りの関数に合わせてください。

ライブプレビューは無制限です。無料サンプル10個を無料アカウントで回数制限なくコピーでき、動画プロンプトは月1回です。Free では MCP に接続できません。カタログ全体は30日ビルドパス($19の1回払い、自動更新なし)、Lifetime Access(先着100名の$79の1回払い)、Plus(月額 $9、いつでも解約可)です。コピーしたあとは、サンプルのメールアドレスや文言を必ず差し替えてから公開してください。

Next.js のお問い合わせフォームについてよくある質問

  • 必須ではありません。見た目を先に作り、送信は既存の API ルートや外部のフォームサービスにつないでも構いません。

    新しく作るなら、API ルートを増やさずに済む Server Actions が手軽です。

まとめ:Next.js でお問い合わせフォームを作る手順

  • LP のお問い合わせフォームは、名前・メール・用件の3項目で作る

  • すべての欄に label を付け、エラーは欄の下に出す

  • ボタンは「相談を送る」など、押したあとが分かる言葉にする

  • 入力はサーバーで動く関数で受け取り、エラー時は入力値を返す

  • 法人向けの相談では2カラムにして、送ったあとの流れを先に見せる

フォームの直前に置く CTA は https://design-layer.com/ja/articles/nextjs-cta-section-guide で解説しています。

ページの最後に置くフッターは https://design-layer.com/ja/articles/nextjs-footer-section-guide で解説しています。

仕上がったセクションから始めたい場合は、カタログを開いてライブプレビューで比べてください。https://design-layer.com/ja/components

関連ガイド