[Shopify Dawn 15.0.3] Reset Second Variant (Radio) When Changing First Variant (Radio)

Topic summary

A Shopify store owner using Dawn 15.0.3 theme needs to reset the second variant (Format) radio button group whenever the first variant (Version) changes, to prevent invalid variant combinations.

Technical Challenge:

  • Both variants use radio buttons with auto-generated name attributes containing line breaks (e.g., Option-1\n, Format-2\n)
  • Dawn re-renders the DOM on variant changes, causing event listeners to be lost
  • Standard JavaScript selectors fail due to the unusual name attribute formatting

Proposed Solution:
A community member (TheUntechnickle) provided a custom JavaScript solution featuring:

  • Name matching that handles line breaks by checking if the attribute contains the identifier
  • MutationObserver to detect DOM changes and reinitialize listeners
  • Multiple event listeners as fallbacks for reliability

Ongoing Issue:
After three iterations of the code, the solution remains partially functional. The original poster reports:

  • First version worked better overall but didn’t always select the first available option
  • Second version (with availability checking) worked inconsistently
  • Third simplified version still has issues

Current Status:
The discussion remains open. TheUntechnickle has offered to access the store directly via collaborator code to diagnose the problem more thoroughly.

Summarized with AI on October 27. AI used: claude-sonnet-4-5-20250929.

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:

  1. Save the attached JavaScript code as variant-reset.js in your theme’s assets folder

  2. Add this line to your product template (probably in sections/product-form.liquid):

    {{ 'variant-reset.js' | asset_url | script_tag }}
    
  3. 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