Skip to main content
Most direct-response checkouts open with Step 1: Select your quantity: one row per bundle, one row already picked, and a total that follows the choice. The package selector is the runtime feature behind that block. You mark up the rows with a few data-ef-package attributes; the checkout does the rest. When the buyer picks a row:
  • the checkout’s main product becomes that package (checkout.product_code, product_title, product_price, retail_price, the cart line and every total);
  • ticked order bumps stay ticked;
  • bonus rules, coupon eligibility, tax, shipping and the Apple Pay / Google Pay amount are recalculated for the new package;
  • the address bar gets ?p=<code> (no new history entry), so a reload keeps the choice;
  • the card submit, PayPal and Klarna all charge the package the buyer picked.
Nothing reloads, so the form the buyer already filled stays filled.

Copy-paste example

Three bundles of one product, WRAP-1, WRAP-2 and WRAP-4. Each row is a <label> with a radio input, so it works with a keyboard and a screen reader without extra code.
The order summary needs no extra wiring. Bind it to the checkout scope as usual and it follows the selector:
Style the picked row on [data-selected="true"]. The server writes that attribute on the row the checkout sells, so the right row is highlighted on first paint, before any JavaScript runs, and the browser moves it on every switch.

Attributes

The price attributes are optional. With them, the switch is instant and makes no request. When a row has no price, the checkout fetches the package prices once from the products API and the switch waits for that answer. Either way, the server prices the order from the product code, so a wrong attribute can never change what the buyer is charged, only what the page shows.
The data-ef-product-* spellings used on add-to-cart buttons (data-ef-product-price, data-ef-product-name, …) are read too.

Selected state

On every row the runtime keeps these in step:
  • data-selected="true" or "false"
  • aria-checked="true" or "false" (and role="radio" on rows that are not inputs)
  • the class is-selected
  • checked on the row’s radio input
Rows that are not focusable get tabindex="0", so Tab then Space or Enter picks a row. A row can be a buy link, for example <a href="[BUY=WRAP-2]" data-ef-package="WRAP-2">. The link is the fallback for a browser without JavaScript. With JavaScript, a click picks the package and never follows the link, and it is not recorded as a buy-link click.

Product-code mode and cart mode

A checkout sells either one product (from ?p=<code> or from the page’s backend script) or a cart. The selector works in both. A checkout that sells nothing yet (no ?p=, empty cart) picks the default row and runs in product-code mode.
In product-code mode, a ?p= that is not one of the rows (for example a product from another funnel) is left alone: no row is highlighted and nothing switches until the buyer picks one.

Pages that set their product in a backend script

A page whose backend script sets checkout_product pins that product to the session, and the submit charges the pin, whatever the browser posts. List the packages too, or the buyer’s pick is ignored at payment:
With checkout_packages set, a posted code from that list is charged, ?p= naming one of them is rendered on reload, and any other posted code still falls back to checkout_product. See How to design a checkout page.

Order bumps

Bumps the page offers stay offered, and ticked bumps stay ticked, across a switch. One exception: a bump whose product code is the package the buyer just picked is taken out of checkout.bump_products (it would sell the same product twice) and comes back when the buyer picks another package.

Events and API

checkout:packageChanged fires on every switch the buyer makes, not when the page loads. Each switch is also recorded in analytics as a package-selected event with the picked code. From your own scripts:

Troubleshooting