Skip to content
A vibrant, colorful featured banner image with rich gradients, modern abstract shapes, and soft lighting. Clean composition with smooth curves, subtle depth, and a visually appealing layout.
King DJ24 November 20254 min read

Building a clean HubSpot React Header & Footer — what we did and why it works

This guide details the architecture for creating high-performance HubSpot React modules. We will cover how to structure your directory for the HubSpot CMS, resolve common Vite build errors, and implement best practices for exposing fields to the content editor.

If you are migrating from legacy templates or building a fresh theme, following this "Module-as-Directory" pattern is essential for passing HubSpot's production build checks.

The Goal

Our objective is to ship well-structured, maintainable React modules that:

  • Adhere to the Module-as-Directory architecture required by the HubSpot CLI.
  • Expose HubSpot-authorable fields (images, text, menus) via the Design Manager.
  • Render correctly inside standard HubL templates.
  • Include a fallback menu and a dynamic Language Switcher using pageContext.
  • Ship with theme-safe CSS and resolve common EISDIR build errors.

 

The Module Directory Tree

The HubSpot React runtime (powered by Vite) expects modules to be exported from a specific folder structure. Below is the exact file tree required to prevent build errors like "Failed to load URL".

components/
  modules/
    Header/
      ├── index.jsx            // Entry point (Exports Component, fields, meta)
      ├── HeaderComponent.jsx  // Main UI Logic
      ├── HeaderFields.jsx     // Editor Fields Configuration
      ├── HeaderMeta.js        // Module metadata (Label, Icon, Boolean options)
      ├── Menu.jsx             // Sub-component for Navigation
      ├── MenuFields.jsx       // Sub-fields for Navigation
      ├── LanguageSwitcher.jsx // Logic for multi-language
      └── style/
          └── header.css       // Scoped CSS (Must match module directory name)

 

Key Module Files Explained

1. The Entry Point (index.jsx)

This file is mandatory. It aggregates your exports so HubSpot knows where to find the component logic, the field definition, and the metadata.

// index.jsx
import Component from "./HeaderComponent";
import fields from "./HeaderFields";
import meta from "./HeaderMeta";

export { Component, fields, meta };
 
2. Field Configuration (HeaderFields.jsx)

This file defines what content editors see in the sidebar. We import standard fields from the @hubspot/cms-components library.

// HeaderFields.jsx
import { ModuleFields, TextField, ImageField } from "@hubspot/cms-components/fields";
import MenuFields from "./MenuFields";
import logo from "../../../assets/sprocket.svg";

const fields = (
  <ModuleFields>
    <ImageField 
      name="logo" 
      label="Logo" 
      default= 
      resizable 
    />

    <TextField 
      name="headerTitle" 
      label="Header Title" 
      default="React Header" 
    />

    {/* We import the menu fields from a separate file to keep this clean */}
    {MenuFields}

    {/* Note: We do NOT use a field for languages. That comes from pageContext. */}
  </ModuleFields>
);

export default fields;
 
3. The UI Component (HeaderComponent.jsx)

This is where the React magic happens. Note that we retrieve languages from pageContext, not from the module fields.

// HeaderComponent.jsx
import "./style/header.css";
import Menu from "./Menu";
import LanguageSwitcher from "./LanguageSwitcher";

export default function HeaderComponent({ fieldValues, pageContext }) {
  const { logo, headerTitle, navigation } = fieldValues;
  
  // Robust check for languages provided by the CMS runtime
  const languages = pageContext?.languages || [];

  return (
    <header className="header-module">
      <div className="header-inner">
        <img 
          src={logo.src} 
          alt={logo.alt} 
          width={logo.width} 
          height={logo.height} 
          className="header-logo" 
        />

        <h1 className="header-title">{headerTitle}</h1>

        <Menu items={navigation} />

        <LanguageSwitcher languages={languages} />
      </div>
    </header>
  );
}
 
4. Navigation with Fallback (Menu.jsx)

To improve the Developer Experience (DX), we add a fallback. This ensures the header looks good in local development even before a user selects a real menu in the CMS.

// Menu.jsx
const sampleMenu = [
  { label: "Home", url: "/" },
  { label: "About Us", url: "/about" },
  { label: "Services", url: "/services" }
];

export default function Menu({ items }) {
  // If the user hasn't selected a menu yet, use the sample
  const finalItems = Array.isArray(items) && items.length > 0 ? items : sampleMenu;

  return (
    <nav className="header-nav">
      <ul>
        {finalItems.map((item, idx) => (
          <li key={idx}><a href={item.url}>{item.label}</a></li>
        ))}
      </ul>
    </nav>
  );
}

 

CSS and the HubSpot Style Folder Rule

HubSpot React modules have a strict convention for CSS auto-loading. You cannot simply import a CSS file from anywhere.

  • Rule 1: Create a style/ folder inside your module directory.
  • Rule 2: Name the CSS file exactly the same as your module directory (e.g., style/header.css for a "Header" module).
  • Rule 3: Import it at the top of your component file.
/* components/modules/Header/style/header.css */
.header-module { padding: 20px; background: #fff; }
.header-inner { display: flex; align-items:center; justify-content:space-between; }
.header-nav ul { display:flex; gap: 20px; list-style:none; }

 

Troubleshooting: Common Build Errors

If your local dev server or the hs project upload command fails, check these common issues.

Error Message Root Cause The Fix
EISDIR: illegal operation on a directory Your HubL template points to a parent folder (e.g., modules/global) instead of the specific module folder. Update the path in your HubL to point explicitly to the module folder: path="../components/modules/Header".
Failed to load url … resolved id: header.jsx Vite is confused because an old file named header.jsx exists next to your new Header/ folder. Delete the legacy single-file module so the folder-based module is the only source of truth.
Element type is invalid You are trying to import a field component that doesn't exist (e.g., LanguageSelectorField). Remove invalid imports. Use pageContext for languages. Only import documented fields from @hubspot/cms-components/fields.
LinkField invalid default The build system rejects LinkField without a valid default value. Explicitly set default={null} on all optional LinkFields.

How to Call the React Module from HubL

Once your folder structure is correct, you can reference the module in your base.hubl.html or any drag-and-drop area. We use the raw tag to ensure HubSpot doesn't try to execute this example code immediately.

Standard Module Call:

{% module "header" path="../../components/modules/Header" %}

Editable Widget Block:

{% widget_block "header_block" type="react_component" path="../../components/modules/Header" %}
  {# Child content can go here #}
{% end_widget_block %}

 

Critical: Footer & LinkFields

When building a Footer with social media icons, you will use the LinkField. The HubSpot cloud builder is strict about defaults. If a link is optional, you must set the default to null, otherwise the upload will fail.

<LinkField name="facebook" label="Facebook URL" default={null} />

 

Final Checklist

Before you commit your changes, verify the following:

  • Module lives in components/modules/YourModuleName.
  • index.jsx exports { Component, fields, meta }.
  • Styles are located at components/modules/YourModuleName/style/yourmodulename.css.
  • No duplicate/legacy files exist in the parent directory.
  • All optional LinkField props are set to default={null}.
  • Language switcher logic relies on pageContext.languages.
avatar

King DJ

Building custom HubSpot CMS themes, HUBL modules, smart forms, and dynamic quote templates—optimized for speed, scalability, and CRM integration.

RELATED ARTICLES