Overview
Luxury Testimonial Pro allows marketers and content editors to showcase social proof with rich styling options. It supports three display modes - Slider/Carousel, Grid, and List - each responsive and accessible out of the box.
Three Display Modes
Slider carousel, multi-column grid, and stacked list - switchable with a single toggle.
21 Color Controls
Individual color pickers for every UI element, with HubSpot theme inheritance for brand consistency.
Scroll Reveal Animations
Fade-up, fade-in, and slide-in entrance effects with configurable speed and reduced-motion support.
Fully Responsive
Configurable column counts per breakpoint (desktop, tablet, mobile) with hardcoded sensible fallbacks.
Accessibility-Aware
ARIA roles, keyboard navigation, focus styles, and screen reader support baked in.
Google Fonts
Curated luxury font pairings (Cormorant Garamond, Playfair Display, Jost, Inter) loaded dynamically.
Supported content types: Landing Page, Site Page, Blog Post. Can be placed in any template layout including drag-and-drop and coded templates ("host_template_types": ["ANY"]).
Module Metadata
Sourced from meta.json. This file registers the module with HubSpot and controls where it can be used.
File Structure
| File | Role | Affects Rendering? |
|---|---|---|
| module.html | Rendering template - HubL logic, CSS, JavaScript, HTML output | YES |
| fields.json | Editor field schema - provides data that module.html reads | INDIRECT |
| meta.json | Module registration - content types & editor appearance only | NO |
How The Module Works
When a HubSpot page containing this module is rendered, the following sequence occurs:
meta.json Validation
HubSpot reads meta.json to confirm the module is valid for the current content type and template.
Field Values Resolved
Editor-saved values (or defaults from fields.json) are applied to every configurable property.
HubL Engine Executes
module.html is processed - namespace aliases, derived values, unique module ID, and font URLs are computed.
CSS Variables Emitted
All color, typography, layout, and animation values are written into a scoped <style> block as CSS custom properties.
HTML Output
Section header, background decorations, testimonial cards (via HubL macro), and display-mode wrappers are conditionally rendered.
JavaScript Initialised
Inline <script> reads data-* attributes and initialises slider, scroll-reveal animations, keyboard navigation, and touch/swipe.
Dynamic vs Static Functionality
The table below summarises the key feature areas and whether they are controlled by editor fields (dynamic) or hardcoded (static).
| Feature Area | Source | Type | Notes |
|---|---|---|---|
| Section header visibility | section_settings.show_header | Dynamic | Single HubL conditional hides entire header block |
| Display mode | display_mode.enable_slider | Dynamic | Master switch: slider vs static rendering |
| Testimonials repeater | testimonials[ ] | Dynamic | 3–20 items; each renders via HubL macro |
| All color values | colors.* | Dynamic | Emitted as CSS custom properties |
| All typography values | typography.* | Dynamic | Font URLs constructed in HubL |
| Slider config | slider_settings.* | Dynamic | Passed as data-* attributes; read by JavaScript |
| Animations | animation.* | Dynamic | IntersectionObserver + CSS class toggling |
| SVG icon artwork | module.html | Static | Quote mark, arrows, map-pin - hardcoded SVGs |
| Star rating glyph | module.html | Static | Unicode star glyph; only quantity is dynamic |
| Responsive breakpoints | CSS | Static | Fixed at 1024px, 768px, 480px |
| Card internal padding | CSS | Static | Not configurable via editor |
| Orb sizes & positions | CSS | Static | Only orb color is dynamic |
| Google Fonts base URL | HubL | Static | Font names are dynamic; URL structure is hardcoded |
Field Reference
All fields are organised by group as defined in fields.json. Fields in the CONTENT tab appear in the Content panel; fields in the STYLE tab appear in the Style panel.
6.1 section_settings CONTENT
Controls the section header displayed above all testimonial cards. Expanded by default in the editor.
| Field | Label | Type | Default | Required |
|---|---|---|---|---|
| show_header | Show Section Header | Boolean toggle | true | No |
| pre_title | Pre-Title Label | Text | "Client Testimonials" | No |
| heading | Section Heading | Text | "What Our Clients Say" | No |
| description | Section Subtext | Rich Text | "Real experiences from the people we are proud to serve." | No |
show_header: false suppresses all four child fields simultaneously via a single HubL {% if %} block. Both pre_title and heading have allow_new_line: false, preventing multi-line input.6.2 display_mode CONTENT
Controls whether testimonials render as a slider or as a static layout, and configures the static layout's column counts.
| Field | Label | Type | Default |
|---|---|---|---|
| enable_slider | Enable Slider/Carousel | Boolean toggle | true |
| static_layout | Static Layout | Select | "grid" |
| grid_columns_desktop | Grid Columns (Desktop) | Select (1–4) | "3" |
| grid_columns_tablet | Grid Columns (Tablet) | Select (1–3) | "2" |
| grid_columns_mobile | Grid Columns (Mobile) | Select (1–2) | "1" |
6.3 testimonials CONTENT - REPEATER
Core content repeater. Each item represents one testimonial card. Min: 3 items · Max: 20 items · Default: 3 items. The editor uses customer_name as the item label.
| Field | Label | Type | Default | Required |
|---|---|---|---|---|
| category_badge | Category / Tag Badge | Text | "Facial Treatment" | No |
| star_rating | Star Rating (1–5) | Number (1–5) | 5 | No |
| content | Testimonial Content | Rich Text | (long default quote) | Yes |
| customer_image | Customer Photo | Image | src: "" · 72×72px | No |
| customer_name | Customer Name | Text | "Isabelle Laurent" | Yes |
| designation | Designation / Job Title | Text | "Creative Director" | No |
| company | Company Name | Text | "" | No |
| company_logo | Company Logo | Image | src: "" · 120×40px | No |
| location | Location | Text | "Dubai, UAE" | No |
| show_cta | Show CTA Button | Boolean toggle | false | No |
| cta_text | CTA Button Text | Text | "Read Full Story" | No |
| cta_link | CTA Button Link | Link | href: "#" · new tab: false | No |
customer_image.src is empty by default - this triggers an initials placeholder in the template. company_logo is only rendered when an image is actually selected. The cta_link supports EXTERNAL, CONTENT (HubSpot page), FILE, and EMAIL_ADDRESS link types.6.4 visibility CONTENT
Global show/hide toggles. These affect all cards simultaneously. The only per-card toggle is testimonials[n].show_cta.
| Field | Label | Default | Applies To |
|---|---|---|---|
| show_stars | Show Star Ratings | true | All cards |
| show_avatar | Show Client Avatar | true | All cards |
| show_designation | Show Designation | true | All cards |
| show_company | Show Company | false | All cards |
| show_location | Show Location | true | All cards |
| show_badge | Show Category Badge | true | All cards |
| show_quote_icon | Show Decorative Quote Icon | true | All cards |
| show_bg_decor | Show Background Decorations | true | Section |
| show_arrows | Show Navigation Arrows | true | Slider only |
| show_dots | Show Pagination Dots | true | Slider only |
6.5 slider_settings
Only visible in the editor when display_mode.enable_slider = true. This group is correctly enforced via a visibility block in fields.json - the best-implemented conditional group in the module.
| Field | Label | Type | Default |
|---|---|---|---|
| autoplay | Enable Autoplay | Boolean toggle | true |
| autoplay_speed | Autoplay Speed (ms) | Number (1000–20000) | 6000 |
| transition_speed | Transition Speed (ms) | Number (200–2000) | 700 |
| infinite_loop | Infinite Loop | Boolean toggle | true |
| pause_on_hover | Pause on Hover | Boolean toggle | true |
| cards_desktop | Cards Per View (Desktop) | Select (1–3) | "1" |
| cards_tablet | Cards Per View (Tablet) | Select (1–2) | "1" |
6.6 animation CONTENT
| Field | Label | Type | Default |
|---|---|---|---|
| enable_reveal | Enable Scroll Reveal Animations | Boolean toggle | true |
| enable_hover_lift | Enable Card Hover Lift | Boolean toggle | true |
| reveal_style | Reveal Animation Style | Select | "fade-up" |
| anim_speed | Animation Speed | Select | "normal" (0.6s) |
Reveal style options: fade-up - Elegant · fade-in - Subtle · slide-in - Dynamic
Speed mapping: slow = 0.9s · normal = 0.6s · fast = 0.4s
6.7 layout CONTENT
| Field | Label | Type | Default | Options |
|---|---|---|---|---|
| header_align | Header Alignment | Select | "center" | left · center · right |
| card_align | Card Content Alignment | Select | "left" | left · center |
| avatar_position | Avatar Position | Select | "bottom" | top · bottom |
| max_width | Max Content Width | Select | "1240px" | 960px · 1100px · 1240px · 1400px |
6.8 colors STYLE
21 individually configurable color properties using HubSpot's color picker (hex + opacity). Two fields inherit from the HubSpot site theme.
colors.decor_color defaults to 8% opacity - all other fields are 100%. If module.html only reads the hex value and ignores opacity, orbs will render at full intensity, breaking the subtle decorative intent. See Known Issue #1.6.9 typography STYLE
| Field | Label | Default | Options / Mapping |
|---|---|---|---|
| heading_font | Heading Font | Cormorant Garamond | Cormorant Garamond · Playfair Display · DM Serif Display · Libre Baskerville |
| body_font | Body Font | Jost | Jost · Plus Jakarta Sans · Inter · Lato |
| testimonial_size | Testimonial Text Size | "large" | small=1rem · medium=1.125rem · large=1.3rem · xlarge=1.5rem |
| card_radius | Card Border Radius | "20px" | 0px · 8px · 16px · 20px · 28px |
| card_shadow | Card Shadow | "soft" | none · soft · medium · elevated |
| section_padding | Section Padding | "large" | small=48px · medium=72px · large=100px · xlarge=140px |
HubL Logic Breakdown
module.html confirmation.Namespace Aliases
{% set ss = module.section_settings %}
{% set dm = module.display_mode %}
{% set vis = module.visibility %}
{% set sl = module.slider_settings %}
{% set anim = module.animation %}
{% set lay = module.layout %}
{% set col = module.colors %}
{% set typ = module.typography %}
Value Resolver Maps
{# Typography size map #}
{% set testimonial_size_map = {
"small": "1rem",
"medium": "1.125rem",
"large": "1.3rem",
"xlarge": "1.5rem"
} %}
{# Section padding map #}
{% set padding_map = {
"small": "48px", "medium": "72px",
"large": "100px", "xlarge": "140px"
} %}
{# Card shadow map #}
{% set shadow_map = {
"none": "none",
"soft": "0 4px 24px rgba(44,33,24,0.07)",
"medium": "0 8px 32px rgba(44,33,24,0.13)",
"elevated": "0 16px 48px rgba(44,33,24,0.18)"
} %}
{# Animation speed map #}
{% set speed_map = {
"slow": "0.9s", "normal": "0.6s", "fast": "0.4s"
} %}
CSS Custom Properties Block
<style>
#{{ module_id }} {
--section-bg: {{ col.section_bg.color }};
--card-bg: {{ col.card_bg.color }};
--accent: {{ col.accent.color }};
--heading-color: {{ col.heading_color.color }};
--text-color: {{ col.text_color.color }};
--star-color: {{ col.star_color.color }};
--testimonial-size: {{ testimonial_font_size }};
--card-radius: {{ typ.card_radius }};
--card-shadow: {{ card_shadow_val }};
--section-padding: {{ section_padding_val }};
--anim-speed: {{ anim_speed_val }};
--max-width: {{ lay.max_width }};
/* ... all 21 color vars + layout vars */
}
</style>
Display Modes
Slider / Carousel
Default mode. Activates all JavaScript slider logic with configurable autoplay, looping, and per-viewport card counts.
Grid
Renders cards in a CSS grid. Column count is independently configurable for desktop (1–4), tablet (1–3), and mobile (1–2).
List
Cards stacked vertically at full container width using flexbox. No column configuration applies.
| Mode | Trigger Condition | Wrapper Class |
|---|---|---|
| Slider | enable_slider = true | .ltp-slider-wrapper |
| Grid | enable_slider = false AND static_layout = "grid" | .ltp-grid |
| List | enable_slider = false AND static_layout = "list" | .ltp-list |
Slider & JavaScript Architecture
All slider behaviour is JavaScript-driven. Configuration is passed via data-* attributes on the section wrapper and read at initialisation. No external slider library is used.
Data Attributes → JavaScript Config
| Data Attribute | Source Field | Example |
|---|---|---|
| data-slider | display_mode.enable_slider | "true" |
| data-autoplay | slider_settings.autoplay | "true" |
| data-autoplay-speed | slider_settings.autoplay_speed | "6000" |
| data-transition | slider_settings.transition_speed | "700" |
| data-infinite | slider_settings.infinite_loop | "true" |
| data-pause-hover | slider_settings.pause_on_hover | "true" |
| data-cards-desktop | slider_settings.cards_desktop | "1" |
| data-cards-tablet | slider_settings.cards_tablet | "1" |
| data-reveal | animation.enable_reveal | "true" |
| data-reveal-style | animation.reveal_style | "fade-up" |
| data-hover-lift | animation.enable_hover_lift | "true" |
Initialisation Sequence
DOM Ready
DOM-ready helper fires; reduced-motion preference is checked via matchMedia.
Slider Check
If data-slider = false, skip slider init and proceed to scroll-reveal only.
Viewport & Card Width
cardsPerView is computed from viewport width vs desktop/tablet thresholds; each slide width is set to 100% / cardsPerView.
Infinite Loop Cloning
First N and last N slides are cloned and prepended/appended for seamless looping when data-infinite = true.
Event Listeners
Arrow clicks, dot clicks, keyboard (←/→), touch/swipe, mouse drag, hover pause, and Page Visibility API are all wired up.
Autoplay Start
If data-autoplay = true and no reduced-motion preference, an interval fires every data-autoplay-speed ms.
Resize Handler
Debounced resize listener recalculates card widths and re-clones for infinite mode on viewport change.
Keyboard Navigation
| Key | Action |
|---|---|
| Tab | Moves focus between arrows, dots, and CTA buttons |
| Arrow Left ← | Previous slide (when slider is focused) |
| Arrow Right → | Next slide (when slider is focused) |
| Enter / Space | Activates focused arrow or dot button |
CSS Architecture
All CSS is scoped using the unique module ID (#ltp-[unique-id]) as a prefix, ensuring multiple instances on one page cannot conflict.
Component Class Reference
| Class | Component | Key CSS Variable(s) |
|---|---|---|
| .ltp-section | Section wrapper | --section-bg, --section-padding |
| .ltp-container | Inner container | --max-width |
| .ltp-header | Section header block | --header-align |
| .ltp-card | Testimonial card | --card-bg, --card-border, --card-radius, --card-shadow |
| .ltp-badge | Category badge pill | --badge-bg, --badge-text, --badge-border |
| .ltp-stars | Star rating row | --star-color |
| .ltp-content | Quote body text | --text-color, --testimonial-size |
| .ltp-avatar img | Customer photo | border-radius: 50% (fixed) |
| .ltp-cta | CTA button | --cta-bg, --cta-text |
| .ltp-arrow | Prev/next slider arrows | --arrow-bg, --arrow-icon |
| .ltp-dot--active | Active pagination dot | --dot-active |
| .ltp-grid | Static grid wrapper | --cols-desktop, --cols-tablet, --cols-mobile |
Reduced Motion Support
@media (prefers-reduced-motion: reduce) {
#ltp-[id] .ltp-card {
transition: none;
transform: none;
}
#ltp-[id] .ltp-slider-track {
transition: none;
}
}
Accessibility
| Feature | Expected Implementation |
|---|---|
| role="group" | Each testimonial card carries this role |
| aria-roledescription="testimonial" | Applied to each card wrapper for semantic context |
| aria-label on stars | "N out of 5 stars" for screen readers |
| aria-hidden on SVGs | Decorative icons (quote, map-pin, arrows) hidden from AT |
| role="tablist" on dots | Applied to the .ltp-dots container |
| aria-selected on dots | "true" for active dot, "false" for others; updated by JS |
| aria-label on arrows | "Previous testimonial" / "Next testimonial" |
| aria-live="polite" | Off-screen region announces "Slide X of Y" after navigation |
| loading="lazy" | Applied to all customer images |
| prefers-reduced-motion | CSS media query + JS matchMedia check both present |
Responsive Behavior
Desktop
cards_desktop (1–3) cards. Grid uses grid_columns_desktop (default 3). Arrows visible if enabled. Full configured padding and font sizes.Tablet
cards_tablet (1–2) cards. Grid uses grid_columns_tablet (default 2). Padding and font sizes slightly reduced.Mobile
grid_columns_mobile (default 1). Arrows hidden. Reduced padding and font sizes.Small Mobile
Editor Usage Guide
Adding & Managing Testimonials
- 1Open the module in the HubSpot page editor and navigate to the Testimonials panel.
- 2Click Add to create a new testimonial (up to 20 total; minimum 3 required).
- 3Fill in the Customer Name (required) and Testimonial Content (required) for each item.
- 4Optionally add a Customer Photo (leave empty for initials placeholder), designation, location, badge, and company info.
- 5Toggle Show CTA Button per card and fill in the button text and link destination.
Choosing a Display Mode
- 1Go to the Display Mode panel.
- 2Toggle Enable Slider/Carousel: ON for slider, OFF for static layout.
- 3If OFF, choose Grid View or List View, then set column counts per breakpoint.
Configuring Slider Behaviour
- 1Open Slider Settings (only visible when slider is enabled).
- 2Set Autoplay Speed - e.g. 6000 = 6 seconds between slides.
- 3Set Transition Speed - e.g. 700 = 0.7 second slide animation.
- 4Enable Infinite Loop for seamless wrap-around from last to first.
- 5Set Cards Per View for desktop (1–3) and tablet (1–2) independently.
Customising Colors
- 1Click the Style tab in the module editor.
- 2Open the Colors panel - 21 independent color pickers are available.
- 3Accent and Heading colors inherit from the site theme by default - override only if needed.
- 4The Decor Orb color defaults to 8% opacity - keep this low for a subtle effect.
Default Configuration
| Setting Group | Key Defaults |
|---|---|
| Section Header | Show: Yes · Pre-Title: "Client Testimonials" · Heading: "What Our Clients Say" |
| Display Mode | Slider: Enabled · Static: Grid · Desktop cols: 3 · Tablet cols: 2 · Mobile cols: 1 |
| Slider | Autoplay: On · Speed: 6000ms · Transition: 700ms · Infinite: On · Pause on hover: On · Cards desktop: 1 |
| Animation | Scroll reveal: On · Hover lift: On · Style: Fade Up · Speed: Normal (0.6s) |
| Layout | Header align: Center · Card align: Left · Avatar: Bottom · Max width: 1240px |
| Typography | Heading: Cormorant Garamond · Body: Jost · Text size: Large (1.3rem) · Radius: 20px · Shadow: Soft · Padding: 100px |
| Default Testimonials | 3 items: Isabelle Laurent (Dubai) · Sophia Carter (London) · Emma Wilson (New York) - all 5 stars |
| Visibility | Show company: OFF · Everything else: ON |
Known Issues & Recommendations
Items marked Potential Bug are possible defects; UX Improvement items are usability suggestions; Verify items require module.html confirmation.
Observation: colors.decor_color has a default opacity of 8%, while all other color fields are 100%. If module.html only reads col.decor_color.color (hex) and ignores the opacity value, orbs render at full intensity, breaking the subtle decorative intent.
Recommended fix: Confirm the template converts opacity to rgba(). Note that HubL has no built-in hex_to_rgb filter - a custom macro is typically required for this conversion.
Observation: When both open_in_new_tab = true and no_follow = true, the security-complete rel value should be "nofollow noreferrer noopener". If the template only sets rel="nofollow" without checking the tab flag, the missing noreferrer noopener values create a reverse tabnapping vulnerability.
Recommended fix: Verify module.html builds the complete rel string based on both boolean values.
Observation: static_layout, grid_columns_desktop, grid_columns_tablet, and grid_columns_mobile have no visibility condition in fields.json - they always appear in the editor, even when the slider is enabled and these fields have no effect.
Recommended fix: Add a visibility condition to each field: controlling_field: "display_mode.enable_slider", operator: "EQUAL", controlling_value_regex: "false".
Observation: visibility.show_arrows and visibility.show_dots have help text noting they only apply to slider mode, but no visibility condition hides them when the slider is disabled. This may confuse editors.
Recommended fix: Add visibility conditions linked to display_mode.enable_slider = true.
Observation: With 2 cards per view and 6 testimonials, there are only 3 unique slide positions - but if dots are HubL-generated via {% for t in module.testimonials %}, 6 dots would be rendered, causing desynchronised dot states.
Recommended fix: Dots should be counted as ceil(total / cards_per_view), or generated by JavaScript after slider initialisation. Verify which approach module.html uses.
Observation: The slider has cards_desktop and cards_tablet fields but no cards_mobile equivalent. Mobile viewport is hardcoded to 1 card in JavaScript, with no editor override available.
Recommended fix: Add a slider_settings.cards_mobile choice field (1 card only option for simplicity, or 1–2) with visibility condition matching the other slider fields.
Summary
Luxury Testimonial Pro is a sophisticated, feature-complete HubSpot CMS testimonial module built for premium brands requiring highly polished design and rich editorial control.
Strengths
- 9 field groups, 50+ fields - deep configurability without code changes
- Three display modes: Slider, Grid, and List
- 21 independent color controls with HubSpot theme inheritance
- Scoped CSS - multiple instances on one page never conflict
- Accessibility-aware: ARIA, keyboard nav, reduced-motion support
- Google Fonts integration with curated luxury font pairings
- slider_settings correctly hidden when slider is disabled
- IntersectionObserver scroll-reveal with three animation styles
Limitations
- module.html not provided - several details are inferred
- Static layout fields lack visibility conditions for slider-on state
- Arrow/dot toggles visible even when slider is off
- Dots count may not account for cards_desktop > 1
- Decor orb opacity may not be applied to rgba() conversion
- CTA rel attribute combination needs security verification
- No cards_mobile field - slider mobile always shows 1 card
Recommended Next Steps
- 1Provide
module.htmlfor full code-level verification of all items marked Verify. - 2Add visibility conditions to static layout fields and arrow/dot toggles.
- 3Verify opacity handling for
colors.decor_color- confirm rgba() conversion. - 4Audit CTA
relattribute logic for security completeness when both flags are true. - 5Audit dot count generation for multi-card-per-view slider configurations.
- 6Consider adding a
cards_mobilefield toslider_settings.