> This page is for Developer.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.bigcommerce.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.bigcommerce.com/_mcp/server.

# Build a Bulk Order Form for BigCommerce Product Variants

Plan: Stencil Developer

Lesson 29 of 31 · 60 min

## Scenario

Sometimes customers want to order multiple variations of a product. Usually, they will need to add a single item to their cart, return to the product page, select different options, and repeat the process. What if we could make it faster and easier to order several variations of a product in one action?

With a bulk order form, we can enable the purchase of multiple product variants simultaneously. In this post, we'll cover building an order form in React, retrieving variants from the server-side Catalog API using middleware and adding multiple variants to the cart at the same time using the Storefront Cart API.

## Prerequisites

* Stencil Development Environment & Cloned Theme
* BigCommerce Store
* Cornerstone Theme
* Stencil CLI

## Install Dependencies

1. **Navigate** to the Cornerstone directory
2. **Run** *npm install* to install the default theme dependencies:

```bash showLineNumbers={false}
npm install
```

3. **Install** the app dependencies by running the command below:

```bash showLineNumbers={false}
npm install --save react react-dom css-loader style-loader @babel/preset-react
```

## Configure Webpack

1. **Configure** webpack to use a loader for JSX files and the custom CSS that will be used in the app
2. Include the code below in the module rules array in *webpack.common.js*:

```js showLineNumbers={false}
{
    test: /\.jsx$/,
    exclude: /node_modules/,
    use: {
        loader: "babel-loader",
            options: {
                presets: ["@babel/preset-react"],
            },
         }
},
{
    test: /\.css/,
    loader:[ "style-loader", "css-loader" ]
}
```

3. **Add** the following to the resolve object in *webpack.common.js*:

```js showLineNumbers={false}
extensions: [".js", ".jsx"]
```

## Preparing the Layout Template

1. **Create** a custom directory *templates/pages/custom*
2. **Include** another folder inside it named *product*
3. **Create** a file named *bulk-variant-form.html* in *templates/pages/custom/product*
4. **Copy** the contents of the default product.html file located in templates/pages into *bulk-variant-form.html*
5. **Wrap** *components/products/product-view* in a div (shown below)
6. **Add** a couple more elements (shown below) to act as the root for React and the toggle for the bulk order form

Example snippet of *bulk-variant-form.html:*

```html showLineNumbers={false}
// --snip

<button id='activateBulkForm' class='button button--primary' style='display:block; margin: 0 auto;'>Purchase in bulk</button>

<div itemscope itemtype="http://schema.org/Product">
    <div id='standardTemplate'>
        {{> components/products/product-view schema=true }}
    </div>
    <div id='bulk-variant-form' style="display:none"></div>

    // --snip

</div>
```

## Styling the Form

1. **Create** a new directory for the React app at *assets/js/bulk-variant-form*
2. **Create** a new file named *styles.css* in the *bulk-variant-form* directory

We have included all the CSS below to get you up and running quickly, which you can modify per your own preferences:

### styles.css

```css showLineNumbers={false}
.bulk-variant-row {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    grid-column-gap: 8px;
    padding: 8px 0;
    border-bottom: 1px solid #e5e5e5;
}

.bulk-variant-col {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.bulk-variant-col > p {
    margin: 0;
}

.bulk-variant-col > img {
    max-width: 80px;
}

.bulk-form-field {
    display: grid;
    max-width: 90%;
    margin: 0 auto;
}

.bulk-button-row {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    grid-column-gap: 8px;
    padding: 16px 0;
}

.qtyField {
    max-width: 64px;
    text-align: center;
    display: block;
}

#bulkAddToCart {
    grid-column: 5/5;
    margin: 0;
    height: 42px;
}

input[type=number]::-webkit-inner-spin-button {
    -webkit-appearance: none;
}

#bulk-messaging {
    grid-column: 4/5;
}
```

## Using the Custom Layout Template in Stencil CLI

When the theme is uploaded to a store, a merchant can select the custom layout file when they edit a product in the BigCommerce control panel. To use the custom layout while developing with Stencil CLI:

1. **Include** the configuration below in your *.stencil* file:

```json showLineNumbers={false}
"customLayouts": {
    "brand": {},
    "category": {},
    "page": {},
    "product": {
      "bulk-variant-form.html": "/your-product-page-url"
    }
  }
```

> **Info**
>
> If you don't see a file named config.stencil in your theme's root directory, be sure to run the *stencil init* command in your terminal.

## Adding React to the Theme

1. **Create** a new file named *bulk-variant-form.jsx* In the *bulk-variant-form* directory
2. To get started, **include** the following code in *bulk-variant-form.jsx*:

**`bulk-variant-form.jsx`**

```jsx title="bulk-variant-form.jsx" showLineNumbers={false}
import React, { Component } from 'react';
import './styles.css';
export default class BulkVariantForm extends Component {
    constructor(props) {
        super(props);
        this.state = {
        }
    }
    render() {
        return (
            <div>
                Bulk order form
            </div>
        )
    }
}
```

To include React and mount our app on product pages, we'll need to import React and our parent component in *assets/js/app.js*.

3. **Include** the following in *app.js*:

**`assets/js/app.js`**

```js title="assets/js/app.js" showLineNumbers={false}
import React from 'react';
import ReactDOM from 'react-dom';
import BulkVariantForm from './bulk-variant-form/bulk-variant-form';
```

4. At the bottom of *app.js*, **write** a function to initialize our React app:

**`assets/js/app.js`**

```js title="assets/js/app.js" showLineNumbers={false}
window.initBulkVariantForm = function initBulkVariantForm(id) {
    ReactDOM.render(
        React.createElement(BulkVariantForm, id, null),
        document.getElementById('bulk-variant-form')
    );
};
```

The *initBulkVariantForm* function takes a product ID as an argument. When we invoke the function on the product page, we'll pass in the product ID so that we can retrieve product variant data through middleware.

5. **Switch** back to the product layout file at *bulk-variant-form.html*
6. At the bottom of the file, **add** the following code:

**`bulk-variant-form.html`**

```html title="bulk-variant-form.html" showLineNumbers={false}
<script>
const productID = {productID: `{{product.id}}`};
window.initBulkVariantForm(productID);
const btn = document.getElementById('activateBulkForm');
const standardForm = document.getElementById('standardTemplate');
const bulkForm = document.getElementById('bulk-variant-form');
btn.addEventListener('click', (e) => {
    e.preventDefault();
    toggleForm(standardForm, bulkForm, btn);
})
function toggleForm(standard, bulk, toggle) {
    if (bulk.style.display == 'none') {
        standard.style.display = 'none';
        bulk.style.display = 'block';
        toggle.textContent = 'Purchase individually';
        return;
    }
    if (bulk.style.display == 'block') {
        standard.style.display = 'block';
        bulk.style.display = 'none';
        toggle.textContent = 'Purchase in bulk';
        return;
    }
}
</script>
```

First we get the product ID from a Handlebars expression. The productID variable we pass as an argument to initBulkVariantForm is defined as an object because it needs to be passed as a prop to our parent component. Once the product ID is available in our component, we will pass it to custom middleware.

## Using Middleware to Pass Responses from a Server-Side API to the Front End

We need to get all of a product's variant IDs, since they are a required property to add variants to the cart using the Storefront API. However, that data isn't completely available on a product page; the standard add to cart flow only needs to identify 1 variant at a time as a shopper chooses options.

Using the BigCommerce Catalog API, we can retrieve all of a product's variants in 1 or (if your product has more than 250 variants) 2 requests. This is where the magic of middleware comes in, enabling us to safely get data restricted to the server side API and sending it to our front end React app.

Our example middleware is going to be an AWS Lambda Function that makes requests to the Catalog API in response to a GET from our React app. Going into detail about the AWS implementation is out of the scope of this article, but the overall work flow should be applicable to your own middleware app or server-less function.

Remember how we pulled the product ID in our custom layout file and passed it to our React app as props? Now we need to pass that to our middleware so it can make a request to the Catalog API.

In AWS I've set up an endpoint that responds to a GET request from the React app that includes the product ID as a query parameter. The middleware's URL endpoint requested by the front end looks similar to this:

```text showLineNumbers={false}
https://xxxxxxxxx.execute-api.region.amazonaws.com/default/middleware-endpoint?product_id={product ID}
```

The Lambda Function reads the product ID from the query param and passes it to a Catalog API request structured like this:

```text showLineNumbers={false}
GET
https://api.bigcommerce.com/stores/hfdehryc/v3/catalog/products/${id}/variants?include_fields=calculated_price,inventory_level,sku,option_values,image_url
```

The include\_fields parameter lets us specify which variant properties we want returned. We'll use these properties to populate a table of products in our bulk order form.

Let's go over how to use this data in our React app.

1. In *bulk-variant-form.jsx*, **set** an initial state in the constructor:

**`bulk-variant-form.jsx`**

```jsx title="bulk-variant-form.jsx" showLineNumbers={false}
this.state = {
    variants: [],
    lineItems: [],
    loaded: false,
    message: ''
}
```

2. Using the *componentDidMount* lifecycle method, **make** a fetch to our middleware endpoint and populate our state with the results:

**`bulk-variant-form.jsx`**

```jsx title="bulk-variant-form.jsx" showLineNumbers={false}
componentDidMount() {
    fetch(`https://xxxxxxxxx.execute-api.region.amazonaws.com/default/middleware-endpoint?product_id=${this.props.productID}`)
    .then(response => response.json())
    .then(formatted => {
        const lineItems = formatted['data'].map(variant => {
            return {variantId: variant.id, quantity: 0, productId: parseInt(this.props.productID)}
        });
        this.setState({
            variants: formatted['data'],
            lineItems: lineItems,
            loaded: true
        })
    })
    .catch(err => console.log('Bulk form could not load: ', err))
}
```

The variants in state will pass all the data returned from the API to create rows in our bulk order form. The lineItems property is structured to match the lineItems requirements for adding variants to the cart using the Storefront API. We are also updating the property loaded to indicate that the details we need are ready to be displayed to the user.

## Loading Variant Details in the Order Form

Now that we can pull variants into our application state, we can feed the values into our form.

First we'll create the basic layout of the form that will be rendered after the API request to get variants is complete.

1. Within the constructor in *bulk-variant-form.jsx*, **add** the following method:

**`bulk-variant-form.jsx`**

```jsx copy filename="bulk-variant-form.jsx"
this.renderAfterLoad = () => {
    if (this.state.loaded) {
        return (
            <div>
                <div className='bulk-variant-row'>
                    <div className='bulk-variant-col'><h5>Images</h5></div>
                    <div className='bulk-variant-col'><h5>Values</h5></div>
                    <div className='bulk-variant-col'><h5>Price</h5></div>
                    <div className='bulk-variant-col'><h5>SKU</h5></div>
                    <div className='bulk-variant-col'><h5>Quantity</h5></div>
                </div>

                <div className='bulk-button-row'>
                    <div className='bulk-variant-col' id='bulk-messaging'>{this.state.message}</div>
                    <button className='bulk-variant-col button button--primary' id='bulkAddToCart'>Add to Cart</button>
                </div>
            </div>
        )
    }
}
```

As the component is rendered, we want to make sure the data we need is available before displaying anything.

2. In the component render method, **return** the results of *renderAfterLoad*

**`bulk-variant-form.jsx`**

```jsx copy filename="bulk-variant-form.jsx"
render() {
    return (
        <div className='bulk-form-field'>
            {this.renderAfterLoad()}
        </div>
    )
}
```

This will give us the basic layout of the columns, as well as the add to cart button. Next, we need to build a component that will accept all the variants and render a row for each one.

3. In *assets/js/bulk-variant-form*, **add** a new file named *bulk-variant-rows.jsx*

This component will take all the variants as props and create a row for every variant.

**`bulk-variant-rows.jsx`**

```jsx copy filename="bulk-variant-rows.jsx"
import React from 'react';
const filterOptionValues = (option_values) => {
    const nameValues = option_values.map((option, index) => {
        return <p key={index}><strong>{option.option_display_name}</strong>: {option.label}</p>
    })
    return nameValues;
}
const bulkVariantRows = (props) => {
    const variants = props.variants;
    const variantRows = variants.map((variant, index) => {
        return (
            <div key={index} className='bulk-variant-row'>
                    <div className='bulk-variant-col'><img src={variant.image_url}/></div>
                    <div className='bulk-variant-col'>{filterOptionValues(variant.option_values)}</div>
                    <div className='bulk-variant-col'>${parseFloat(variant.calculated_price).toPrecision(4)}</div>
                    <div className='bulk-variant-col'>{variant.sku}</div>
                    <div className='bulk-variant-col'>
                        <input
                          type='number'
                          min='0'
                          placeholder='0'
                          className='qtyField'
                        />
                    </div>
            </div>
        )
    })
    return variantRows;
}
export default bulkVariantRows;
```

Since a variant's `option_values` property is an array, we map all the option names and values in *filterOptionValues* and show this to the shopper. As an example, if I'm selling shirts, this could show size and color.

Each element representing a column in the form populates with data from the API, except the last column, which has an input field for the quantity.

Now that you have a component that consumes variant data and returns a row for each variant, wire it into the parent form.

4. **Import** it into our parent component, *bulk-variant-form.jsx*:

**`bulk-variant-form.jsx`**

```jsx copy filename="bulk-variant-form.jsx"
import BulkVariantRows from './bulk-variant-rows';
```

5. **Add** the *BulkVariantRows* component in our *renderAfterLoad* method:

**`bulk-variant-form.jsx`**

```jsx copy filename="bulk-variant-form.jsx"
<div className='bulk-variant-row'>
    // --snip
</div>
<BulkVariantRows variants={this.state.variants}/>
<div className='bulk-button-row'>
    // --snip
</div>
```

At this point, when you toggle the bulk order form on a product you should see the variants listed in rows with their image, option values, price, and sku displayed. Now we need to provide the ability to set quantities and add variants to the cart.

## Setting Variant Quantity

We need to update the lineItems in our component state whenever a shopper sets the quantity of any variant.

1. **Create** a new method in *bulk-variant-form.jsx* within the constructor:

**`bulk-variant-form.jsx`**

```jsx copy filename="bulk-variant-form.jsx"
this.changeQty = (variantID, qty) => {
    this.setState({message: ''});
    const lineItems = [...this.state.lineItems];
    lineItems.forEach(item => {
        if (item.variantId == variantID) {
            item.quantity = parseInt(qty);
        }
    })

    this.setState({
        lineItems: lineItems
    })
}
```

This takes the variant ID and quantity set in the input field and updates the lineItems in our state so that they're ready to be passed to a Storefront API request.

2. **Pass** this as a prop to *BulkVariantRows* so that all our variant rows have access to this method:

**`bulk-variant-form.jsx`**

```jsx copy filename="bulk-variant-form.jsx"
<BulkVariantRows variants={this.state.variants} changeQty={this.changeQty}/>
```

3. In *bulk-variant-rows.jsx*, **update** the number `<input>` field so that it calls *changeQty* in response to an *onChange* event:

**`bulk-variant-rows.jsx`**

```jsx copy filename="bulk-variant-rows.jsx"
<input
type='number'
min='0'
placeholder='0'
className='qtyField'
onChange={(e) => props.changeQty(variant.id, e.target.value)}
/>
```

At this point, adjusting the quantity in the form will update the quantity in our state. Now we need to be able to add items to the cart.

4. In *bulk-variant-form.jsx* we'll **add** our last method in the constructor:

**`bulk-variant-form.jsx`**

```jsx copy filename="bulk-variant-form.jsx"
this.addToCart = (e) => {
    const lineItems = this.state.lineItems.map(item => {
        if (item.quantity > 0) {
            return item;
        }
    }).filter(item => {
        if (item !== undefined) {
            return item;
        }
    });

    if (lineItems.length < 1) {
        this.setState({message: 'Please set the quantity of at least 1 item.'})
    } else {
        e.target.disabled = true;
        this.setState({message: 'Adding items to your cart...'})
        fetch(`/api/storefront/cart`)
        .then(response => response.json())
        .then(cart => {
            if(cart.length > 0) {
                return addToExistingCart(cart[0].id)
            } else {
                return createNewCart()
            }
        })
        .then(() => window.location = '/cart.php')
        .catch(err => console.log(err))
    }
    async function createNewCart() {
        const response = await fetch(`/api/storefront/carts`, {
            credentials: "include",
            method: "POST",
            body: JSON.stringify({ lineItems: lineItems })
        });
        const data = await response.json();
        if (!response.ok) {
            return Promise.reject("There was an issue adding items to your cart. Please try again.")
        } else {
            console.log(data);
        }
    }
    async function addToExistingCart(cart_id) {
        const response = await fetch(`/api/storefront/carts/${cart_id}/items`, {
            credentials: "include",
            method: "POST",
            body: JSON.stringify({ lineItems: lineItems })
        });
        const data = await response.json();
        if (!response.ok) {
            return Promise.reject("There was an issue adding items to your cart. Please try again.")
        } else {
            console.log(data);
        }
    }
}
```

To prepare the lineItems we'll send to the Storefront Cart API, we need to get the ID of all variants with a quantity of at least 1. Using map on an array returns a new array with the same number of items, which means any variant with a quantity of 0 is instead replaced with undefined in the new array.

We filter out all the items that are undefined, leaving us with a lineItems array that only includes a shopper's selected variants.

Depending on if any lineItems are present, a message is displayed to the shopper. If there are lineItems, the add to cart button is disabled to prevent multiple submissions, and a fetch is made to the API to check if a cart exists or if a cart needs to be created.

Once the request completes successfully, the shopper is redirected to the cart page.

Now that the add to cart function is defined,

5. **Attach** it to the add to cart button:

**`bulk-variant-form.jsx`**

```jsx copy filename="bulk-variant-form.jsx"
<button className='bulk-variant-col button button--primary' id='bulkAddToCart' onClick={this.addToCart}>Add to Cart</button>
```

![Image](https://storage.googleapis.com/bigcommerce-production-dev-center/learning-edu/stencil-advanced/5H83NZj-aAjQnJ9t_Dti_P-B9Zuc-F4on.png)

## Summary

On product pages using a custom layout template, a React app is activated in the background after the default product view is rendered. After the React app is mounted on the page, a request is made to send the product's ID to an endpoint belonging to middleware.

The middleware makes a request to the BigCommerce Catalog API to retrieve the product's variants. The middleware sends variant data back to the React app.

A bulk order form is generated from the variants. The variant ID and the quantity for each variant are tracked in the component state.

When a shopper adds variants to their cart, the variant ID and quantity are included in a request to the BigCommerce Storefront Cart API. After the request to the Cart API succeeds, a shopper is redirected to the cart page.

The shopper completes their purchase, marveling at how quickly they could buy all those items.