Skip to content

Instantly share code, notes, and snippets.

@derekcavaliero
Last active June 1, 2025 15:40
Show Gist options
  • Select an option

  • Save derekcavaliero/1d16bc7f02357087e179aa19f8c7ce08 to your computer and use it in GitHub Desktop.

Select an option

Save derekcavaliero/1d16bc7f02357087e179aa19f8c7ce08 to your computer and use it in GitHub Desktop.
HubSpot Embedded Form Accessibility Polyfills
/**
* HubSpot Embedded Form Accessibility Pollyfills
*
* This script fixes the HubSpot HTML blunders that make their embedded forms inaccessible for assistive technology.
* - Replaces/removes improper use of <fieldset>, <legend>, <label>, and role attributes.
* - Note - this can cause forms configured for mulitple column field layouts to break - you will need to adjust your CSS accordingly.
**/
hubspotFormA11y = {
changeTag: function(node, tag) {
const clone = document.createElement(tag);
for (const attr of node.attributes) {
clone.setAttributeNS(null, attr.name, attr.value);
}
while (node.firstChild) {
clone.appendChild(node.firstChild);
}
node.replaceWith(clone);
return clone;
},
polyfillInitialHtml: function(form) {
form.querySelectorAll('.hs-form-field').forEach(field => {
let input = field.querySelector('.hs-input');
let errorTarget = document.createElement('div');
errorTarget.id = input.name + '-error-target';
errorTarget.className = 'hs-error-target';
errorTarget.setAttribute('aria-live', 'polite');
input.setAttribute('aria-describedby', errorTarget.id);
input.removeAttribute('placeholder');
field.append(errorTarget);
});
form.querySelectorAll('.hs-form-required').forEach(element => {
element.setAttribute('aria-hidden', true);
});
form.querySelectorAll('fieldset, legend').forEach(element => {
this.changeTag(element, 'div');
});
form.querySelectorAll('.hs-fieldtype-checkbox, .hs-fieldtype-radio').forEach(element => {
element.querySelectorAll('[role]').forEach(item => {
item.removeAttribute('role');
});
this.changeTag(element, 'fieldset');
});
form.querySelectorAll('.hs-fieldtype-checkbox > label, .hs-fieldtype-radio > label').forEach(element => {
this.changeTag(element, 'legend');
});
},
polyfillErrorMessages: function(fieldRoot) {
if (!fieldRoot.matches('.hs-form-field'))
return;
let errorTarget = fieldRoot.querySelector('.hs-error-target');
let errorRoot = fieldRoot.querySelector('.hs-error-msgs');
errorRoot.removeAttribute('role');
let input = fieldRoot.querySelector('.hs-input.invalid');
let errorClone = errorRoot.cloneNode(true);
errorClone.removeAttribute('class');
let errors = errorClone.querySelectorAll('.hs-error-msg');
errors.forEach(error => {
let label = fieldRoot.querySelector('label[for="' + input.id + '"]');
if (error.innerText == 'Please complete this required field.')
error.innerText = 'The "' + label.innerText.replace('*', '') + '" field is required.';
if (errors.length === 1) {
console.log(error.closest('li'));
}
this.changeTag(error, 'span');
});
errorRoot.style.display = 'none';
errorRoot.setAttribute('aria-hidden', true);
errorTarget.replaceChildren(errorClone);
},
untetherErrorMessages: function(fieldRoot) {
if (!fieldRoot.matches('.hs-form-field'))
return;
let errorTarget = fieldRoot.querySelector('.hs-error-target');
if (!errorTarget)
return;
errorTarget.replaceChildren();
},
observer: function(form) {
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type !== 'childList')
return;
if (mutation.addedNodes.length) {
let node = mutation.addedNodes[0];
if (node.nodeType === 3) // text node early return
return;
//console.log('Node(s) added', mutation.addedNodes, mutation.target);
if (node.matches('.hs-error-msgs'))
this.polyfillErrorMessages(mutation.target);
}
if (mutation.removedNodes.length) {
let node = mutation.removedNodes[0];
if (node.nodeType === 3) // text node early return
return;
//console.log('Node(s) removed', mutation.removedNodes, mutation.target);
if (node.matches('.hs-error-msgs'))
this.untetherErrorMessages(mutation.target);
}
}
});
observer.observe(form, {
attributes: false,
childList: true,
subtree: true
});
}
};
window.addEventListener('message', msg => {
if (msg.data.type !== 'hsFormCallback')
return;
const formId = msg.data.id;
const form = document.querySelector('form[data-form-id="' + formId + '"]');
switch(msg.data.eventName) {
case 'onFormReady':
hubspotFormA11y.polyfillInitialHtml(form);
hubspotFormA11y.observer(form);
break;
case 'onFormFailedValidation':
form.querySelector('.hs-input.invalid').focus();
break;
case 'onFormSubmit':
break;
case 'onFormSubmitted':
break;
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment