【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 を付けることです。
プレースホルダだけで代用すると、入力を始めた瞬間に何の欄か分からなくなります。スクリーンリーダーでも読まれにくくなります。
// 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 程度に絞ります。
横に広い入力欄は、目線の移動が増えて書きにくくなります。
// 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項目を検証し、問題があれば欄ごとのエラーと入力値を返します。メール配信サービスやデータベースへの保存は、コメントの位置に足してください。
// 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 だけで切り替えられます。
<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 には、写真の上にお問い合わせの案内を置く「映像制作・コンタクト」など、仕上がったお問い合わせのセクションがあります。
ブラウザでライブプレビューを確認し、気に入ったらコードをコピーします。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


