how-to · · 8 min read
how to add a custom input field to a woocommerce product page
Add a text field to a WooCommerce product page (engraving, a gift message) that is checked, shown in the cart and saved on the order. Five hooks, no plugin.
By Big Bad Agency
Engraving text. A name on a jersey. A gift message. A date for a cake. Sooner or later a store needs the customer to type something on the product page and needs that text to land on the order where the person packing it will actually see it.
Putting an input box on the page is the easy part. Most tutorials stop there, and that’s the problem: the text then has to survive four more steps before anyone can act on it. It has to be checked, kept with the item in the cart, shown back to the customer, and saved onto the order. Miss one step and you get a box that looks like it works and quietly throws the customer’s text away.
Here’s the whole chain, five hooks, in code you can paste into a small plugin or your child theme’s functions.php.
first, is a field the right tool?
A free-text field is for things only the customer knows. Before you build one, check you’re not using it for something WooCommerce already handles better:
- A fixed set of choices (size, colour, finish) should be variations. They get their own price, stock and SKU, and a typo can’t create a size that doesn’t exist.
- Facts about the product (brand, material, model) should be attributes. They’re filterable, consistent across the catalogue, and nobody types them at checkout. If those attribute values are themselves products in your store, like a brand you also sell as a product, our free Product-Linked Attributes Mapper turns them into links to those product pages.
- Anything the customer types (a name, a message, a measurement) is a field. That’s what this post builds.
the five steps
We’ll build an engraving field, up to 20 characters, shown only on products tagged engravable. Swap the names for your own.
1. show the field
woocommerce_before_add_to_cart_button fires inside the add-to-cart form, which matters: anything outside the form never gets submitted. It fires for simple products and inside the variation form on variable products.
add_action( 'woocommerce_before_add_to_cart_button', function () {
global $product;
if ( ! $product || ! has_term( 'engravable', 'product_tag', $product->get_id() ) ) {
return;
}
?>
<p class="form-row engraving-field">
<label for="engraving_text"><?php esc_html_e( 'Engraving (up to 20 characters)', 'your-textdomain' ); ?></label>
<input type="text" id="engraving_text" name="engraving_text" maxlength="20" />
</p>
<?php
} );
The maxlength is a courtesy to the customer. It’s not a rule anyone has to obey, because a browser can be told to ignore it. The real check is the next step.
2. check it before it goes in the cart
add_filter( 'woocommerce_add_to_cart_validation', function ( $passed, $product_id, $quantity ) {
if ( isset( $_POST['engraving_text'] ) ) {
$text = sanitize_text_field( wp_unslash( $_POST['engraving_text'] ) );
if ( mb_strlen( $text ) > 20 ) {
wc_add_notice( __( 'Engraving can be up to 20 characters.', 'your-textdomain' ), 'error' );
return false;
}
}
return $passed;
}, 10, 3 );
Returning false stops the add-to-cart, and the notice tells the customer why. Use mb_strlen, not strlen. An accented letter or an emoji is more than one byte, and strlen would refuse a name that fits.
3. keep it on the cart line
add_filter( 'woocommerce_add_cart_item_data', function ( $cart_item_data, $product_id, $variation_id ) {
if ( ! empty( $_POST['engraving_text'] ) ) {
$cart_item_data['engraving_text'] = sanitize_text_field( wp_unslash( $_POST['engraving_text'] ) );
}
return $cart_item_data;
}, 10, 3 );
This is the step most snippets get half right. Whatever you add here becomes part of the cart line’s identity. WooCommerce builds the line’s ID from the product, the variation and this data, so two of the same mug with different names become two separate lines, not one line with quantity two. That’s what you want. Each one has to be engraved differently.
4. show it back in the cart and at checkout
add_filter( 'woocommerce_get_item_data', function ( $item_data, $cart_item ) {
if ( ! empty( $cart_item['engraving_text'] ) ) {
$item_data[] = array(
'key' => __( 'Engraving', 'your-textdomain' ),
'value' => $cart_item['engraving_text'],
);
}
return $item_data;
}, 10, 2 );
Customers check this. A misspelt name caught in the cart is a two-second fix. Caught after engraving, it’s a remake and an apology.
5. save it on the order
add_action( 'woocommerce_checkout_create_order_line_item', function ( $item, $cart_item_key, $values, $order ) {
if ( ! empty( $values['engraving_text'] ) ) {
$item->add_meta_data( __( 'Engraving', 'your-textdomain' ), $values['engraving_text'], true );
}
}, 10, 4 );
Cart data is temporary. Without this step the text disappears the moment the order is placed. With it, the engraving is stored as meta on that line item. It shows under the product in the admin order screen and in the order emails, which is where the person doing the work will look for it.
In current WooCommerce the block-based Checkout builds its order lines through the same function as the classic checkout, so this hook fires for both. Still, place a test order through whichever checkout your store actually uses before you trust it.
making it required
If an engravable product without engraving makes no sense, replace step 2 with a version that refuses an empty field:
add_filter( 'woocommerce_add_to_cart_validation', function ( $passed, $product_id, $quantity ) {
if ( ! has_term( 'engravable', 'product_tag', $product_id ) ) {
return $passed;
}
$text = isset( $_POST['engraving_text'] ) ? sanitize_text_field( wp_unslash( $_POST['engraving_text'] ) ) : '';
if ( '' === $text ) {
wc_add_notice( __( 'Please enter the engraving text.', 'your-textdomain' ), 'error' );
return false;
}
if ( mb_strlen( $text ) > 20 ) {
wc_add_notice( __( 'Engraving can be up to 20 characters.', 'your-textdomain' ), 'error' );
return false;
}
return $passed;
}, 10, 3 );
Know the side effect. The “Add to cart” buttons on shop and category pages don’t show your field, so for a simple product they submit without it, and this check refuses them. On a classic theme WooCommerce responds by sending the shopper to the product page, where the field is. That’s arguably what you want, but it should be a decision, not a surprise. Check what your theme’s shop page does.
charging extra for it
If engraving costs $5, adjust the price on lines that carry engraving text:
add_action( 'woocommerce_before_calculate_totals', function ( $cart ) {
foreach ( $cart->get_cart() as $cart_item ) {
if ( empty( $cart_item['engraving_text'] ) ) {
continue;
}
$fresh = wc_get_product( $cart_item['data']->get_id() );
$cart_item['data']->set_price( (float) $fresh->get_price( 'edit' ) + 5 );
}
} );
Two things in there are deliberate:
- It starts from a freshly loaded price every time. Totals get calculated more than once per page load, and adding $5 to the price the cart is already holding would add it again on every pass.
- The surcharge is in the line price. That’s simple, and the customer sees it in the line total. If you’d rather show “Engraving: $5.00” as its own line, use a cart fee (
woocommerce_cart_calculate_fees) instead. Don’t do both.
Also tell the customer about the charge next to the field. A price that changes in the cart without warning is the kind of surprise that ends a checkout.
before it goes live
- Test it on staging first: simple product, variable product, two lines with different text, a required-field refusal, and a full order through to the confirmation email.
- Look at the order in the admin, not just the thank-you page. The admin order screen is what your team will work from.
- Escape on the way out.
sanitize_text_fieldcleans the input. WooCommerce escapes item meta when it displays it, but if you print the value anywhere yourself (a packing slip, a custom email), escape it there withesc_html. - Mind your text domain. Replace
your-textdomainwith your plugin’s or theme’s, or the labels can’t be translated.
when to reach for a plugin instead
Five hooks is fine for one or two fields. If you need a dozen field types, per-product field builders, conditional logic, file uploads or price rules managed by non-developers, a product add-ons plugin is the better tool. That’s a whole product, and maintaining a homegrown version of one isn’t a good use of anyone’s time.
But if what you need is “let the customer type one thing and make sure it reaches the order”, the code above is the whole job. It’s code you own, with nothing to renew.