How to create a custom WordPress form?

In the process of creating a WordPress website, it is sometimes necessary to create a form for a very specific need. Plugins exist, of course, but let’s take a look at how to create a custom “homemade” WordPress form and study best practices to take advantage of the many tools and APIs WordPress offers.

This is a translation of a tutorial I initially published in French on Créer un formulaire WordPress sur-mesure.

It goes without saying that forms populate our websites, and this trend is no exception to WordPress sites. In many cases, the use of a dedicated form management plugin is effective.

But sometimes, due to the specificity or even complexity of the objectives of the form to be created, the WordPress site developer must know how to get his hands dirty and create a custom form from scratch.

In this article, I work directly in a WordPress TwentyFifteen child theme. You can download the PHP files of the tutorial (1 page template and 1 functions.php) and use them as a basis for your homemade form.

When and why use a WordPress form plugin?

Contact Form 7, Gravity Forms, Ninja Forms… WordPress plugins to manage forms on a WP site are numerous. And better: some are excellent and do the job very well!

Right here, we recently praised ACF and listed many web developer tips and tricks for this plugin to develop a custom experiences with it.

With or without a plugin, in any case, a good WordPress developer will always fall back on his feet and will know how to take advantage of the actions and filters of (good) plugins to process and manipulate the data of a form after submission.

But a plugin will always do “more” – or even “too much” – than a custom code:

  • external CSS and JS files are often called,
  • customization can be tedious (overwriting CSS styles or JS events),
  • plugin updates can even sometimes make a form unusable overnight,
  • third-party addons are often required to link a form to third party applications, sometimes for an additional fee,
  • not to mention the time spent going through the documentation or the source code of the plugin to understand what goes on in the background.

In short… It’s up to you to judge the time saved and the time lost thanks to (or because of) a WordPress form plugin.

How NOT to develop a WordPress form…

Eviter les mauvais développements de WordPress
Beware, ugly code can be harmful !

You will find many tutorials on how to create a custom WordPress contact form. Some solutions work well, of course, but are poorly thought out: here, we will separate the visual template from all the data processing logic.

You can indeed include in your page template all the code necessary for the proper functioning of the form: HTML structure, error display, receiving data, validating entries, sending mail or other logic …

But I strongly advise against this: from a separation of concerns point of view, it makes sense to separate the scene from backstage.

The scene is the display of the form or errors, everything that is presented to the user.

Behind the scenes are all the actions that take place after a form entry has been submitted: data verification, validation, conditional analysis, processing and decision of the actions…

Imagine, in the first case, having to decide to change the design and HTML structure of a template: it will be tedious to separate the code concerning the form from the rest of the page.

If the core logic, which has nothing to do with the appearance of the form, is placed elsewhere (in functions.php of a theme, or in a separate plugin), then it becomes much easier to maintain the theme, modify the design or even add features to this form in the future.

Thus, your code is more organized: the view (the visible part of the iceberg) is editable in the page template while the controller (the hidden part of the iceberg) is managed in a separate file. This is what we will do here.

Create your own custom WordPress form: the right way

To illustrate our tutorial, we will here create a very simple little form: a field lets a user define the amount of a donation.

A jackpot grows each time a user submits the form, only if the donation is greater than 0 and less than 10000 €.

Formulaire de don (cagnotte) dans WordPress
A small custom form to manage donation amounts

Creating the form markup

In a template (or page template) made especially for this form, we insert the useful HTML structure to the form.

Nothing complicated here… A numeric field with its label, a push button and that’s it!

<form action="#" method="POST" class="comment-form">
		<label for="don">Donation amount</label>
		<input id="don" type="number" name="don" value="5" />

	<input id="submit" type="submit" name="cagnote-don-envoi" id="submit" class="submit" value="Submit" />

Note, however, that the submit button has a name attribute that we will use later on to make sure, in PHP, that a form entry has been submitted and needs to be processed.

The POST method is more usual for me, but if you want to send your data in GET, don’t worry as long as you change the rest of the code accordingly.

Enhancing this form with PHP and WordPress magic

<form action="#" method="POST" class="comment-form">
	<?php wp_nonce_field( 'faire-don', 'cagnotte-verif' ); ?>

		<label for="don"><?php _e( 'Amount donation' ); ?></label>
		<input id="don" type="number" name="don" value="5" />

	<input id="submit" type="submit" name="cagnote-don-envoi" id="submit" class="submit" value="<?php esc_attr_e( 'Submit', 'msk' ); ?>" />

The idea here is to take advantage of the power of WordPress in order to secure the use of our form with nonces. The wp_nonce_field() function allows you to check that the content of a form request comes from the current site, and not from another site. So, it is not a perfect protection, but protects in many cases.

Its first argument represents an action, the second argument represents a name. These two words are reused a little later in our code in order to check if the received nonce corresponds to these two arguments.

It is highly important to include nonce fields in your WordPress forms: “nonces can be used to verify a user intends to perform an action, and is instrumental in protecting against Cross-Site Request Forgery (CSRF)”.

Data interception with template redirect action and form security check

Let’s focus on the processing logic of the form: we leave the template to go to the functions.php file in order to process the data received.

<?php function traitement_formulaire_don_cagnotte() {
	if ( ! isset( $_POST['cagnote-don-envoi'] ) || ! isset( $_POST['cagnotte-verif'] ) )  {

	if ( ! wp_verify_nonce( $_POST['cagnotte-verif'], 'faire-don' ) ) {

	// Process the form...
add_action( 'template_redirect', 'traitement_formulaire_don_cagnotte' );

This is the custom function where the magic happens…

The template_redirect action to intercept the data as soon as possible

In order to possibly redirect the user to another page after analyzing the form data, it is essential to hook to a WordPress action that runs before the page headers are generated.

We check what we want, process our $_POST data and then we can redirect with wp_redirect() without the fear of getting an error like Cannot modify header information – headers already sent.

The first condition present in our function allows us to be sure that a form entry has been submitted. Since our function is coupled to the template_redirect action, it will be executed every time a page is loaded. It is therefore important to filter the requests and check that it is time to parse the form entry: if $_POST[‘cagnotte-don-envoi’] does exist, it means that the submit button on the form has been clicked and it is therefore time to analyze the request.

This same condition also verifies the presence of the nonce created above, via isset( $_POST[‘cagnotte-verif’] ).

Securing the form submission by validating nonces

The wp_verify_nonce() function is a pair to wp_nonce_field() previously used and allows to verify the veracity of the nonce that has been sent.

All you have to do is to define the name and then the action of the nonce in parameters (contrary to what was written in wp_nonce_field() previously).

Processing the data coming from your custom WP form

<?php function traitement_formulaire_don_cagnotte() {
	if ( ! isset( $_POST['cagnote-don-envoi'] ) || ! isset( $_POST['cagnotte-verif'] ) )  {

	if ( ! wp_verify_nonce( $_POST['cagnotte-verif'], 'faire-don' ) ) {

	$don = intval( $_POST['don'] );
	$url = wp_get_referer();

	// Donation amount is too low.
	if ( $don < 0 ) {
		$url = add_query_arg( 'erreur', 'radin', wp_get_referer() );

	// Donation amount is too high.
	} elseif ( $don > 10000 ) {
		$url = add_query_arg( 'erreur', 'trop', wp_get_referer() );

	// Everything's OK, let's do the work...
	} else {
		$cagnotte_actuelle = intval( get_option( 'valeur_cagnotte', 0 ) );
		update_option( 'valeur_cagnotte', $cagnotte_actuelle + $don );
		$url = add_query_arg( 'success', 1, wp_get_referer() );

	// Redirect user back to the form, with an error or success marker in $_GET.
	wp_safe_redirect( $url );
add_action( 'template_redirect', 'traitement_formulaire_don_cagnotte' );

The hardest part is done: once our checks are done, it’s up to you to do what you have to do with the form data contained in the $_POST variable. But let’s describe what we do here…

  • we store the amount of the donation in a variable
  • if this donation is less than 0, we add a GET variable ?error=radin in the URL in order to be able to display an error message on the frontend, using the add_query_arg() function,
  • if the donation is more than 10 000 €, we do the same thing but add a GET variable ?erreur=trop in the URL,
  • otherwise, we add the amount of this new donation to the old jackpot value to update the jackpot stored in the WordPress jackpot_value option,
  • finally, we redirect the user back to the form by using wp_safe_redirect()

That’s really where it all comes into play and where the tailor-made form shines: you can do whatever you want!

You get the data of the form via $_POST and the user data if he logged-in… It’s up to you to develop and create the logic and behaviors that are needed for your project.

And, at the risk of repeating myself: since headers are not sent, you can redirect the user wherever you want according to this or that condition.

Note that the wp_get_referer() function allows you to access the original URL, the one from which the form was posted.

Redirecting the user back to the form page, with a status

We have previously added GET values in the redirect URL if certain conditions were not met in order to display errors. A small update of our template allows us to display error messages before the form.

if ( isset( $_GET['erreur'] ) ) {
	$error = sanitize_title( $_GET['erreur'] );

	switch ( $error ) {
		case 'radin' :
			$message = __( 'We need a positive amount.', 'msk' );

		case 'trop' :
			$message = __( 'Thanks, but we do not need that much money.', 'msk' );

		default :
			$message = __( 'Something went wrong.', 'msk' );

	printf( '<div class="error"><p>%1$s</p></div>', esc_html( $message ) );

We analyze the value of our variable $_GET[‘erreur’] and display the corresponding message according to the error case.

The same thing can be applied if everything went well: the redirected page will get a success marker in the URL.

We hard-coded the error/success message displaying in the template here, for simplicity sake. By outsourcing this logic into a separate custom function, you’ll be able to maintain the template and the message display separately and more easily.

Last words

With this practical example, we learned how to set up a custom WordPress form pretty quickly:

  • a page template allows us to display the form itself as well as error messages,
  • in functions.php, the template_redirect action allows us to intercept the form values, analyze them as we see fit and redirect the user accordingly.

In addition to being flexible, this solution will be much more powerful than a heavy plugin full of options and external files. It will also be easier to maintain thanks to a clear separation of concerns.

But more importantly, the possibilities are endless: creating a post, sending an e-mail, saving in a Google Spreadsheet, connecting to a third-party API (MailChimp, Slack…), publishing on social networks, identifying or updating a user account, etc. The possibilities are endless!

Ping me on Twitter to share the unique uses of custom WordPress forms you’ve had the opportunity to create!

You liked this content?

Make sure to share it on your social networks!