Get the complete solution on GitHub: Custom HubSpot Styles Plugin
When dealing with embedded forms in web projects, we often encounter a frustrating limitation – default styles that seem impossible to override, even with !important CSS declarations. This becomes particularly challenging when working with forms embedded through iframes, which create a separate context for styles and make traditional CSS approaches ineffective.
In this case, we needed to style basic form embeds but had limited access to premium customization features. Rather than fight against iframe restrictions and style precedence rules, we developed a solution that completely transforms how embedded forms can be styled – breaking through cross-origin limitations and gaining full control over the form’s appearance.
This observer pattern approach gives us granular control over form styling while maintaining clean, maintainable code that scales across multiple form instances.
The Challenge
HubSpot forms are embedded through iframes, which makes them notoriously difficult to style. The traditional approach of adding CSS rules, even with !important declarations, often falls short because:
- Styles are loaded within the iframe context
- HubSpot’s own styles take precedence
- The iframe creates a separate document context
Before & After Transformation
Default HubSpot Form

Default form with standard HubSpot styling and branding
Styled HubSpot Form

The same form after applying our custom styling solution – clean, branded, and professional
The Solution: Observer Pattern Implementation
Let’s break down our solution into key components that make this work:
1. Core Class Structure
First, we create our main class that will handle all the form styling. The key here is that we’re setting up a system that can handle multiple forms and maintain consistent styling:
class HubSpotFormStyler {
constructor() {
this.styledFormsCount = 0;
this.maxForms = 5;
this.options = window.hfsOptions || {
buttonColor: '#007ddf',
fontFamily: 'Eurostile, Inter, arial, helvetica, sans-serif',
textSize: '12px'
};
this.init();
}
// ... rest of the class
}
The constructor is crucial because it sets up our configuration options and initializes the form tracking system. We limit the number of forms to prevent any potential infinite loops.
2. Form Detection System
Here’s where things get interesting. Instead of relying on CSS, we actively watch for forms being loaded into the page:
setupEventListeners() {
window.addEventListener('load', () => this.customizeHubSpotForm());
window.addEventListener('message', (event) => {
if (event.data?.type === 'hsFormCallback') {
this.customizeHubSpotForm();
}
});
}
This code is the heart of our observer pattern. It listens for both the initial page load and HubSpot’s own form callbacks. This ensures we catch forms whether they’re loaded immediately or injected later.
3. Breaking Through the iframe Barrier
The magic happens when we access the iframe’s content and apply our styles directly. This bypasses the normal CSS cascade and !important limitations:
styleFormElements(iframeDoc, iframe) {
const submitButton = iframeDoc.querySelector('.hs-button');
if (!submitButton) return false;
this.styleButton(submitButton);
this.styleRichText(iframeDoc);
this.styleTextElements(iframeDoc);
this.addCustomFont(iframeDoc);
this.hideViralityLink(iframeDoc);
iframe.setAttribute('data-styled', 'true');
this.styledFormsCount++;
return true;
}
This approach is powerful because we’re not fighting against HubSpot’s styles – we’re applying ours directly in the same context as theirs.
4. Custom Font Integration
One of the trickiest parts of form styling is getting custom fonts to work in iframes. Here’s how we solve it:
addCustomFont(doc) {
if (!doc.querySelector('link[href*="Inter"]')) {
const fontLink = doc.createElement('link');
fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap';
fontLink.rel = 'stylesheet';
doc.head.appendChild(fontLink);
}
}
By injecting the font link directly into the iframe’s document, we ensure the font is available for our styles.
5. Usage Example
Implementation is straightforward – just add your configuration and initialize the styler:
window.hfsOptions = {
buttonColor: '#3182ce',
fontFamily: 'Inter, system-ui, sans-serif',
textSize: '14px'
};
document.addEventListener('DOMContentLoaded', () => {
new HubSpotFormStyler();
});
How It All Works Together
The beauty of this solution lies in its approach to the problem. Instead of fighting against HubSpot’s iframe encapsulation, we work with it. The observer pattern allows us to:
- Detect when forms are loaded into the page
- Access the iframe’s document context safely
- Apply our styles with the same priority as HubSpot’s own styles
- Maintain consistent styling across form reloads
- Remove unwanted branding elements automatically
Conclusion
This solution provides a robust way to style HubSpot forms while maintaining clean code practices. By leveraging the Observer pattern and direct iframe manipulation, we achieve what CSS alone cannot – complete control over form styling within iframes.
Looking for the complete solution? Check out our GitHub repository for the full code and documentation.