Help Creating a Custom Flexible Columns Module (CMS)

Hi everyone,

I’m looking for help creating a custom HubSpot module that allows more flexible content layouts within our website pages.

What we need:

A reusable custom module that allows editors to:

  • Choose the number of columns (e.g., 1–4 columns)

  • Add repeatable content blocks

  • Insert rich text and/or images inside each column

  • Optionally include buttons

  • Control basic styling such as:

    • Text alignment
    • Background color
    • Spacing (padding/margins)

Goal:
Just a more flexible module that will allow me to stack individual items like text fields or images within each column instead of hard coded custom modules with limited flexibility.

I would greatly apreciate any advice!

Hey @JMackenzie8,

you can do this with a so-called repeater function.

  1. create a new custom module
  2. Add an image field, (rich-)text to it
  3. Click on Actions/Group and select those two fields

  1. Enable the repeater options in this group (not the individual fields)

  1. Add a boolean field (add button) to this group

  2. Add a Link field(not URL) and a text field to this group and create a group again, but add the Link field and text field to it

  3. Set the visibility option of this group to “if add button is true”

  4. Now click on the left icon in the breadcrumb navigation to navigate back to the whole module settings

You should see something like this:

  1. Click on “Actions” in the Column and select “Copy Snippet”

  1. Paste the snippet in the module.html area

  1. Technically you’re half way there
  2. While this gives you quite a lot, you’ll most likely need to style it. To do so, I recommend to modify the whole thing to something like this:
<div class="card-wrapper">
{%- for item in module.column -%}
	<div class="card-image">
	 {%- if item.image.src -%}
		 {%- set loadingAttr = item.image.loading != 'disabled' ? 'loading="{{ item.image.loading|escape_attr }}"' : '' -%}
		 <img src="{{ item.image.src|escape_url }}" alt="{{ item.image.alt|escape_attr }}" {{ loadingAttr }}>
	 {%- endif -%}
 </div>
 <div class="card-content">
	{{ item.column_content }}
 {# Button starts here #}
	{%- if item.add_button -%}
		{%- set href = item.button.button_url.url.href -%}
		{%- if item.button.button_url.url.type is equalto "EMAIL_ADDRESS" -%}
		 {%- set href = "mailto:" + href -%}
		{%- elif item.button.button_url.url.type is equalto "PHONE_NUMBER" -%}
		 {%- set href = "tel:" + href -%}
		{%- endif -%}
	<a
	 {%- if item.button.button_url.url.type is equalto "CALL_TO_ACTION" -%}
	 href="{{ href }}"
	 {%- else -%}
	 href="{{ href|escape_url }}"
	 {%- endif -%}
	 {%- if item.button.button_url.open_in_new_tab -%}
	 target="_blank"
	 {%- endif -%}
	 {%- if item.button.button_url.rel -%}
	 rel="{{ item.button.button_url.rel|escape_attr }}"
	 {%- endif -%}
	 >
	 {{ item.button.button_label }}
	</a>
	{%- endif -%}
{%- endfor -%} 
	</div>
</div>

When it comes to styling, I recommend to create a bunch of CSS classes and trigger them with choice fields in the style group.

This will give you the option to control it globally and in one single place rather than in every instance on every page and every tranaltion variation (if you’re using mutli-lang pages)

My personal approach for creating such classes is to write something like this:

Bildschirmfoto 2026-02-23 um 12.20.51.png

But if you like to go the per module route, you can add a require_css with a scope_css block to it like this

{% require_css %}
<style>
{% scope_css %}
.card-wrapper{
text-align: {{module.style.text_align}};
}
{% scope_css %}
</style>
{% require_css %}

Happy coding

best,

Anton