画面遷移なしで問い合わせを送信する最低限のAjaxフォーム。CSRF対策(nonce)込みで、本番化しやすい構成にしています。UIはプレーンでOK、まずは動かす。

PHP(functions.php)
PHP
// スクリプト登録 & nonce埋め込み
add_action('wp_enqueue_scripts', function () {
  wp_enqueue_script('ajax-form', get_stylesheet_directory_uri() . '/ajax-form.js', [], false, true);
  wp_localize_script('ajax-form', 'AJX', [
    'url' => admin_url('admin-ajax.php'),
    'nonce' => wp_create_nonce('send_contact'),
  ]);
});

// ハンドラ
add_action('wp_ajax_send_contact', 'handle_send_contact');
add_action('wp_ajax_nopriv_send_contact', 'handle_send_contact');
function handle_send_contact() {
  check_ajax_referer('send_contact', 'nonce');
  $name = sanitize_text_field($_POST['name'] ?? '');
  $msg  = sanitize_textarea_field($_POST['message'] ?? '');
  if ($name === '' || $msg === '') wp_send_json_error(['message' => '必須項目が未入力です'], 422);

  // ここでwp_mail()などに接続可能
  wp_send_json_success(['message' => '送信しました']);
}
JS(ajax-form.js)
JavaScript
document.addEventListener('submit', async (e) => {
  const form = e.target.closest('#ajax-contact');
  if (!form) return;
  e.preventDefault();
  const fd = new FormData(form);
  fd.append('action', 'send_contact');
  fd.append('nonce', AJX.nonce);

  const res = await fetch(AJX.url, { method: 'POST', body: fd });
  const data = await res.json();
  form.querySelector('.message').textContent =
    data.success ? data.data.message : (data.data?.message || '失敗しました');
});
フォームHTML
HTML
<form id="ajax-contact">
  <input name="name" placeholder="お名前">
  <textarea name="message" placeholder="メッセージ"></textarea>
  <button>送信</button>
  <p class="message" aria-live="polite"></p>
</form>

wp_localize_scriptでnonceとURLをJSへ安全に橋渡し
・返却はwp_send_json_*を使い、フロントで一行で扱える形に
・実運用ではreCAPTCHA/Rate limit/ログを追加