How to Create a Custom Payment Form with Stripe and Formspree

Learn how to use Stripe Elements to build a custom payment form and handle the server code with Formspree.

in

Collecting payment is the most crucial experience of your online business. So why settle for a stock Stripe Checkout page? Building a custom checkout form with Stripe Elements gives you control over the entire payment experience, all the way to the final moments of the sale.

Now with formspree-react and the Formspree Stripe plugin, you can build fully custom payment experiences in a fraction of the time it would take with Stripe Elements alone.

By going the custom route, you have more control over the form styling and data collected. You can also add integrations to make your form do double-duty for lead generation, event registration, and file uploads.

This tutorial will walk you through how to create a simple React payment form with Stripe and Formspree that you can build upon to your heart’s content.

Here’s a quick preview of the form we’ll be building:

A custom payment form powered by Stripe.

Step 1: Set up the Stripe Plugin

Before we get started, you’ll need a Formspree account and a new form to collect payments. You can do that by registering for a free account, if you don’t already have one.

Next, you will need is to enable Stripe plugin on your form’s plugins tab:

A screenshot of the Formspree plugins tab

We will start off with the integration in Test Mode while we are building out the form. Once everything is working the way you want it to, be sure to switch to Live Mode (currently only available for Professional and higher members).

You will be prompted to sign in to your Stripe account (if you don’t have a Stripe account, now is a great time to make one). Next, set the currency type, price, and description for your payment form.

A popup window for “Finish Stripe Setup” in Test Mode, where the user enters a description, currency, and price.

Make sure to specify a price. There’s an option to “Allow variable pricing”, which lets you pass a price dynamically from your app. This is useful for donations, but we don’t recommend it for regular payments since it will let the customer name their price!

Finally, copy the Publishable key returned by the plugin, and note your form’s hashid. You’ll need these to set-up the form in your front-end code.

Step 2: Build your payment form

We’ll be using @formspree/react to simplify form processing and creating payments. You can read more about Formspree React here.

In your app, first install Formspree React by running:

  npm i @formspree/react

  or

  yarn add @formspree/react

  or

  pnpm add @formspree/react

Then in your form, import the useForm hook and set the form’s onSubmit property to the returned handleSubmit function. Add the CardElement component where you want to capture card details:

  import { 
    FormspreeProvider, 
    useForm, 
    CardElement 
  } from '@formspree/react';

  function SignupForm() {
    const [state, handleSubmit] = useForm('YOUR_FORMSPREE_FORM_ID');

    return (
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" type="email" name="email" />
        </div>
        <div>
          <label>Card details</label>
          <CardElement />
        </div>
        <button type="submit">
          Pay
        </button>
      </form>
    )
  }

  const App = () => (
    <FormspreeProvider stripePK="YOUR_STRIPE_PUBLISHABLE_KEY">
      <SignupForm />
    </FormspreeProvider>
  )

Notes:

  • <CardElement> is a wrapper around Stripe’s CardElement component, exposing all the capabilities of that component. We wrap it to ensure only one copy of Stripe’s libraries are loaded.
  • <FormspreeProvider> needs to sit above your form in the app’s component hierarchy. This is a context provider that initializes Stripe.

Step 2b: Handle payment state and errors

To handle if payment went successfully, or if there were any errors, use the state object returned from useForm. You can display errors by passing them to ValidationError components:

  import { 
    useForm, 
    FormspreeProvider, 
    CardElement, 
    ValidationError 
  } from '@formspree/react';

  function SignupForm() {
    const [state, handleSubmit] = useForm('YOUR_FORMSPREE_FORM_ID');

    if (state.succeeded) {
      return <h4>Payment has been processed successfully!</h4>
    }

    return (
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" type="email" name="email" />
          <ValidationError errors={state.errors} />
        </div>
        <div>
          <label>Card details</label>
          <CardElement />
          <ValidationError
            field="paymentMethod"
            errors={state.errors}
          />
        </div>
        <button type="submit" disabled={state.submitting}>
          {state.submitting ? 'Processing payment...' : 'Pay'}
        </button>
      </form>
    )
  }

Notes:

  • <ValidationError> is a convenience component for displaying form errors. Read more here.
  • The field paymentMethod in the <ValidationError> for card details is necessary to catch payment related errors.

Step 2c: Styling your Card element

Since Formspree wraps Stripe’s CardElement component, the styling experience is exactly the same. See the style options for CardElement here.

Here’s an example:

    import { 
      useForm, 
      FormspreeProvider, 
      CardElement, 
      ValidationError 
    } from '@formspree/react';

    const useOptions = () => {
      const options = React.useMemo(
        () => ({
          style: {
            base: {
              color: '#424770',
              letterSpacing: '0.025em',
              fontFamily: 'Source Code Pro, monospace',
              '::placeholder': {
                color: '#aab7c4',
              },
            },
            invalid: {
              color: '#9e2146',
            },
          },
        }),
        []
      );

      return options;
  };

  function SignupForm() {
    const options = useOptions()
    const [state, handleSubmit] = useForm('YOUR_FORMSPREE_FORM_ID');

    if (state.succeeded) {
      return <h4>Payment has been processed successfully!</h4>
    }

    return (
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" type="email" name="email" />
          <ValidationError errors={state.erorrs} />
        </div>
        <div>
          <label>Card details</label>
          <CardElement options={options} />
          <ValidationError
            field="paymentMethod"
            errors={state.errors}
          />
        </div>
        <button type="submit" disabled={state.submitting}>
          {state.submitting ? 'Processing payment...' : 'Pay'}
        </button>
      </form>
    )
  }

You can also style Stripe Elements by using class names in your css file. Find an example here.

Step 3: Submit the form

Our form is ready to collect payments! Note that you don’t have to set up any server-side code, since Formspree securely process your payments for you using the Stripe plugin we configured in Step 1.

Let’s submit the form and see what happens! You should see that your form has been replaced with the following message:

successful payment

After submitting, we can see the latest submission contains a link to the Stripe receipt in the Formspree dashboard.

receipt

If we view the payment in Stripe, we can see that the additional fields from our form (the name and email) are also reflected in the charge’s metadata.

charge metadata

Now you have the foundation for a functioning custom payment form. To see it all put together, play around with our working demo. You can find the source code on GitHub.

To support the widest array of cards and payment authentication requirements, we recommend supporting strong customer authentication (SCA) with Stripe. Luckily @formspree/react handles it all for you, so you’re good to go!

Next: Build a Workflow for Your Stripe Custom Form

Building a payment form is just the beginning. From here, you can literally go wild customizing your payment form with Formspree with our native plugins for Trello, Slack, Zendesk, Airtable, Google Sheets, and more. We also have resources to help you make sure your forms are accessible and to offer design inspiration. Good luck!

Go wild animated gif

Got Feedback?