Love this!
I’ve challenged ChatGPT(Free) and Cursor for creating something I’d call a common HubSpot module. I’ve choosen this on purpose because HubL isn’t that common compared to something as React, but the majority of HubSpot pages are build upon it.
The prompt:
Help me create a HubSpot module with a headline, subline, an image repeater and an optional button above the image repeater. The repeater should be used to display an image slider with the help of splide js.
Furthermore the user should have the options to style many aspects of module like colors, border-radius of the images and button styles.
The module should pass current accessibility features like as screen-reader texts, semantic HTML tags.
To compare both of them as best as possible I did not use any MCP or rules (like cursorrules). Also I didn’t want to spend much time in “prompt engineering” as I assume many people will just write something right away and also might not have a developers knowledge. This is why the prompt is on the low-end of a “good prompt” and also has a few errors.
Before switching to ChatGPT I’ve tried Replit, but it failed as Replit told me that it’s not capable of creating standalone modules since it’s focusing on app creation. Bummer.
Cursor
Cursor did way better job but also made mistakes.
It started with the module.json (which is the meta.json)
Spoiler
{
"label": "Image Slider Module",
"css_assets": [],
"external_js": [
"https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js"
],
"external_css": [
"https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide.min.css"
],
"global": false,
"help_text": "A customizable image slider module with headline, subline, and optional button. Uses Splide.js for smooth sliding functionality.",
"host_template_types": [
"PAGE",
"BLOG_POST",
"BLOG_LISTING"
],
"icon": "https://cdn2.hubspot.net/hubfs/6326501/boilerplate/Module%20Icons/image-gallery.svg",
"js_assets": [],
"other_assets": [],
"smart_type": "NOT_SMART",
"tags": [],
"is_available_for_new_content": true
}
procceeded with the fields.json
Spoiler
[
{
"name": "headline",
"label": "Headline",
"required": false,
"type": "text",
"default": "Image Slider",
"show_emoji_picker": false,
"help_text": "Enter the main headline for the image slider section"
},
{
"name": "subline",
"label": "Subline",
"required": false,
"type": "text",
"default": "Browse through our collection of images",
"show_emoji_picker": false,
"help_text": "Enter a subtitle or description for the image slider"
},
{
"name": "show_button",
"label": "Show Button",
"required": false,
"type": "boolean",
"default": false,
"help_text": "Toggle to show/hide the button above the image slider"
},
{
"name": "button_text",
"label": "Button Text",
"required": false,
"type": "text",
"default": "Learn More",
"show_emoji_picker": false,
"help_text": "Text to display on the button",
"visibility": {
"controlling_field": "show_button",
"controlling_value": true,
"operator": "EQUAL"
}
},
{
"name": "button_link",
"label": "Button Link",
"required": false,
"type": "link",
"help_text": "Select where the button should link to",
"visibility": {
"controlling_field": "show_button",
"controlling_value": true,
"operator": "EQUAL"
}
},
{
"name": "images",
"label": "Images",
"required": true,
"type": "repeater",
"help_text": "Add images to the slider. Each image can have a title and description for accessibility.",
"field_list": [
{
"name": "image",
"label": "Image",
"required": true,
"type": "image",
"help_text": "Select an image for the slider"
},
{
"name": "image_title",
"label": "Image Title",
"required": false,
"type": "text",
"help_text": "Title for the image (used for accessibility and alt text)"
},
{
"name": "image_description",
"label": "Image Description",
"required": false,
"type": "text",
"help_text": "Description of the image (used for accessibility)"
}
]
},
{
"name": "slider_options",
"label": "Slider Options",
"type": "group",
"help_text": "Configure the slider behavior and appearance",
"children": [
{
"name": "autoplay",
"label": "Autoplay",
"required": false,
"type": "boolean",
"default": true,
"help_text": "Enable automatic sliding"
},
{
"name": "autoplay_speed",
"label": "Autoplay Speed (seconds)",
"required": false,
"type": "number",
"default": 3,
"min": 1,
"max": 10,
"help_text": "Time between automatic slides",
"visibility": {
"controlling_field": "autoplay",
"controlling_value": true,
"operator": "EQUAL"
}
},
{
"name": "show_arrows",
"label": "Show Navigation Arrows",
"required": false,
"type": "boolean",
"default": true,
"help_text": "Display previous/next arrow buttons"
},
{
"name": "show_dots",
"label": "Show Dot Indicators",
"required": false,
"type": "boolean",
"default": true,
"help_text": "Display dot indicators for navigation"
},
{
"name": "slides_per_view",
"label": "Slides Per View",
"required": false,
"type": "choice",
"choices": [
["1", "1"],
["2", "2"],
["3", "3"],
["4", "4"]
],
"default": "1",
"help_text": "Number of slides visible at once"
}
]
},
{
"name": "styling",
"label": "Styling Options",
"type": "group",
"help_text": "Customize the appearance of the module",
"children": [
{
"name": "headline_color",
"label": "Headline Color",
"required": false,
"type": "color",
"default": "#333333",
"help_text": "Color for the headline text"
},
{
"name": "subline_color",
"label": "Subline Color",
"required": false,
"type": "color",
"default": "#666666",
"help_text": "Color for the subline text"
},
{
"name": "image_border_radius",
"label": "Image Border Radius",
"required": false,
"type": "choice",
"choices": [
["0", "None"],
["4px", "Small"],
["8px", "Medium"],
["12px", "Large"],
["16px", "Extra Large"],
["50%", "Circular"]
],
"default": "8px",
"help_text": "Border radius for the images"
},
{
"name": "button_style",
"label": "Button Style",
"required": false,
"type": "choice",
"choices": [
["primary", "Primary"],
["secondary", "Secondary"],
["outline", "Outline"],
["ghost", "Ghost"]
],
"default": "primary",
"help_text": "Style variant for the button",
"visibility": {
"controlling_field": "show_button",
"controlling_value": true,
"operator": "EQUAL"
}
},
{
"name": "button_color",
"label": "Button Color",
"required": false,
"type": "color",
"default": "#007cba",
"help_text": "Background color for the button",
"visibility": {
"controlling_field": "show_button",
"controlling_value": true,
"operator": "EQUAL"
}
},
{
"name": "button_text_color",
"label": "Button Text Color",
"required": false,
"type": "color",
"default": "#ffffff",
"help_text": "Text color for the button",
"visibility": {
"controlling_field": "show_button",
"controlling_value": true,
"operator": "EQUAL"
}
},
{
"name": "arrow_color",
"label": "Arrow Color",
"required": false,
"type": "color",
"default": "#333333",
"help_text": "Color for navigation arrows",
"visibility": {
"controlling_field": "show_arrows",
"controlling_value": true,
"operator": "EQUAL"
}
},
{
"name": "dot_color",
"label": "Dot Color",
"required": false,
"type": "color",
"default": "#007cba",
"help_text": "Color for dot indicators",
"visibility": {
"controlling_field": "show_dots",
"controlling_value": true,
"operator": "EQUAL"
}
}
]
},
{
"name": "accessibility",
"label": "Accessibility",
"type": "group",
"help_text": "Accessibility settings for screen readers and keyboard navigation",
"children": [
{
"name": "slider_label",
"label": "Slider Label",
"required": false,
"type": "text",
"default": "Image slider",
"help_text": "Label for screen readers to identify the slider"
},
{
"name": "previous_button_label",
"label": "Previous Button Label",
"required": false,
"type": "text",
"default": "Previous image",
"help_text": "Label for the previous button (screen readers)",
"visibility": {
"controlling_field": "show_arrows",
"controlling_value": true,
"operator": "EQUAL"
}
},
{
"name": "next_button_label",
"label": "Next Button Label",
"required": false,
"type": "text",
"default": "Next image",
"help_text": "Label for the next button (screen readers)",
"visibility": {
"controlling_field": "show_arrows",
"controlling_value": true,
"operator": "EQUAL"
}
}
]
}
]
wrote a module.html
Spoiler
{% if module.headline or module.subline or module.images %}
<section class="image-slider-module"
{% if module.accessibility.slider_label %}aria-label="{{ module.accessibility.slider_label }}"{% endif %}>
<!-- Header Section -->
{% if module.headline or module.subline %}
<header class="image-slider-header">
{% if module.headline %}
<h2 class="image-slider-headline"
style="color: {{ module.styling.headline_color }};">
{{ module.headline }}
</h2>
{% endif %}
{% if module.subline %}
<p class="image-slider-subline"
style="color: {{ module.styling.subline_color }};">
{{ module.subline }}
</p>
{% endif %}
</header>
{% endif %}
<!-- Optional Button -->
{% if module.show_button and module.button_text %}
<div class="image-slider-button-container">
{% if module.button_link %}
<a href="{{ module.button_link.url }}"
class="image-slider-button image-slider-button--{{ module.styling.button_style }}"
style="background-color: {{ module.styling.button_color }}; color: {{ module.styling.button_text_color }};"
{% if module.button_link.open_in_new_tab %}target="_blank"{% endif %}
{% if module.button_link.no_follow %}rel="nofollow"{% endif %}>
{{ module.button_text }}
</a>
{% else %}
<button class="image-slider-button image-slider-button--{{ module.styling.button_style }}"
style="background-color: {{ module.styling.button_color }}; color: {{ module.styling.button_text_color }};">
{{ module.button_text }}
</button>
{% endif %}
</div>
{% endif %}
<!-- Image Slider -->
{% if module.images %}
<div class="image-slider-container">
<div class="splide image-slider"
role="region"
aria-label="{{ module.accessibility.slider_label|default('Image slider') }}">
<div class="splide__track">
<ul class="splide__list">
{% for image_item in module.images %}
<li class="splide__slide">
<figure class="image-slider-figure">
<img src="{{ image_item.image.src }}"
alt="{{ image_item.image_title|default(image_item.image.alt)|default('Slider image') }}"
class="image-slider-image"
style="border-radius: {{ module.styling.image_border_radius }};"
width="{{ image_item.image.width }}"
height="{{ image_item.image.height }}"
loading="lazy">
{% if image_item.image_title or image_item.image_description %}
<figcaption class="image-slider-caption">
{% if image_item.image_title %}
<h3 class="image-slider-caption-title">{{ image_item.image_title }}</h3>
{% endif %}
{% if image_item.image_description %}
<p class="image-slider-caption-description">{{ image_item.image_description }}</p>
{% endif %}
</figcaption>
{% endif %}
</figure>
</li>
{% endfor %}
</ul>
</div>
<!-- Navigation Arrows -->
{% if module.slider_options.show_arrows %}
<div class="splide__arrows">
<button class="splide__arrow splide__arrow--prev"
type="button"
aria-label="{{ module.accessibility.previous_button_label|default('Previous image') }}"
style="color: {{ module.styling.arrow_color }};">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40" focusable="false">
<path d="m15.5 0.932-4.3 4.38 14.5 14.6-14.5 14.5 4.3 4.4 14.6-14.6 4.4-4.3-4.4-4.4-14.6-14.6z"></path>
</svg>
</button>
<button class="splide__arrow splide__arrow--next"
type="button"
aria-label="{{ module.accessibility.next_button_label|default('Next image') }}"
style="color: {{ module.styling.arrow_color }};">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40" focusable="false">
<path d="m15.5 0.932-4.3 4.38 14.5 14.6-14.5 14.5 4.3 4.4 14.6-14.6 4.4-4.3-4.4-4.4-14.6-14.6z"></path>
</svg>
</button>
</div>
{% endif %}
<!-- Pagination Dots -->
{% if module.slider_options.show_dots %}
<div class="splide__pagination"
role="tablist"
aria-label="Slider navigation">
</div>
{% endif %}
</div>
</div>
{% endif %}
</section>
{% endif %}
added a module.css
Spoiler
/* Image Slider Module Styles */
.image-slider-module {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
/* Header Styles */
.image-slider-header {
text-align: center;
margin-bottom: 2rem;
}
.image-slider-headline {
font-size: 2.5rem;
font-weight: 700;
margin: 0 0 1rem 0;
line-height: 1.2;
}
.image-slider-subline {
font-size: 1.125rem;
font-weight: 400;
margin: 0;
line-height: 1.6;
opacity: 0.8;
}
/* Button Styles */
.image-slider-button-container {
text-align: center;
margin-bottom: 2rem;
}
.image-slider-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
min-height: 44px;
min-width: 44px;
}
.image-slider-button:hover,
.image-slider-button:focus {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
outline: none;
}
.image-slider-button:active {
transform: translateY(0);
}
/* Button Style Variants */
.image-slider-button--primary {
background-color: #007cba;
color: #ffffff;
}
.image-slider-button--primary:hover {
background-color: #005a87;
}
.image-slider-button--secondary {
background-color: #6c757d;
color: #ffffff;
}
.image-slider-button--secondary:hover {
background-color: #545b62;
}
.image-slider-button--outline {
background-color: transparent;
border-color: currentColor;
}
.image-slider-button--outline:hover {
background-color: currentColor;
color: #ffffff;
}
.image-slider-button--ghost {
background-color: transparent;
color: inherit;
}
.image-slider-button--ghost:hover {
background-color: rgba(0, 0, 0, 0.1);
}
/* Slider Container */
.image-slider-container {
position: relative;
margin: 0 auto;
max-width: 100%;
}
/* Splide Customization */
.image-slider {
--splide-arrow-size: 2.5rem;
--splide-arrow-bg: rgba(255, 255, 255, 0.9);
--splide-arrow-color: #333333;
--splide-pagination-color: #007cba;
}
.splide__arrow {
background: var(--splide-arrow-bg);
border: none;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
width: var(--splide-arrow-size);
height: var(--splide-arrow-size);
display: flex;
align-items: center;
justify-content: center;
}
.splide__arrow:hover {
background: rgba(255, 255, 255, 1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
transform: scale(1.1);
}
.splide__arrow:focus {
outline: 2px solid #007cba;
outline-offset: 2px;
}
.splide__arrow svg {
width: 1.5rem;
height: 1.5rem;
fill: currentColor;
}
.splide__arrow--prev svg {
transform: rotate(180deg);
}
/* Pagination Dots */
.splide__pagination {
bottom: -2rem;
gap: 0.5rem;
}
.splide__pagination__page {
background: rgba(0, 0, 0, 0.2);
border: none;
border-radius: 50%;
width: 0.75rem;
height: 0.75rem;
transition: all 0.3s ease;
cursor: pointer;
}
.splide__pagination__page:hover {
background: rgba(0, 0, 0, 0.4);
transform: scale(1.2);
}
.splide__pagination__page.is-active {
background: var(--splide-pagination-color);
transform: scale(1.3);
}
/* Image Styles */
.image-slider-figure {
margin: 0;
position: relative;
overflow: hidden;
border-radius: inherit;
}
.image-slider-image {
width: 100%;
height: auto;
display: block;
object-fit: cover;
transition: transform 0.3s ease;
}
.image-slider-figure:hover .image-slider-image {
transform: scale(1.05);
}
/* Image Caption */
.image-slider-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
color: #ffffff;
padding: 2rem 1rem 1rem;
margin: 0;
}
.image-slider-caption-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
line-height: 1.3;
}
.image-slider-caption-description {
font-size: 0.875rem;
margin: 0;
line-height: 1.4;
opacity: 0.9;
}
/* Accessibility Improvements */
.splide__arrow:focus-visible,
.splide__pagination__page:focus-visible {
outline: 2px solid #007cba;
outline-offset: 2px;
}
/* Screen Reader Only Text */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Responsive Design */
@media (max-width: 768px) {
.image-slider-module {
padding: 1rem 0.5rem;
}
.image-slider-headline {
font-size: 2rem;
}
.image-slider-subline {
font-size: 1rem;
}
.image-slider {
--splide-arrow-size: 2rem;
}
.splide__arrow svg {
width: 1.25rem;
height: 1.25rem;
}
.image-slider-caption {
padding: 1.5rem 0.75rem 0.75rem;
}
.image-slider-caption-title {
font-size: 1.125rem;
}
.image-slider-caption-description {
font-size: 0.8rem;
}
}
@media (max-width: 480px) {
.image-slider-headline {
font-size: 1.75rem;
}
.image-slider {
--splide-arrow-size: 1.75rem;
}
.splide__arrow svg {
width: 1rem;
height: 1rem;
}
.image-slider-caption {
padding: 1rem 0.5rem 0.5rem;
}
}
/* High Contrast Mode Support */
@media (prefers-contrast: high) {
.splide__arrow {
border: 2px solid currentColor;
}
.splide__pagination__page {
border: 1px solid currentColor;
}
}
/* Reduced Motion Support */
@media (prefers-reduced-motion: reduce) {
.image-slider-image,
.splide__arrow,
.splide__pagination__page,
.image-slider-button {
transition: none;
}
.image-slider-figure:hover .image-slider-image {
transform: none;
}
.splide__arrow:hover {
transform: none;
}
.splide__pagination__page:hover,
.splide__pagination__page.is-active {
transform: none;
}
.image-slider-button:hover {
transform: none;
}
}
then a module.js
Spoiler
// Image Slider Module JavaScript
(function() {
'use strict';
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', function() {
initializeImageSliders();
});
// Initialize all image sliders on the page
function initializeImageSliders() {
const sliders = document.querySelectorAll('.image-slider');
sliders.forEach(function(slider) {
if (slider && !slider.classList.contains('splide--initialized')) {
initializeSlider(slider);
}
});
}
// Initialize individual slider
function initializeSlider(slider) {
// Get module data from data attributes or default values
const moduleData = getModuleData(slider);
// Create Splide instance
const splide = new Splide(slider, {
type: 'slide',
perPage: parseInt(moduleData.slidesPerView) || 1,
perMove: 1,
gap: '1rem',
padding: '1rem',
autoplay: moduleData.autoplay || false,
interval: (moduleData.autoplaySpeed || 3) * 1000,
pauseOnHover: true,
pauseOnFocus: true,
resetProgress: false,
arrows: moduleData.showArrows !== false,
pagination: moduleData.showDots !== false,
drag: true,
snap: true,
rewind: false,
rewindByDrag: false,
speed: 400,
easing: 'cubic-bezier(0.25, 1, 0.5, 1)',
focus: 'center',
updateOnMove: true,
trimSpace: true,
omitEnd: false,
keyboard: 'focused',
direction: 'ltr',
height: 'auto',
width: '100%',
fixedWidth: false,
fixedHeight: false,
cover: false,
heightRatio: 0.5,
focus: 0,
isNavigation: false,
updateOnMove: true,
trimSpace: true,
omitEnd: false,
keyboard: 'focused',
direction: 'ltr',
height: 'auto',
width: '100%',
fixedWidth: false,
fixedHeight: false,
cover: false,
heightRatio: 0.5,
focus: 0,
isNavigation: false,
classes: {
arrows: 'splide__arrows',
arrow: 'splide__arrow',
prev: 'splide__arrow--prev',
next: 'splide__arrow--next',
pagination: 'splide__pagination',
page: 'splide__pagination__page',
active: 'is-active',
visible: 'is-visible',
clone: 'is-clone',
slide: 'splide__slide',
track: 'splide__track',
list: 'splide__list',
container: 'splide__container'
}
});
// Mount the slider
splide.mount();
// Add custom event listeners for accessibility
addAccessibilityFeatures(splide, moduleData);
// Mark as initialized
slider.classList.add('splide--initialized');
// Store reference for potential cleanup
slider.splideInstance = splide;
}
// Get module data from data attributes or parent elements
function getModuleData(slider) {
const moduleContainer = slider.closest('.image-slider-module');
if (!moduleContainer) return {};
// Try to get data from data attributes (if set by HubSpot)
const data = {
autoplay: getDataAttribute(moduleContainer, 'autoplay') === 'true',
autoplaySpeed: parseInt(getDataAttribute(moduleContainer, 'autoplay-speed')) || 3,
showArrows: getDataAttribute(moduleContainer, 'show-arrows') !== 'false',
showDots: getDataAttribute(moduleContainer, 'show-dots') !== 'false',
slidesPerView: getDataAttribute(moduleContainer, 'slides-per-view') || '1'
};
return data;
}
// Helper function to get data attributes
function getDataAttribute(element, attribute) {
return element.getAttribute('data-' + attribute);
}
// Add accessibility features
function addAccessibilityFeatures(splide, moduleData) {
const track = splide.Components.Track;
const pagination = splide.Components.Pagination;
const arrows = splide.Components.Arrows;
// Add keyboard navigation improvements
if (arrows) {
const prevButton = arrows.arrows.prev;
const nextButton = arrows.arrows.next;
if (prevButton) {
prevButton.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
splide.go('-');
}
});
}
if (nextButton) {
nextButton.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
splide.go('+');
}
});
}
}
// Add ARIA live region for screen readers
addLiveRegion(splide);
// Add focus management
addFocusManagement(splide);
// Add swipe gesture announcements for screen readers
addSwipeAnnouncements(splide);
}
// Add live region for screen reader announcements
function addLiveRegion(splide) {
const liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.className = 'sr-only';
liveRegion.id = 'slider-live-region-' + Math.random().toString(36).substr(2, 9);
splide.root.appendChild(liveRegion);
// Announce slide changes
splide.on('moved', function(newIndex) {
const slides = splide.Components.Elements.slides;
const currentSlide = slides[newIndex];
const image = currentSlide.querySelector('img');
const altText = image ? image.alt : 'Slide ' + (newIndex + 1);
liveRegion.textContent = 'Now viewing: ' + altText;
});
}
// Add focus management
function addFocusManagement(splide) {
const track = splide.Components.Track;
// Keep focus within slider when navigating
splide.on('moved', function() {
const activeSlide = track.slides[splide.index];
if (activeSlide) {
const focusableElements = activeSlide.querySelectorAll('button, a, input, textarea, select, [tabindex]:not([tabindex="-1"])');
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
}
});
}
// Add swipe gesture announcements
function addSwipeAnnouncements(splide) {
let startX = 0;
let startY = 0;
let isSwiping = false;
splide.root.addEventListener('touchstart', function(e) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
isSwiping = false;
}, { passive: true });
splide.root.addEventListener('touchmove', function(e) {
if (!isSwiping) {
const deltaX = Math.abs(e.touches[0].clientX - startX);
const deltaY = Math.abs(e.touches[0].clientY - startY);
if (deltaX > deltaY && deltaX > 10) {
isSwiping = true;
}
}
}, { passive: true });
splide.root.addEventListener('touchend', function(e) {
if (isSwiping) {
const deltaX = e.changedTouches[0].clientX - startX;
const threshold = 50;
if (Math.abs(deltaX) > threshold) {
const liveRegion = document.getElementById('slider-live-region-' + splide.root.querySelector('[aria-live]').id.split('-').pop());
if (liveRegion) {
liveRegion.textContent = deltaX > 0 ? 'Swiped right, previous slide' : 'Swiped left, next slide';
}
}
}
}, { passive: true });
}
// Handle window resize
window.addEventListener('resize', function() {
const sliders = document.querySelectorAll('.splide--initialized');
sliders.forEach(function(slider) {
if (slider.splideInstance) {
slider.splideInstance.refresh();
}
});
});
// Handle visibility change (pause autoplay when tab is not visible)
document.addEventListener('visibilitychange', function() {
const sliders = document.querySelectorAll('.splide--initialized');
sliders.forEach(function(slider) {
if (slider.splideInstance) {
if (document.hidden) {
slider.splideInstance.Components.Autoplay.pause();
} else {
slider.splideInstance.Components.Autoplay.play();
}
}
});
});
// Cleanup function for module removal
window.cleanupImageSliders = function() {
const sliders = document.querySelectorAll('.splide--initialized');
sliders.forEach(function(slider) {
if (slider.splideInstance) {
slider.splideInstance.destroy();
slider.classList.remove('splide--initialized');
delete slider.splideInstance;
}
});
};
})();
created a README.md (which is a nice to have but was never requested)
Spoiler
# HubSpot Image Slider Module
A comprehensive, accessible image slider module for HubSpot CMS with extensive customization options and Splide.js integration.
## Features
### 🎨 Content Management
- **Headline & Subline**: Customizable text content with color options
- **Optional Button**: Configurable button above the slider with multiple style variants
- **Image Repeater**: Add unlimited images with titles and descriptions
- **Accessibility**: Built-in screen reader support and semantic HTML
### 🎯 Slider Functionality
- **Splide.js Integration**: Smooth, performant sliding with touch support
- **Autoplay**: Configurable automatic sliding with pause on hover/focus
- **Navigation**: Optional arrows and dot indicators
- **Responsive**: Multiple slides per view with responsive breakpoints
- **Keyboard Navigation**: Full keyboard accessibility
### 🎨 Styling Options
- **Color Customization**: Headline, subline, button, arrow, and dot colors
- **Image Styling**: Configurable border radius (none to circular)
- **Button Styles**: Primary, secondary, outline, and ghost variants
- **Responsive Design**: Mobile-optimized with breakpoint-specific styling
### ♿ Accessibility Features
- **Screen Reader Support**: ARIA labels, live regions, and semantic markup
- **Keyboard Navigation**: Full keyboard support with focus management
- **High Contrast Mode**: Enhanced visibility for accessibility
- **Reduced Motion**: Respects user motion preferences
- **Touch Gestures**: Swipe support with screen reader announcements
## File Structure
```
image-slider-module/
├── module.json # Module configuration
├── fields.json # Field definitions and options
├── module.html # HubL template with semantic HTML
├── module.css # Comprehensive styling with accessibility
├── module.js # Splide.js initialization and features
└── README.md # This documentation
```
## Installation
1. **Create Module Directory**: Create a new directory in your HubSpot design manager
2. **Upload Files**: Upload all module files to the directory
3. **Configure Module**: Set up the module in HubSpot's design manager
4. **Add to Templates**: Include the module in your page templates
## Field Configuration
### Content Fields
- `headline`: Main title text
- `subline`: Subtitle or description
- `show_button`: Toggle button visibility
- `button_text`: Button label text
- `button_link`: Button destination URL
- `images`: Repeater field for slider images
### Slider Options
- `autoplay`: Enable automatic sliding
- `autoplay_speed`: Time between slides (1-10 seconds)
- `show_arrows`: Display navigation arrows
- `show_dots`: Display pagination dots
- `slides_per_view`: Number of visible slides (1-4)
### Styling Options
- `headline_color`: Headline text color
- `subline_color`: Subline text color
- `image_border_radius`: Image corner rounding
- `button_style`: Button appearance variant
- `button_color`: Button background color
- `button_text_color`: Button text color
- `arrow_color`: Navigation arrow color
- `dot_color`: Pagination dot color
### Accessibility
- `slider_label`: Screen reader label for slider
- `previous_button_label`: Previous button accessibility text
- `next_button_label`: Next button accessibility text
## Usage Examples
### Basic Image Slider
```hubl
{% module "image_slider" path="/path/to/module" %}
```
### With Custom Content
```hubl
{% module "image_slider"
path="/path/to/module",
headline="Our Portfolio",
subline="Explore our latest work",
show_button=true,
button_text="View All Projects",
button_link={
"url": "/portfolio",
"open_in_new_tab": false
}
%}
```
## Accessibility Compliance
### WCAG 2.1 AA Standards
- ✅ **Perceivable**: Proper alt text, color contrast, screen reader support
- ✅ **Operable**: Keyboard navigation, focus management, touch gestures
- ✅ **Understandable**: Clear labels, consistent navigation
- ✅ **Robust**: Semantic HTML, ARIA attributes, progressive enhancement
### Screen Reader Features
- ARIA live regions for slide announcements
- Proper button and link labeling
- Focus management within slider
- Touch gesture announcements
### Keyboard Navigation
- Tab navigation through all interactive elements
- Enter/Space activation for buttons
- Arrow key navigation (when focused)
- Escape key to exit slider mode
## Browser Support
- **Modern Browsers**: Chrome 60+, Firefox 55+, Safari 12+, Edge 79+
- **Mobile**: iOS Safari 12+, Chrome Mobile 60+
- **Accessibility**: NVDA, JAWS, VoiceOver, TalkBack
## Performance Features
- **Lazy Loading**: Images load only when needed
- **Optimized Animations**: Hardware-accelerated CSS transitions
- **Memory Management**: Proper cleanup on module removal
- **Responsive Images**: Automatic sizing and optimization
## Customization
### CSS Customization
The module uses CSS custom properties for easy theming:
```css
.image-slider {
--splide-arrow-size: 2.5rem;
--splide-arrow-bg: rgba(255, 255, 255, 0.9);
--splide-arrow-color: #333333;
--splide-pagination-color: #007cba;
}
```
### JavaScript Extensions
The module provides hooks for custom functionality:
```javascript
// Listen for slide changes
splide.on('moved', function(newIndex) {
console.log('Slide changed to:', newIndex);
});
// Custom initialization
document.addEventListener('DOMContentLoaded', function() {
// Your custom code here
});
```
## Troubleshooting
### Common Issues
1. **Slider Not Initializing**
- Check if Splide.js is loaded
- Verify module structure is correct
- Check browser console for errors
2. **Images Not Displaying**
- Verify image URLs are accessible
- Check HubSpot file manager permissions
- Ensure proper image format (JPG, PNG, WebP)
3. **Accessibility Issues**
- Test with screen readers
- Verify keyboard navigation
- Check color contrast ratios
### Debug Mode
Enable debug logging by adding this to your page:
```javascript
window.splideDebug = true;
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
This module is provided as-is for use with HubSpot CMS. Please ensure compliance with HubSpot's terms of service and your organization's policies.
## Support
For issues and questions:
1. Check the troubleshooting section
2. Review HubSpot's module documentation
3. Test in different browsers and devices
4. Verify accessibility with screen readers
---
**Version**: 1.0.0
**Last Updated**: 2024
**HubSpot Compatibility**: CMS Hub Professional/Enterprise
created a sample-data.json (by itself; not requested)
Spoiler
{
"headline": "Our Amazing Portfolio",
"subline": "Discover our latest projects and creative work",
"show_button": true,
"button_text": "View All Projects",
"button_link": {
"url": "/portfolio",
"open_in_new_tab": false,
"no_follow": false
},
"images": [
{
"image": {
"src": "https://via.placeholder.com/800x600/007cba/ffffff?text=Project+1",
"alt": "Modern web application interface",
"width": 800,
"height": 600
},
"image_title": "E-commerce Platform",
"image_description": "A modern, responsive e-commerce solution with advanced features"
},
{
"image": {
"src": "https://via.placeholder.com/800x600/28a745/ffffff?text=Project+2",
"alt": "Mobile app design mockup",
"width": 800,
"height": 600
},
"image_title": "Mobile Banking App",
"image_description": "Secure and intuitive mobile banking experience for iOS and Android"
},
{
"image": {
"src": "https://via.placeholder.com/800x600/dc3545/ffffff?text=Project+3",
"alt": "Dashboard analytics interface",
"width": 800,
"height": 600
},
"image_title": "Analytics Dashboard",
"image_description": "Real-time data visualization and reporting platform"
},
{
"image": {
"src": "https://via.placeholder.com/800x600/ffc107/000000?text=Project+4",
"alt": "Creative branding design",
"width": 800,
"height": 600
},
"image_title": "Brand Identity Design",
"image_description": "Complete brand identity package including logo, colors, and guidelines"
},
{
"image": {
"src": "https://via.placeholder.com/800x600/6f42c1/ffffff?text=Project+5",
"alt": "3D product visualization",
"width": 800,
"height": 600
},
"image_title": "3D Product Visualization",
"image_description": "High-quality 3D renders for product marketing and e-commerce"
}
],
"slider_options": {
"autoplay": true,
"autoplay_speed": 4,
"show_arrows": true,
"show_dots": true,
"slides_per_view": "1"
},
"styling": {
"headline_color": "#2c3e50",
"subline_color": "#7f8c8d",
"image_border_radius": "12px",
"button_style": "primary",
"button_color": "#007cba",
"button_text_color": "#ffffff",
"arrow_color": "#2c3e50",
"dot_color": "#007cba"
},
"accessibility": {
"slider_label": "Portfolio image slider",
"previous_button_label": "Previous project",
"next_button_label": "Next project"
}
}
as well as a demo.html (outside the request; completely wrong useage)
Spoiler
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HubSpot Image Slider Module Demo</title>
<!-- Splide.js CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide.min.css">
<!-- Module CSS -->
<link rel="stylesheet" href="module.css">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f8f9fa;
}
.demo-container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.demo-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
text-align: center;
}
.demo-header h1 {
margin: 0 0 0.5rem 0;
font-size: 2.5rem;
font-weight: 700;
}
.demo-header p {
margin: 0;
font-size: 1.125rem;
opacity: 0.9;
}
.demo-content {
padding: 2rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.feature-card {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
border-left: 4px solid #007cba;
}
.feature-card h3 {
margin: 0 0 0.5rem 0;
color: #2c3e50;
}
.feature-card p {
margin: 0;
color: #6c757d;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="demo-container">
<div class="demo-header">
<h1>HubSpot Image Slider Module</h1>
<p>A comprehensive, accessible image slider with Splide.js integration</p>
</div>
<div class="demo-content">
<!-- Module Demo -->
<section class="image-slider-module" aria-label="Portfolio image slider">
<!-- Header Section -->
<header class="image-slider-header">
<h2 class="image-slider-headline" style="color: #2c3e50;">
Our Amazing Portfolio
</h2>
<p class="image-slider-subline" style="color: #7f8c8d;">
Discover our latest projects and creative work
</p>
</header>
<!-- Optional Button -->
<div class="image-slider-button-container">
<a href="/portfolio"
class="image-slider-button image-slider-button--primary"
style="background-color: #007cba; color: #ffffff;">
View All Projects
</a>
</div>
<!-- Image Slider -->
<div class="image-slider-container">
<div class="splide image-slider"
role="region"
aria-label="Portfolio image slider">
<div class="splide__track">
<ul class="splide__list">
<li class="splide__slide">
<figure class="image-slider-figure">
<img src="https://via.placeholder.com/800x600/007cba/ffffff?text=Project+1"
alt="Modern web application interface"
class="image-slider-image"
style="border-radius: 12px;"
width="800"
height="600"
loading="lazy">
<figcaption class="image-slider-caption">
<h3 class="image-slider-caption-title">E-commerce Platform</h3>
<p class="image-slider-caption-description">A modern, responsive e-commerce solution with advanced features</p>
</figcaption>
</figure>
</li>
<li class="splide__slide">
<figure class="image-slider-figure">
<img src="https://via.placeholder.com/800x600/28a745/ffffff?text=Project+2"
alt="Mobile app design mockup"
class="image-slider-image"
style="border-radius: 12px;"
width="800"
height="600"
loading="lazy">
<figcaption class="image-slider-caption">
<h3 class="image-slider-caption-title">Mobile Banking App</h3>
<p class="image-slider-caption-description">Secure and intuitive mobile banking experience for iOS and Android</p>
</figcaption>
</figure>
</li>
<li class="splide__slide">
<figure class="image-slider-figure">
<img src="https://via.placeholder.com/800x600/dc3545/ffffff?text=Project+3"
alt="Dashboard analytics interface"
class="image-slider-image"
style="border-radius: 12px;"
width="800"
height="600"
loading="lazy">
<figcaption class="image-slider-caption">
<h3 class="image-slider-caption-title">Analytics Dashboard</h3>
<p class="image-slider-caption-description">Real-time data visualization and reporting platform</p>
</figcaption>
</figure>
</li>
<li class="splide__slide">
<figure class="image-slider-figure">
<img src="https://via.placeholder.com/800x600/ffc107/000000?text=Project+4"
alt="Creative branding design"
class="image-slider-image"
style="border-radius: 12px;"
width="800"
height="600"
loading="lazy">
<figcaption class="image-slider-caption">
<h3 class="image-slider-caption-title">Brand Identity Design</h3>
<p class="image-slider-caption-description">Complete brand identity package including logo, colors, and guidelines</p>
</figcaption>
</figure>
</li>
<li class="splide__slide">
<figure class="image-slider-figure">
<img src="https://via.placeholder.com/800x600/6f42c1/ffffff?text=Project+5"
alt="3D product visualization"
class="image-slider-image"
style="border-radius: 12px;"
width="800"
height="600"
loading="lazy">
<figcaption class="image-slider-caption">
<h3 class="image-slider-caption-title">3D Product Visualization</h3>
<p class="image-slider-caption-description">High-quality 3D renders for product marketing and e-commerce</p>
</figcaption>
</figure>
</li>
</ul>
</div>
<!-- Navigation Arrows -->
<div class="splide__arrows">
<button class="splide__arrow splide__arrow--prev"
type="button"
aria-label="Previous project"
style="color: #2c3e50;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40" focusable="false">
<path d="m15.5 0.932-4.3 4.38 14.5 14.6-14.5 14.5 4.3 4.4 14.6-14.6 4.4-4.3-4.4-4.4-14.6-14.6z"></path>
</svg>
</button>
<button class="splide__arrow splide__arrow--next"
type="button"
aria-label="Next project"
style="color: #2c3e50;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40" focusable="false">
<path d="m15.5 0.932-4.3 4.38 14.5 14.6-14.5 14.5 4.3 4.4 14.6-14.6 4.4-4.3-4.4-4.4-14.6-14.6z"></path>
</svg>
</button>
</div>
<!-- Pagination Dots -->
<div class="splide__pagination"
role="tablist"
aria-label="Slider navigation">
</div>
</div>
</div>
</section>
<!-- Features Overview -->
<div class="feature-grid">
<div class="feature-card">
<h3>🎨 Content Management</h3>
<p>Headline, subline, optional button, and unlimited images with titles and descriptions for accessibility.</p>
</div>
<div class="feature-card">
<h3>🎯 Slider Functionality</h3>
<p>Smooth Splide.js integration with autoplay, navigation arrows, dot indicators, and responsive design.</p>
</div>
<div class="feature-card">
<h3>🎨 Styling Options</h3>
<p>Extensive customization including colors, border radius, button styles, and responsive breakpoints.</p>
</div>
<div class="feature-card">
<h3>♿ Accessibility</h3>
<p>WCAG 2.1 AA compliant with screen reader support, keyboard navigation, and semantic HTML.</p>
</div>
</div>
</div>
</div>
<!-- Splide.js JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js"></script>
<!-- Module JavaScript -->
<script src="module.js"></script>
<script>
// Initialize the demo slider with custom options
document.addEventListener('DOMContentLoaded', function() {
const slider = document.querySelector('.image-slider');
if (slider) {
const splide = new Splide(slider, {
type: 'slide',
perPage: 1,
perMove: 1,
gap: '1rem',
padding: '1rem',
autoplay: true,
interval: 4000,
pauseOnHover: true,
pauseOnFocus: true,
arrows: true,
pagination: true,
drag: true,
snap: true,
speed: 400,
easing: 'cubic-bezier(0.25, 1, 0.5, 1)',
keyboard: 'focused'
});
splide.mount();
}
});
</script>
</body>
</html>
I’ve uploaded the whole chat as attachment. You can open it with a text editor or something like VSCode
ChatGPT
As Cursor it started with the fields.json
Spoiler
[
{
"name": "headline",
"label": "Headline",
"type": "text",
"required": true
},
{
"name": "subline",
"label": "Subline",
"type": "text"
},
{
"name": "show_button",
"label": "Show Button",
"type": "boolean"
},
{
"name": "button_text",
"label": "Button Text",
"type": "text",
"visibility": {
"controlling_field": "show_button",
"controlling_value_regex": "true"
}
},
{
"name": "button_url",
"label": "Button URL",
"type": "url",
"visibility": {
"controlling_field": "show_button",
"controlling_value_regex": "true"
}
},
{
"name": "images",
"label": "Image Slider Items",
"type": "repeatable_group",
"children": [
{
"name": "image",
"label": "Image",
"type": "image"
},
{
"name": "alt_text",
"label": "Alt Text",
"type": "text"
}
]
},
{
"name": "headline_color",
"label": "Headline Color",
"type": "color",
"default": "#000000"
},
{
"name": "subline_color",
"label": "Subline Color",
"type": "color",
"default": "#333333"
},
{
"name": "image_border_radius",
"label": "Image Border Radius (px)",
"type": "number",
"default": 8
},
{
"name": "button_bg_color",
"label": "Button Background Color",
"type": "color",
"default": "#0d75dd"
},
{
"name": "button_text_color",
"label": "Button Text Color",
"type": "color",
"default": "#ffffff"
},
{
"name": "button_border_radius",
"label": "Button Border Radius (px)",
"type": "number",
"default": 6
}
]
but the fields.json won’t work properly as it’s missing quite a few important elements and contains errors. Also it did not include the repeater functionality correctly.
It proceeded with the module.html
Spoiler
<section class="custom-slider-module">
<header>
<h2 style="color: {{ module.headline_color }}">{{ module.headline }}</h2>
{% if module.subline %}
<p style="color: {{ module.subline_color }}">{{ module.subline }}</p>
{% endif %}
{% if module.show_button %}
<a
href="{{ module.button_url }}"
class="custom-button"
style="background-color: {{ module.button_bg_color }}; color: {{ module.button_text_color }}; border-radius: {{ module.button_border_radius }}px;"
aria-label="{{ module.button_text }}"
>
{{ module.button_text }}
</a>
{% endif %}
</header>
{% if module.images %}
<div class="splide" aria-label="Image Slider">
<div class="splide__track">
<ul class="splide__list">
{% for item in module.images %}
<li class="splide__slide">
<img
src="{{ item.image.src }}"
alt="{{ item.alt_text|default('Slide image') }}"
loading="lazy"
style="border-radius: {{ module.image_border_radius }}px;"
/>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</section>
the module.css
Spoiler
.custom-slider-module {
text-align: center;
padding: 2rem;
}
.custom-slider-module h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.custom-slider-module p {
font-size: 1.1rem;
margin-bottom: 1.5rem;
}
.custom-button {
display: inline-block;
padding: 0.75rem 1.5rem;
font-weight: bold;
text-decoration: none;
transition: background-color 0.3s ease;
margin-bottom: 2rem;
}
.custom-button:hover,
.custom-button:focus {
filter: brightness(0.9);
}
the module.js
Spoiler
document.addEventListener('DOMContentLoaded', function () {
if (typeof Splide !== 'undefined') {
new Splide('.splide', {
type : 'loop',
perPage: 1,
autoplay: true,
pagination: true,
arrows: true,
}).mount();
}
});
last but not least it told me to include splide.js into my template or theme.
Spoiler
<!-- Add to module.html head_end or via theme settings -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide.min.css">
<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js"></script>
^ this is possible but will load the JavaScript regardless of if it’s being used or not. Therefore there’s a risk of negative performance impact.
Link to the whole chat
Conclusion
Cursor is the winner as it gives you a way better starting point.
Why starting point?
Based on the super simple prompt and without rules that define things like “create a module in a folder like NAME.module” or “here’s the doc about creating custom HubSpot modules” it provided a way better and cleaner code.
Is the code perfect?
No, but a developer can optimize it much easier than the ChatGPT version
Would I use Cursor to help me create modules in the future?
Yes/No. Over the time I’ve developed my own development style and setup which my clients benefit from and is different than the result of the basic test prompt. For ideas how to write something or adding features I might not think of - yes. But the “final” code would most likely be quite different unless I’d create and add my cursorrules to the prompt.
Would I recommend Cursor for real-life useage?
Only if you’re aware that you’ll need to provide better prompts, instructions and double and triple check the code. Debugging and rewriting this may leave you with something that has almost nothing left in common with the initial module from the Ai.
Is the result any good?
I would say that it does not meet my personal quality standards but for something like learning and understanding how modules can be written, it’s a way better result than ChatGPT
1172517_1_cursor_creating_a_hubspot_module_with_i.txt (54.5 KB)