If you're working with HubSpot CMS, custom modules are the single most important skill you can learn. They give content editors full control over page content while keeping the code clean, fast, and maintainable.
But here's the problem — most HubSpot developers build modules the wrong way. They write bloated CSS, skip field grouping, hardcode heading tags, and ignore performance completely. The result? Slow pages, frustrated editors, and messy code that nobody wants to maintain.
This guide covers everything you need to build production-ready custom modules in HubSpot — the right way. With real code examples, proper field structure, HubL best practices, and performance tips that actually matter.
A custom module is a reusable content block made up of four files inside the Design Manager:
Think of it like building a Lego brick. You design it once with all the right connection points, and then content editors can use it anywhere — on any page, in any section — without ever touching code.
Before building anything, configure your module properly. Here's a clean meta.json for a Hero Section module:
Two things to note here. First, host_template_types controls where this module can be used. Set it to PAGE for page-only modules, or include BLOG_POST if it should work in blog templates too. Never leave it open for email templates if your module uses CSS or JS — emails don't support them. Second, always add meaningful help_text. This shows up in the Design Manager and helps your team understand what the module does without reading the code.
The fields.json file is where most developers go wrong. A flat list of 15+ fields makes the editing experience painful. Grouping related fields together creates a clean, intuitive interface.
Before any content fields, every module should have a Section Settings group. This gives content editors control over the section's ID, custom classes, and visibility:
Why is this important? Content editors often need to hide a section temporarily, add an anchor link for navigation, or apply a custom class for special styling. Without these fields, they have to ask a developer for every small change. With them, they're self-sufficient.
Never hardcode your heading tag. Always provide a choice field so editors can control the SEO hierarchy:
Notice the default heading tag is set to h2, not h1. This is intentional. Most pages should only have one H1, and that's usually handled by the first module. Every module after that should default to H2. If the editor places this module as the first section on a page, they can switch it to H1 themselves.
A common mistake is using HubSpot's CTA module for every button. CTAs are powerful but they add tracking overhead and slow down page loading when overused. For most buttons, a simple link field with a text field is all you need:
The link field type automatically gives editors options for URL, new tab, and nofollow — all without any extra work from you.
How you name fields matters more than you think. Content editors see these labels every time they edit a page. Be specific:
Also add help_text to every field. A 10-word description now saves a 10-minute Slack conversation later.
Now let's build the actual module.html. This is where all the fields come together into a rendered component.
Let's break down the critical patterns used in this code.
Every HubL tag in the code above uses the dash syntax: instead of . This single change makes a massive difference in your rendered HTML.
Without dashes, HubL inserts blank lines wherever it processes a tag. Your page source ends up full of unnecessary whitespace — empty lines between every element. This makes the HTML heavier, harder to debug, and slightly slower to parse.
With dashes, HubL strips that whitespace cleanly. The rendered HTML is tight, minimal, and exactly what the browser needs. On a page with 20+ modules, this can reduce your HTML file size noticeably.
The same applies to variable output: use instead of to prevent whitespace around dynamic values.
The heading uses the field value directly as the HTML tag:
If the editor selects "H2" from the dropdown, this renders as a proper <h2> tag. If they select "H1", it renders as <h1>. The editor has full SEO control without needing a developer.
Every section of content is wrapped in an if check. If the heading field is empty, no <h2> tag renders. If the button text is empty, no button appears. If the image has no source, the image column disappears entirely.
This prevents empty HTML elements from appearing on the page — no broken layouts, no empty divs taking up space, no accessibility issues from empty heading tags.
When using HubSpot's image field type, always output the field values directly — src, alt, width, and height. HubSpot's image field is already optimized for SEO. It provides responsive image attributes and proper alt text handling out of the box.
Do not override these values with hardcoded attributes. Just add loading="lazy" for below-the-fold images to improve page performance.
If every module needs section settings (ID, class, visibility), you shouldn't be copying that code into every module.html. Instead, create a macro file once and import it everywhere.
Write it once. Import it in 50 modules. When you need to change how section IDs work, update one file and every module gets the update. This is how professional HubSpot themes are built.
This might be the most important section in this entire guide. Most HubSpot developers write CSS inside every module's module.css file. This creates a mess — duplicate styles across modules, inconsistent spacing, growing file sizes, and pages that load slower with every new module.
Look at the module.html above. There is zero custom CSS. Every style is applied through Tailwind utility classes directly in the HTML:
Benefits of this approach:
The only time to write module.css is for things Tailwind genuinely can't handle — typically custom animations or complex pseudo-element patterns:
Everything else should be in your global theme CSS or handled by Tailwind utilities. Period.
The same rule applies to module.js. Most modules don't need JavaScript at all.
Modules that do not need JS: hero sections, text and image blocks, feature grids, testimonial cards, pricing tables, footer sections. These are purely visual — HTML and CSS handle everything.
Modules that do need JS: accordions, carousels/sliders, tab components, modal popups, form validation, scroll-triggered animations.
When you do write JavaScript, keep it vanilla. No jQuery in 2026. HubSpot loads module.js only once per module type even if the module appears multiple times on a page — but it still adds to your page weight. Be intentional about every script you include.
Google considers accessibility signals in search rankings. Building accessible modules isn't just good practice — it directly impacts your SEO.
If you're using Tailwind CSS, you already have an advantage. Tailwind's default color palette is designed with proper contrast ratios. Stick to these safe pairings:
Avoid light text on light backgrounds. A contrast ratio below 4.5:1 fails WCAG AA and can hurt your search rankings.
Before publishing any module, run through this checklist:
Writing CSS in every module. This creates duplicated styles and heavier page loads. Use Tailwind utility classes or write global CSS in your theme stylesheet. Module CSS should be your last resort.
Adding JavaScript unnecessarily. Static content modules like hero sections, feature grids, and testimonials don't need JavaScript. Every script adds to page weight.
Hardcoding heading tags. If you write <h2> directly in your HTML, content editors can't control the SEO hierarchy. Always use a choice field and render the tag dynamically.
Ignoring HubL whitespace. Using instead offills your rendered HTML with blank lines. The dash syntax strips whitespace and produces cleaner, lighter output.
Flat field lists without groups. Twenty ungrouped fields in the page editor is a nightmare for content editors. Group related fields — Content, Image, Button, Section Settings — so the interface is clean and navigable.
Skipping section settings. Without an ID field, editors can't create anchor links. Without a visibility toggle, they can't hide a section temporarily. Without a custom class field, they need a developer for every styling adjustment. Add these three fields to every module as a standard practice.
Overriding HubSpot's image snippet. HubSpot's image field type already generates SEO-optimized output with proper src, alt, width, height, and responsive attributes. Don't replace this with custom code — just output the field values directly and add loading="lazy" when appropriate.
Using CTAs for every button. HubSpot CTAs have tracking overhead. For simple buttons that link to a page, use an anchor tag with a text field and link field instead. Reserve CTAs for buttons that genuinely need tracking and analytics.
Building custom modules in HubSpot is not just about making things look right — it's about building components that are fast, accessible, SEO-friendly, and easy for content editors to use.
The techniques covered in this guide — grouped fields, dynamic heading tags, HubL whitespace control, Tailwind utility classes, reusable macros, and proper image handling — are what separate professional HubSpot development from amateur work.
Start applying these practices to your next module. Your content editors will thank you, your pages will load faster, and your SEO scores will improve.
Building HubSpot modules from scratch takes time. Rapid lets you upload any UI screenshot and generates a complete, production-ready HubSpot module — with fields.json, HubL bindings, and proper structure — in under 60 seconds. Try it free →