How to build a custom product configurator on Shopify (without an app)

A step-by-step build guide for a custom product configurator on Shopify, using line item properties, sections, and a small amount of vanilla JS.

Plenty of Shopify apps will sell you a product configurator. Most of them do the basics in 200 lines of code, plus a $39/month subscription. If you want full control and you’re comfortable in Liquid + vanilla JS, here’s how to build one yourself.

We’ll build an engraving configurator: customer picks a font, types text, sees a preview, and the engraving details flow through to the order. Same pattern works for jewellery customisation, custom packaging, monogramming, etc.

The data model

We have three pieces of customer-supplied data:

  • Engraving text — string, max 20 chars
  • Font — choice from Serif | Sans | Script
  • PositionFront | Back

These flow to the order as line item properties — the Shopify-native way to attach metadata to a cart item.

The Liquid section

In your PDP template, add a new section engraving-config.liquid:

<div class="engraving" data-engraving>
  <label>
    Engraving text
    <input type="text" name="properties[Engraving]" maxlength="20" />
  </label>

  <fieldset>
    <legend>Font</legend>
    <label><input type="radio" name="properties[Font]" value="Serif" checked /> Serif</label>
    <label><input type="radio" name="properties[Font]" value="Sans" /> Sans</label>
    <label><input type="radio" name="properties[Font]" value="Script" /> Script</label>
  </fieldset>

  <fieldset>
    <legend>Position</legend>
    <label><input type="radio" name="properties[Position]" value="Front" checked /> Front</label>
    <label><input type="radio" name="properties[Position]" value="Back" /> Back</label>
  </fieldset>

  <div class="engraving__preview" data-engraving-preview>
    <span data-preview-text></span>
  </div>
</div>

The name="properties[X]" syntax is the magic — Shopify reads anything inside properties[*] and attaches it to the cart line item.

The preview

Vanilla JS, no dependencies:

(() => {
  const root = document.querySelector('[data-engraving]');
  if (!root) return;
  const text = root.querySelector('[data-preview-text]');
  const input = root.querySelector('input[name="properties[Engraving]"]');
  const fontRadios = root.querySelectorAll('input[name="properties[Font]"]');

  const sync = () => {
    text.textContent = input.value || 'Your text';
    const font = root.querySelector('input[name="properties[Font]"]:checked').value;
    text.dataset.font = font.toLowerCase();
  };

  input.addEventListener('input', sync);
  fontRadios.forEach(r => r.addEventListener('change', sync));
  sync();
})();

Style the preview based on data-font:

.engraving__preview [data-preview-text][data-font="serif"]  { font-family: "Fraunces", serif; }
.engraving__preview [data-preview-text][data-font="sans"]   { font-family: "Inter", sans-serif; }
.engraving__preview [data-preview-text][data-font="script"] { font-family: "Caveat", cursive; }

Conditional pricing (optional)

If engraving costs $15 extra, you have two options:

Option A — separate “engraving” line item product. When the customer adds the main product, your JS also adds an “Engraving” product to the cart at $15. Pros: simple, works on any plan. Cons: shows as two line items.

Option B — Cart Function (Plus only). A Cart Transform Function detects the engraving property and bumps the line item price.

For most stores, Option A is fine. Customers see “Necklace ($120) + Engraving ($15)” and that’s actually clearer.

Validating before add-to-cart

Don’t trust the form. Validate on the JS side too:

form.addEventListener('submit', (e) => {
  const txt = input.value.trim();
  if (txt.length > 20) {
    e.preventDefault();
    alert('Engraving must be 20 characters or fewer.');
  }
});

Server-side validation (e.g. via a Shopify Function or webhook) is overkill for engraving. For high-stakes configurators (e.g. monogram colour matching), you’d validate at the order webhook level too.

Showing the configuration in the cart, checkout, and order

Line item properties show automatically:

  • In the cart drawer (most themes render item.properties automatically)
  • In the checkout summary
  • On the order confirmation email
  • On the merchant’s order detail page

If your theme doesn’t render properties in the cart, this is the snippet:

{% for property in item.properties %}
  {% unless property.last == blank %}
    <li>{{ property.first }}: {{ property.last }}</li>
  {% endunless %}
{% endfor %}

The unless property.last == blank is important — Shopify creates empty properties for some apps; you don’t want them showing.

When this approach breaks

  • Hundreds of options. If your configurator has dozens of variables (think: custom furniture), Liquid + JS gets unwieldy. Consider a real configurator app or a custom React/Hydrogen approach.
  • 3D / WebGL preview. If you need to render a real 3D visualisation, you’re past Liquid territory.
  • Complex pricing. If engraving cost varies by length, font, position — you want server-side logic, either a Cart Function or a real app.

For 90% of “let the customer customise this product” use cases, this 100-line approach beats every $39/month app.

— Read next

Migrating from BigCommerce, Wix or WooCommerce to Shopify — the playbook

— Hit a wall?

We can help building properly?