PDP Page

Topic summary

Request to create a PDP (product detail page) matching a provided screenshot. Clarified that the desired layout is a custom product configurator, not a standard Shopify PDP.

Key build guidance:

  • Product setup: use variants only for sizes; do not create variants for colors/prints.
  • Colors: implement custom swatch buttons; save the selected color as a line item property (cart-level option) and swap images with JavaScript.
  • Tabs (Front/Back/Sleeve): build with custom HTML + JavaScript; not a native Shopify feature.
  • Add-on pricing (+$3/+$5): use checkboxes; update the displayed price via JavaScript and store selections as line item properties. (Optionally use Shopify Functions for cleaner pricing logic.)
  • Size-based price increases: handle with variant pricing (e.g., 2XL, 3XL).

Constraints and tools:

  • Cannot be achieved through the theme editor alone; requires Liquid + JavaScript or a product options app (e.g., Customily, Kickflip, Zepto).

Status: Guidance provided; implementation not confirmed, discussion effectively open. Image reference informs the desired design.

Summarized with AI on January 28. AI used: gpt-5.

Hi All,

I want design and add functionality pdp page as per attached screenshot. Anyone help me to create pdp page design.

1 Like

Thanks Zuhairan

I have done almost with customize code only Add-on pricing (+$3 / +$5) not showing cart page.

Can you help me to create this?

Can you help me this one

Create hidden add-on variants ($3, $5).

When checkbox is checked, use JS to auto-add the add-on variant to cart.

This makes pricing show correctly in cart and checkout.

You could go the easy route in this. So with variant metafields display the extra amount for the sizes and the checkbox selected. And where you set the price create prices for those variants including that size and selection.

Since all these are variants, you so get to add different prices for different variation mix in product admin.

Hope I was able to explain

Best

@shreys did it work on your end?

not yet, this code i used.

.custom-options-wrapper { margin: 20px 0; border: 1px solid #e5e5e5; border-radius: 8px; padding: 20px; } /\* Color Swatches \*/ .color-swatches { margin-bottom: 20px; } .color-swatches h3 { font-size: 14px; font-weight: 600; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; } .color-options { display: flex; gap: 8px; flex-wrap: wrap; } .color-swatch { width: 40px; height: 40px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; transition: all 0.2s ease; position: relative; background-size: cover; background-position: center; } .color-swatch:hover { transform: scale(1.1); border-color: #333; } .color-swatch.selected { border-color: #333; box-shadow: 0 0 0 2px #fff, 0 0 0 4px #333; } .color-swatch-label { font-size: 11px; text-align: center; margin-top: 4px; color: #666; } /\* Tab Navigation \*/ .customization-tabs { display: flex; gap: 0; margin-bottom: 20px; border-bottom: 2px solid #e5e5e5; flex-wrap: wrap; } .tab-button { flex: 1; min-width: 100px; padding: 12px 20px; background: transparent; border: none; font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer; transition: all 0.3s ease; position: relative; color: #666; } .tab-button:hover { color: #333; } .tab-button.active { color: #c8102e; background: transparent; } .tab-button.active::after { content: ''; position: absolute; bottom: -2px; left: 0; right: 0; height: 2px; background: #c8102e; } /\* Tab Content \*/ .tab-content { display: none; } .tab-content.active { display: block; } .customization-options { margin-bottom: 20px; } .option-checkbox { display: flex; align-items: center; padding: 10px 0; cursor: pointer; } .option-checkbox input\[type="checkbox"\] { width: 18px; height: 18px; margin-right: 10px; cursor: pointer; accent-color: #c8102e; } .option-checkbox label { cursor: pointer; font-size: 14px; display: flex; align-items: center; gap: 5px; width: 100%; } .option-price { color: #666; font-size: 13px; margin-left: auto; } /\* Size Selection with Quantities \*/ .size-selection { margin-top: 20px; } .size-selection h4 { font-size: 14px; font-weight: 600; margin-bottom: 16px; text-transform: uppercase; letter-spacing: 0.5px; } .size-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; margin-bottom: 15px; } .size-item { border: 1px solid #ddd; border-radius: 8px; padding: 12px; background: #fff; transition: all 0.2s ease; } .size-item:hover { border-color: #c8102e; box-shadow: 0 2px 8px rgba(200, 16, 46, 0.1); } .size-item.has-quantity { border-color: #c8102e; background: #fff5f7; } .size-label { font-size: 16px; font-weight: 600; margin-bottom: 8px; text-align: center; color: #333; } .size-price { font-size: 12px; color: #666; text-align: center; margin-bottom: 8px; } .quantity-selector { display: flex; align-items: center; justify-content: center; gap: 8px; } .qty-btn { width: 28px; height: 28px; border: 1px solid #ddd; background: #fff; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; color: #333; transition: all 0.2s ease; } .qty-btn:hover { background: #c8102e; color: white; border-color: #c8102e; } .qty-btn:disabled { opacity: 0.3; cursor: not-allowed; background: #f5f5f5; } .qty-input { width: 50px; height: 32px; text-align: center; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; font-weight: 600; } /\* Size upcharge info \*/ .size-upcharge-note { font-size: 12px; color: #666; margin-top: 10px; text-align: center; font-style: italic; } /\* Price Display \*/ .dynamic-price-display { margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 6px; } .price-breakdown { font-size: 14px; } .price-row { display: flex; justify-content: space-between; margin-bottom: 8px; } .price-row.total { font-weight: 700; font-size: 18px; border-top: 2px solid #ddd; padding-top: 12px; margin-top: 12px; color: #c8102e; } .total-items { font-size: 12px; color: #666; text-align: right; margin-top: 4px; } /\* Add to Cart Button \*/ .custom-add-to-cart { width: 100%; padding: 16px; background: #c8102e; color: white; border: none; border-radius: 4px; font-size: 15px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer; transition: background 0.3s ease; } .custom-add-to-cart:hover { background: #a00d25; } .custom-add-to-cart:disabled { background: #ccc; cursor: not-allowed; } @media (max-width: 768px) { .tab-button { font-size: 11px; padding: 10px 12px; } .size-grid { grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px; } .color-swatch { width: 35px; height: 35px; } }

{% assign color_option = product.options_with_values | where: “name”, “Color” | first %}

{% if color_option %}

<h3>Colors</h3>

<div class="color-options">

  {% for value in color_option.values %}

    {% assign color_name = value %}

    {% assign color_handle = value | handleize %}

    

    <div>

      <button type="button" 

              class="color-swatch{% if forloop.first %} selected{% endif %}" 

              data-color="{{ color_name }}"

              style="background-color: {{ value | replace: ' ', '' | downcase }};"

              title="{{ color_name }}">

      </button>

      

    </div>

  {% endfor %}

</div>

{% endif %}

Custom Text

<input

type="text"

id="customTextInput"

placeholder="Enter custom text here"

style="

  width:100%;

  padding:12px;

  border:1px solid #ddd;

  border-radius:6px;

  font-size:14px;

"
<button type="button" class="tab-button active" data-tab="front">Front</button>

<button type="button" class="tab-button" data-tab="fullback">Full Back</button>

<button type="button" class="tab-button" data-tab="leftsleeve">Left Sleeve</button>

<button type="button" class="tab-button" data-tab="rightsleeve">Right Sleeve</button>
<div class="customization-options">

  <div class="option-checkbox">

    <input type="checkbox" id="front-printed" name="customization\[front\]" value="Left Chest Printed Logo" data-price="3">

    <label for="front-printed">

      <span>Left Chest Printed Logo</span>

      <span class="option-price">+$3</span>

    </label>

  </div>

  <div class="option-checkbox">

    <input type="checkbox" id="front-embroidered" name="customization\[front\]" value="Left Chest Embroidered Logo" data-price="5">

    <label for="front-embroidered">

      <span>Left Chest Embroidered Logo</span>

      <span class="option-price">+$5</span>

    </label>

  </div>

  <div class="option-checkbox">

    <input type="checkbox" id="front-full" name="customization\[front\]" value="Full Chest Printed" data-price="5">

    <label for="front-full">

      <span>Full Chest Printed</span>

      <span class="option-price">+$5</span>

    </label>

  </div>

</div>
<div class="customization-options">

  <div class="option-checkbox">

    <input type="checkbox" id="back-printed" name="customization\[fullback\]" value="Full Back Printed Logo" data-price="5">

    <label for="back-printed">

      <span>Full Back Printed Logo</span>

      <span class="option-price">+$5</span>

    </label>

  </div>

</div>
<div class="customization-options">

  <div class="option-checkbox">

    <input type="checkbox" id="leftsleeve-printed" name="customization\[leftsleeve\]" value="Left Sleeve Printed" data-price="3">

    <label for="leftsleeve-printed">

      <span>Left Sleeve Printed</span>

      <span class="option-price">+$3</span>

    </label>

  </div>

  <div class="option-checkbox">

    <input type="checkbox" id="leftsleeve-embroidered" name="customization\[leftsleeve\]" value="Left Sleeve Embroidered" data-price="5">

    <label for="leftsleeve-embroidered">

      <span>Left Sleeve Embroidered</span>

      <span class="option-price">+$5</span>

    </label>

  </div>

</div>
<div class="customization-options">

  <div class="option-checkbox">

    <input type="checkbox" id="rightsleeve-printed" name="customization\[rightsleeve\]" value="Right Sleeve Printed" data-price="3">

    <label for="rightsleeve-printed">

      <span>Right Sleeve Printed</span>

      <span class="option-price">+$3</span>

    </label>

  </div>

  <div class="option-checkbox">

    <input type="checkbox" id="rightsleeve-embroidered" name="customization\[rightsleeve\]" value="Right Sleeve Embroidered" data-price="5">

    <label for="rightsleeve-embroidered">

      <span>Right Sleeve Embroidered</span>

      <span class="option-price">+$5</span>

    </label>

  </div>

</div>

{% assign size_option = product.options_with_values | where: “name”, “Size” | first %}

{% if size_option %}

<h4>Adult - Select Quantities by Size</h4>

<div class="size-grid" id="sizeGrid">

  {% for value in size_option.values %}

    {% assign size_name = value %}

    {% assign size_handle = value | handleize %}

    

    {% assign upcharge = 0 %}

    

    <div class="size-item" data-size="{{ size_name }}" data-upcharge="{{ upcharge }}">

      <div class="size-label">{{ size_name }}</div>

      {% if upcharge > 0 %}

        <div class="size-price">+${{ upcharge }} each</div>

      {% else %}

        <div class="size-price">&nbsp;</div>

      {% endif %}

      

      <div class="quantity-selector">

        <button type="button" class="qty-btn qty-minus" data-size="{{ size_name }}">−</button>

        <input type="number" 

               class="qty-input" 

               data-size="{{ size_name }}"

               value="0" 

               min="0" 

               max="999"

               readonly>

        <button type="button" class="qty-btn qty-plus" data-size="{{ size_name }}">+</button>

      </div>

    </div>

  {% endfor %}

</div>

<div class="size-upcharge-note">

 

</div>

{% endif %}

<div class="price-breakdown">

  <div class="price-row">

    <span>Base Price (per item):</span>

    <span id="basePrice">{{ product.price | money }}</span>

  </div>

  <div class="price-row" id="customizationRow" style="display: none;">

    <span>Customization:</span>

    <span id="customizationPrice">$0.00</span>

  </div>

  <div class="price-row" id="sizeUpchargeRow" style="display: none;">

    <span>Size Upcharges:</span>

    <span id="sizeUpcharge">$0.00</span>

  </div>

  <div class="price-row">

    <span>Subtotal:</span>

    <span id="subtotalPrice">$0.00</span>

  </div>

  <div class="price-row total">

    <span>Total Price:</span>

    <span id="totalPrice">$0.00</span>

  </div>

  <div class="total-items" id="totalItems">0 items</div>

</div>
Add to Cart

document.addEventListener(‘DOMContentLoaded’, function() {

const customOptions = document.getElementById(‘customProductOptions’);

if (!customOptions) return;

const productPrice = {{ product.price | money_without_currency | remove: ‘,’ }};

// Tab switching

const tabButtons = customOptions.querySelectorAll(‘.tab-button’);

const tabContents = customOptions.querySelectorAll(‘.tab-content’);

tabButtons.forEach(button => {

button.addEventListener('click', function() {

  const targetTab = this.dataset.tab;

  

  tabButtons.forEach(btn => btn.classList.remove('active'));

  tabContents.forEach(content => content.classList.remove('active'));

  

  this.classList.add('active');

  document.getElementById('tab-' + targetTab).classList.add('active');

});

});

// Color selection

const colorSwatches = customOptions.querySelectorAll(‘.color-swatch’);

let selectedColor = colorSwatches.length > 0 ? colorSwatches[0].dataset.color : ‘’;

colorSwatches.forEach(swatch => {

swatch.addEventListener('click', function() {

  colorSwatches.forEach(s => s.classList.remove('selected'));

  this.classList.add('selected');

  selectedColor = this.dataset.color;

});

});

// Quantity management

const quantities = {};

function updateQuantity(size, change) {

const input = customOptions.querySelector(\`.qty-input\[data-size="${size}"\]\`);

const sizeItem = customOptions.querySelector(\`.size-item\[data-size="${size}"\]\`);

let currentQty = parseInt(input.value) || 0;



currentQty += change;

if (currentQty < 0) currentQty = 0;

if (currentQty > 999) currentQty = 999;



input.value = currentQty;

quantities\[size\] = currentQty;



// Visual feedback

if (currentQty > 0) {

  sizeItem.classList.add('has-quantity');

} else {

  sizeItem.classList.remove('has-quantity');

}



calculatePrice();

}

// Plus/Minus buttons

customOptions.querySelectorAll(‘.qty-plus’).forEach(btn => {

btn.addEventListener('click', function() {

  const size = this.dataset.size;

  updateQuantity(size, 1);

});

});

customOptions.querySelectorAll(‘.qty-minus’).forEach(btn => {

btn.addEventListener('click', function() {

  const size = this.dataset.size;

  updateQuantity(size, -1);

});

});

// Direct input

customOptions.querySelectorAll(‘.qty-input’).forEach(input => {

input.addEventListener('change', function() {

  const size = this.dataset.size;

  let value = parseInt(this.value) || 0;

  if (value < 0) value = 0;

  if (value > 999) value = 999;

  this.value = value;

  quantities\[size\] = value;

  calculatePrice();

});

});

// Price calculation - THIS IS THE KEY FIX

function calculatePrice() {

let totalQuantity = 0;

let subtotal = 0;

let customizationCostPerItem = 0;

let sizeUpchargeTotal = 0;



// Calculate total quantity and base cost

Object.keys(quantities).forEach(size => {

  const qty = quantities\[size\];

  if (qty > 0) {

    totalQuantity += qty;

    

    const sizeItem = customOptions.querySelector(\`.size-item\[data-size="${size}"\]\`);

    const upcharge = parseFloat(sizeItem.dataset.upcharge || 0);

    

    subtotal += (productPrice + upcharge) \* qty;

    sizeUpchargeTotal += upcharge \* qty;

  }

});



// Calculate customization cost per item

const checkedOptions = customOptions.querySelectorAll('input\[type="checkbox"\]:checked');

checkedOptions.forEach(option => {

  customizationCostPerItem += parseFloat(option.dataset.price || 0);

});



const customizationTotal = customizationCostPerItem \* totalQuantity;



// Update display

document.getElementById('basePrice').textContent = '$' + productPrice.toFixed(2);



if (customizationTotal > 0) {

  document.getElementById('customizationRow').style.display = 'flex';

  document.getElementById('customizationPrice').textContent = '$' + customizationTotal.toFixed(2);

} else {

  document.getElementById('customizationRow').style.display = 'none';

}



if (sizeUpchargeTotal > 0) {

  document.getElementById('sizeUpchargeRow').style.display = 'flex';

  document.getElementById('sizeUpcharge').textContent = '$' + sizeUpchargeTotal.toFixed(2);

} else {

  document.getElementById('sizeUpchargeRow').style.display = 'none';

}



document.getElementById('subtotalPrice').textContent = '$' + subtotal.toFixed(2);



const totalPrice = subtotal + customizationTotal;

document.getElementById('totalPrice').textContent = '$' + totalPrice.toFixed(2);

document.getElementById('totalItems').textContent = totalQuantity + ' item' + (totalQuantity !== 1 ? 's' : '');



// Enable/disable add to cart button

const addToCartBtn = document.getElementById('customAddToCart');

if (totalQuantity > 0) {

  addToCartBtn.disabled = false;

} else {

  addToCartBtn.disabled = true;

}



return {

  totalQuantity,

  subtotal,

  customizationTotal,

  sizeUpchargeTotal,

  totalPrice,

  customizationCostPerItem

};

}

// Listen for changes

customOptions.querySelectorAll(‘input[type=“checkbox”]’).forEach(input => {

input.addEventListener('change', calculatePrice);

});

// Initial calculation

calculatePrice();

// UPDATED Add to Cart functionality

document.getElementById(‘customAddToCart’).addEventListener(‘click’, function() {

const prices = calculatePrice();



if (prices.totalQuantity === 0) {

  alert('Please select at least one size with quantity');

  return;

}



// Get custom text

const customText = document.getElementById('customTextInput').value;



// Gather all selected options

const customizations = \[\];

const checkedOptions = customOptions.querySelectorAll('input\[type="checkbox"\]:checked');

checkedOptions.forEach(option => {

  customizations.push(option.value);

});



// Build size breakdown

const sizeBreakdown = \[\];

Object.keys(quantities).forEach(size => {

  if (quantities\[size\] > 0) {

    sizeBreakdown.push(\`${size}: ${quantities\[size\]}\`);

  }

});



// MAIN FIX: Store all pricing information in properties

const properties = {

  'Color': selectedColor || 'Not specified',

  'Custom Text': customText || 'None',

  'Sizes': sizeBreakdown.join(', '),

  'Total Quantity': prices.totalQuantity.toString(),

  'Customizations': customizations.join(', ') || 'None',

  '\_Base_Price': '$' + productPrice.toFixed(2),

  '\_Customization_Per_Item': '$' + prices.customizationCostPerItem.toFixed(2),

  '\_Total_Customization': '$' + prices.customizationTotal.toFixed(2),

  '\_Subtotal': '$' + prices.subtotal.toFixed(2),

  '\_Total_Price': '$' + prices.totalPrice.toFixed(2)

};



const formData = {

  items: \[{

    id: {{ product.variants.first.id }},

    quantity: prices.totalQuantity,

    properties: properties

  }\]

};



// Show loading state

this.textContent = 'Adding...';

this.disabled = true;



fetch('/cart/add.js', {

  method: 'POST',

  headers: {

    'Content-Type': 'application/json',

  },

  body: JSON.stringify(formData)

})

.then(response => response.json())

.then(data => {

  console.log('Added to cart:', data);

  

  // Update cart attributes with total custom price

  return fetch('/cart/update.js', {

    method: 'POST',

    headers: {

      'Content-Type': 'application/json',

    },

    body: JSON.stringify({

      attributes: {

        '\_has_custom_pricing': 'true',

        '\_custom_total': prices.totalPrice.toFixed(2)

      }

    })

  });

})

.then(() => {

  window.location.href = '/cart';

})

.catch(error => {

  console.error('Error:', error);

  alert('Error adding to cart. Please try again.');

  this.textContent = 'Add to Cart';

  this.disabled = false;

});

});

});

@shreys sent you a dm

okay got it i will do

@shreys sent you a message via the provided email