Hi all,
I’m using the Dawn 15.0.3 theme on my Shopify store.
I need help with a common UX issue for products with multiple options/variants.
Context
- My products have two options (e.g. “Version” and “Format”), both displayed as radio buttons.
- The name attribute of the inputs is not a plain string but has a line break, e.g.
Option-1\n and Format-2\n. This is auto-generated by Shopify/Dawn and I can’t easily change it.
- Example snippet:
<input type="radio" name="Option-1
" value="Version digitale par email (JPEG)" checked>
<input type="radio" name="Format-2
" value="Digital" checked>
- When I change the first variant (Version), the second one (Format) keeps its previous value. This sometimes results in invalid variant combinations.
What I Want
Whenever the first variant (option 1) is changed, the second variant (option 2) should be:
- reset to its default value (hopefully the first one available)
What I’ve Tried
- I tried to use JavaScript to listen for
change events on the first option, and then manually reset the second one by clearing/checking the radio input.
- My code works only on initial page load, not when options are changed dynamically by the user.
- The line break in the input
name attribute (Option-1\n) makes classic selectors (like input[name="Option-1"]) not work as expected.
Example JS I Tried
document.querySelectorAll('input[type="radio"]').forEach(r => {
console.log('name:', r.name, 'value:', r.value, 'checked:', r.checked);
});
// Listen for change
document.querySelectorAll('input[name="Option-1\n"]').forEach(input => {
input.addEventListener('change', function() {
// Try to reset second option
document.querySelectorAll('input[name="Format-2\n"]').forEach(f => f.checked = false);
});
});
But the script often fails because of the line break, and Shopify/Dawn seems to re-render the DOM on every option change, so my event listeners are lost.
Question
How can I reliably reset the second variant radio group when the first is changed, in Dawn 15.0.3?
Is there a “Shopify way” to do this, or a robust JS snippet that works even if the DOM changes?
How do I best select the right inputs with these strange name attributes?
Thanks a lot for your help! I would really apreciate it.
If you need more code/context, just ask.
Hey @Lucie06 ,
I put together a solution that should handle all the issues you mentioned:
variant-reset.js
// Dawn 15.0.3 Variant Reset Script
// This script resets the second variant when the first one changes
(function() {
'use strict';
// Configuration - adjust these if needed
const FIRST_OPTION_IDENTIFIER = 'Option-1'; // without the line break
const SECOND_OPTION_IDENTIFIER = 'Format-2'; // without the line break
// Function to find inputs by name containing the identifier
function findInputsByNameContaining(identifier) {
return Array.from(document.querySelectorAll('input[type="radio"]')).filter(input => {
// Handle the line break issue by checking if name contains our identifier
return input.name && input.name.trim().includes(identifier);
});
}
// Function to reset second variant to first available option
function resetSecondVariant() {
const secondVariantInputs = findInputsByNameContaining(SECOND_OPTION_IDENTIFIER);
if (secondVariantInputs.length > 0) {
// First, uncheck all second variant options
secondVariantInputs.forEach(input => input.checked = false);
// Then check the first one (default)
secondVariantInputs[0].checked = true;
// Trigger change event to update Shopify's variant system
secondVariantInputs[0].dispatchEvent(new Event('change', { bubbles: true }));
console.log('Reset second variant to:', secondVariantInputs[0].value);
}
}
// Function to attach event listeners to first variant
function attachFirstVariantListeners() {
const firstVariantInputs = findInputsByNameContaining(FIRST_OPTION_IDENTIFIER);
firstVariantInputs.forEach(input => {
// Remove existing listener if any (to prevent duplicates)
input.removeEventListener('change', handleFirstVariantChange);
// Add the listener
input.addEventListener('change', handleFirstVariantChange);
});
console.log(`Attached listeners to ${firstVariantInputs.length} first variant inputs`);
}
// Event handler for first variant changes
function handleFirstVariantChange(event) {
console.log('First variant changed to:', event.target.value);
// Small delay to ensure DOM updates are complete
setTimeout(() => {
resetSecondVariant();
}, 50);
}
// Main initialization function
function init() {
attachFirstVariantListeners();
}
// Observer to watch for DOM changes (when Dawn re-renders variants)
function setupDOMObserver() {
const observer = new MutationObserver(function(mutations) {
let shouldReinit = false;
mutations.forEach(function(mutation) {
// Check if radio inputs were added/removed
if (mutation.type === 'childList') {
const addedNodes = Array.from(mutation.addedNodes);
const removedNodes = Array.from(mutation.removedNodes);
// Check if any radio inputs were affected
const hasRadioChanges = [...addedNodes, ...removedNodes].some(node => {
if (node.nodeType === 1) { // Element node
return node.tagName === 'INPUT' && node.type === 'radio' ||
node.querySelector && node.querySelector('input[type="radio"]');
}
return false;
});
if (hasRadioChanges) {
shouldReinit = true;
}
}
});
if (shouldReinit) {
console.log('DOM changed, reinitializing variant listeners...');
setTimeout(init, 100); // Small delay to ensure DOM is stable
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
return observer;
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Set up the DOM observer to handle dynamic changes
setupDOMObserver();
// Also listen for Dawn's specific variant change events if they exist
document.addEventListener('variant:change', function() {
console.log('Dawn variant:change event detected, reinitializing...');
setTimeout(init, 100);
});
// Alternative: listen for any form changes in the product form
const productForms = document.querySelectorAll('form[action*="/cart/add"]');
productForms.forEach(form => {
form.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
setTimeout(init, 50);
}
});
});
})();
What the script does:
- Works around those annoying line breaks in the name attributes by using includes() instead of exact matching
- Automatically reattaches event listeners when Dawn re-renders the variant selectors (which happens on every change)
- Has multiple fallback strategies to catch DOM changes
- Properly triggers Shopify’s variant system after resetting the second option
How to implement it:
-
Save the attached JavaScript code as variant-reset.js in your theme’s assets folder
-
Add this line to your product template (probably in sections/product-form.liquid):
{{ 'variant-reset.js' | asset_url | script_tag }}
-
Update the FIRST_OPTION_IDENTIFIER and SECOND_OPTION_IDENTIFIER variables at the top of the script to match your actual option names (without the line breaks)
Key features that solve your specific problems:
- Uses a MutationObserver to detect when Dawn changes the DOM and automatically reinitializes
- Handles the line break issue by checking if the name contains your identifier rather than exact matching
- Includes console.log statements so you can see what’s happening in the browser dev tools
- Has multiple event listeners as fallbacks in case one method doesn’t work
The script will automatically find your radio inputs even with those weird name attributes and keep working even when the DOM gets re-rendered. Let me know how it works for you! If you run into any issues or need to customize it further (like if your option names are different), just reply and I can help adjust it.
Also, if you open your browser’s developer console, you should see some helpful log messages that show when the script is working - that might help with debugging if needed.
Hope this helps solve your UX issue!
Best,
Shubham | Untechnickle
P.S. - That line break issue in Dawn’s generated HTML is definitely annoying. You’re not the first person to get tripped up by it!
Thank you so much, Shubham! I’ve been stuck on this for two whole days. For
the first option, the first format exists and is correctly selected by
default. But for the other options, nothing is selected because the first
format doesn’t exist anymore—it was deleted. Could you help me make it so
that the default selected format is always the first available one, no
matter which option is chosen?
1 Like
I’m super stoked to help @Lucie06 .
I got where you’re coming from, please replace the code with the one below:
// Dawn 15.0.3 Variant Reset Script - Updated Version
// This script resets the second variant to the first AVAILABLE option when the first one changes
(function() {
'use strict';
// Configuration - adjust these if needed
const FIRST_OPTION_IDENTIFIER = 'Option-1'; // without the line break
const SECOND_OPTION_IDENTIFIER = 'Format-2'; // without the line break
// Function to find inputs by name containing the identifier
function findInputsByNameContaining(identifier) {
return Array.from(document.querySelectorAll('input[type="radio"]')).filter(input => {
// Handle the line break issue by checking if name contains our identifier
return input.name && input.name.trim().includes(identifier);
});
}
// Function to check if a radio input is available/selectable
function isInputAvailable(input) {
// Check if the input is disabled
if (input.disabled) {
return false;
}
// Check if the input is hidden or its parent is hidden
const style = window.getComputedStyle(input);
if (style.display === 'none' || style.visibility === 'hidden') {
return false;
}
// Check if parent elements are hidden
let parent = input.parentElement;
while (parent && parent !== document.body) {
const parentStyle = window.getComputedStyle(parent);
if (parentStyle.display === 'none' || parentStyle.visibility === 'hidden') {
return false;
}
parent = parent.parentElement;
}
// Check for Shopify/Dawn specific disabled classes
if (input.classList.contains('disabled') ||
input.closest('.disabled') ||
input.closest('[disabled]') ||
input.closest('.variant-input--unavailable')) {
return false;
}
return true;
}
// Function to reset second variant to first available option
function resetSecondVariant() {
const secondVariantInputs = findInputsByNameContaining(SECOND_OPTION_IDENTIFIER);
if (secondVariantInputs.length > 0) {
// First, uncheck all second variant options
secondVariantInputs.forEach(input => input.checked = false);
// Find the first available option
const firstAvailableInput = secondVariantInputs.find(input => isInputAvailable(input));
if (firstAvailableInput) {
// Check the first available option
firstAvailableInput.checked = true;
// Trigger change event to update Shopify's variant system
firstAvailableInput.dispatchEvent(new Event('change', { bubbles: true }));
console.log('Reset second variant to first available option:', firstAvailableInput.value);
} else {
console.log('No available options found for second variant');
}
}
}
// Function to attach event listeners to first variant
function attachFirstVariantListeners() {
const firstVariantInputs = findInputsByNameContaining(FIRST_OPTION_IDENTIFIER);
firstVariantInputs.forEach(input => {
// Remove existing listener if any (to prevent duplicates)
input.removeEventListener('change', handleFirstVariantChange);
// Add the listener
input.addEventListener('change', handleFirstVariantChange);
});
console.log(`Attached listeners to ${firstVariantInputs.length} first variant inputs`);
}
// Event handler for first variant changes
function handleFirstVariantChange(event) {
console.log('First variant changed to:', event.target.value);
// Longer delay to ensure Shopify has finished updating variant availability
setTimeout(() => {
resetSecondVariant();
}, 150);
}
// Main initialization function
function init() {
attachFirstVariantListeners();
}
// Observer to watch for DOM changes (when Dawn re-renders variants)
function setupDOMObserver() {
const observer = new MutationObserver(function(mutations) {
let shouldReinit = false;
mutations.forEach(function(mutation) {
// Check if radio inputs were added/removed or attributes changed
if (mutation.type === 'childList' || mutation.type === 'attributes') {
const addedNodes = Array.from(mutation.addedNodes || []);
const removedNodes = Array.from(mutation.removedNodes || []);
// Check if any radio inputs were affected
const hasRadioChanges = [...addedNodes, ...removedNodes].some(node => {
if (node.nodeType === 1) { // Element node
return node.tagName === 'INPUT' && node.type === 'radio' ||
node.querySelector && node.querySelector('input[type="radio"]');
}
return false;
});
// Also check if disabled/class attributes changed on inputs
if (mutation.type === 'attributes' &&
mutation.target.tagName === 'INPUT' &&
mutation.target.type === 'radio') {
hasRadioChanges = true;
}
if (hasRadioChanges) {
shouldReinit = true;
}
}
});
if (shouldReinit) {
console.log('DOM changed, reinitializing variant listeners...');
setTimeout(init, 200); // Slightly longer delay to ensure DOM is stable
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['disabled', 'class', 'style'] // Watch for these attribute changes
});
return observer;
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Set up the DOM observer to handle dynamic changes
setupDOMObserver();
// Also listen for Dawn's specific variant change events if they exist
document.addEventListener('variant:change', function() {
console.log('Dawn variant:change event detected, reinitializing...');
setTimeout(init, 200);
});
// Listen for Shopify's variant change events
document.addEventListener('variant:changed', function() {
console.log('Shopify variant:changed event detected, reinitializing...');
setTimeout(init, 200);
});
// Alternative: listen for any form changes in the product form
const productForms = document.querySelectorAll('form[action*="/cart/add"]');
productForms.forEach(form => {
form.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
setTimeout(init, 100);
}
});
});
})();
Let me know if you come across any problems, always here to help! Also, you can mail us in the future at hello@untechnickle.com - you can send a ‘hi’ right now, for any queries - would love to help.
Cheers!
Shubham | Untechnickle
Thank you so much for your valuable help! Unfortunately, this second code
doesn’t work every time. Actually, the first version worked better, even
though it didn’t select the very first available option.
Hey @Lucie06 ,
Apologies for the second solution not working. I’ve tweaked the code a bit, try this please:
(function() {
'use strict';
// Configuration - adjust these if needed
const FIRST_OPTION_IDENTIFIER = 'Option-1'; // without the line break
const SECOND_OPTION_IDENTIFIER = 'Format-2'; // without the line break
// Function to find inputs by name containing the identifier
function findInputsByNameContaining(identifier) {
return Array.from(document.querySelectorAll('input[type="radio"]')).filter(input => {
// Handle the line break issue by checking if name contains our identifier
return input.name && input.name.trim().includes(identifier);
});
}
// Simple function to check if input is available (minimal checks only)
function isInputSelectable(input) {
return !input.disabled && !input.closest('.disabled');
}
// Function to reset second variant to first available option
function resetSecondVariant() {
const secondVariantInputs = findInputsByNameContaining(SECOND_OPTION_IDENTIFIER);
if (secondVariantInputs.length > 0) {
// First, uncheck all second variant options
secondVariantInputs.forEach(input => input.checked = false);
// Try to find first available option, fallback to first option
let targetInput = secondVariantInputs.find(input => isInputSelectable(input));
// If no available option found, just use the first one (original behavior)
if (!targetInput) {
targetInput = secondVariantInputs[0];
}
// Check the target option
targetInput.checked = true;
// Trigger change event to update Shopify's variant system
targetInput.dispatchEvent(new Event('change', { bubbles: true }));
console.log('Reset second variant to:', targetInput.value);
}
}
// Function to attach event listeners to first variant
function attachFirstVariantListeners() {
const firstVariantInputs = findInputsByNameContaining(FIRST_OPTION_IDENTIFIER);
firstVariantInputs.forEach(input => {
// Remove existing listener if any (to prevent duplicates)
input.removeEventListener('change', handleFirstVariantChange);
// Add the listener
input.addEventListener('change', handleFirstVariantChange);
});
console.log(`Attached listeners to ${firstVariantInputs.length} first variant inputs`);
}
// Event handler for first variant changes
function handleFirstVariantChange(event) {
console.log('First variant changed to:', event.target.value);
// Small delay to ensure DOM updates are complete
setTimeout(() => {
resetSecondVariant();
}, 50);
}
// Main initialization function
function init() {
attachFirstVariantListeners();
}
// Observer to watch for DOM changes (when Dawn re-renders variants)
function setupDOMObserver() {
const observer = new MutationObserver(function(mutations) {
let shouldReinit = false;
mutations.forEach(function(mutation) {
// Check if radio inputs were added/removed
if (mutation.type === 'childList') {
const addedNodes = Array.from(mutation.addedNodes);
const removedNodes = Array.from(mutation.removedNodes);
// Check if any radio inputs were affected
const hasRadioChanges = [...addedNodes, ...removedNodes].some(node => {
if (node.nodeType === 1) { // Element node
return node.tagName === 'INPUT' && node.type === 'radio' ||
node.querySelector && node.querySelector('input[type="radio"]');
}
return false;
});
if (hasRadioChanges) {
shouldReinit = true;
}
}
});
if (shouldReinit) {
console.log('DOM changed, reinitializing variant listeners...');
setTimeout(init, 100); // Small delay to ensure DOM is stable
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
return observer;
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Set up the DOM observer to handle dynamic changes
setupDOMObserver();
// Also listen for Dawn's specific variant change events if they exist
document.addEventListener('variant:change', function() {
console.log('Dawn variant:change event detected, reinitializing...');
setTimeout(init, 100);
});
// Alternative: listen for any form changes in the product form
const productForms = document.querySelectorAll('form[action*="/cart/add"]');
productForms.forEach(form => {
form.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
setTimeout(init, 50);
}
});
});
})();
If it’s still causing the issue, I’d request you to DM or email us your collaborator code so that we can dig deep into your store and check what’s missing.
Thanks,
Shubham | Untechnickle