Very Advanced Reviews Slider + Grid with all controls:
Create a Section with this code:
{% comment %}
bpers - Testimonials Slider (layout matched to screenshot)
NEW:
✅ Added Layout option: Slider / Grid
- Slider = current behavior (arrows, swipe, dots, autoplay, transform)
- Grid = same cards shown in a responsive grid (no slider JS, no arrows/dots)
FIXES kept:
1) Drag only starts after move threshold (clicks work)
2) Clamp applied on inner wrapper (.bpers-content-inner)
3) Read more / Read less toggles via delegated click
{% endcomment %}
{%- liquid
assign sid = section.id
assign slides_desktop = section.settings.slides_desktop | default: 4
assign slides_tablet = section.settings.slides_tablet | default: 2
assign slides_mobile = section.settings.slides_mobile | default: 1
assign gap = section.settings.card_gap | default: 24
assign layout_mode = section.settings.layout_mode | default: 'slider'
-%}
<style>
/* =========================
bpers Testimonials (Screenshot Layout)
========================= */
#bpers-testimonials-{{ sid }}{
--bpers-gap: {{ gap }}px;
--bpers-slides-desktop: {{ slides_desktop }};
--bpers-slides-tablet: {{ slides_tablet }};
--bpers-slides-mobile: {{ slides_mobile }};
--bpers-maxw: {{ section.settings.max_width }}px;
--bpers-bg: {{ section.settings.section_bg }};
--bpers-card: {{ section.settings.card_bg }};
--bpers-text: {{ section.settings.text_color }};
--bpers-muted: {{ section.settings.muted_color }};
--bpers-star: {{ section.settings.star_color }};
--bpers-verified: {{ section.settings.verified_color }};
--bpers-initial-bg: {{ section.settings.initial_bg }};
--bpers-initial-text: {{ section.settings.initial_text }};
--bpers-arrow-bg: {{ section.settings.arrow_bg }};
--bpers-arrow-color: {{ section.settings.arrow_color }};
--bpers-border: {{ section.settings.card_border_width }}px solid {{ section.settings.card_border_color }};
--bpers-radius: {{ section.settings.card_radius }}px;
--bpers-shadow: {{ section.settings.card_shadow }};
--bpers-avatar: {{ section.settings.avatar_size }}px;
--bpers-clamp: {{ section.settings.clamp_lines }};
background: var(--bpers-bg);
color: var(--bpers-text);
padding: {{ section.settings.padding_top }}px 0 {{ section.settings.padding_bottom }}px;
}
#bpers-testimonials-{{ sid }} .bpers-wrap{
max-width: var(--bpers-maxw);
margin: 0 auto;
padding: 0 {{ section.settings.side_padding }}px;
}
/* Header */
#bpers-testimonials-{{ sid }} .bpers-head{
text-align: center;
margin-bottom: {{ section.settings.header_spacing }}px;
}
#bpers-testimonials-{{ sid }} .bpers-title{
margin: 0;
font-size: clamp(28px, 3vw, 56px);
line-height: 1.05;
letter-spacing: -0.02em;
}
#bpers-testimonials-{{ sid }} .bpers-subtitle{
margin: 14px auto 0;
max-width: {{ section.settings.subtitle_max_width }}px;
color: var(--bpers-muted);
font-size: 15px;
line-height: 1.6;
}
/* Shell */
#bpers-testimonials-{{ sid }} .bpers-slider{
position: relative;
}
/* --- Slider mode styles --- */
#bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-viewport{
overflow: hidden;
padding: 6px 0;
touch-action: pan-y;
}
#bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-track{
display: flex;
gap: var(--bpers-gap);
will-change: transform;
transition: transform {{ section.settings.transition_ms }}ms ease;
}
/* Slides per view (only in slider mode) */
#bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-slide{
flex: 0 0 calc((100% - (var(--bpers-gap) * (var(--bpers-slides-desktop) - 1))) / var(--bpers-slides-desktop));
min-width: 0;
}
@media (max-width: 989px){
#bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-slide{
flex-basis: calc((100% - (var(--bpers-gap) * (var(--bpers-slides-tablet) - 1))) / var(--bpers-slides-tablet));
}
}
@media (max-width: 749px){
#bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-slide{
flex-basis: calc((100% - (var(--bpers-gap) * (var(--bpers-slides-mobile) - 1))) / var(--bpers-slides-mobile));
}
}
/* --- Grid mode styles --- */
#bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-viewport{
overflow: visible;
padding: 0;
}
#bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-track{
display: grid;
gap: var(--bpers-gap);
transform: none !important;
transition: none !important;
grid-template-columns: repeat(var(--bpers-slides-desktop), minmax(0, 1fr));
}
@media (max-width: 989px){
#bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-track{
grid-template-columns: repeat(var(--bpers-slides-tablet), minmax(0, 1fr));
}
}
@media (max-width: 749px){
#bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-track{
grid-template-columns: repeat(var(--bpers-slides-mobile), minmax(0, 1fr));
}
}
#bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-slide{
min-width: 0;
}
/* Card */
#bpers-testimonials-{{ sid }} .bpers-card{
height: 100%;
background: var(--bpers-card);
border: var(--bpers-border);
border-radius: var(--bpers-radius);
box-shadow: var(--bpers-shadow);
padding: {{ section.settings.card_padding }}px {{ section.settings.card_padding_x }}px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
/* Avatar */
#bpers-testimonials-{{ sid }} .bpers-avatar-wrap{
position: relative;
width: var(--bpers-avatar);
height: var(--bpers-avatar);
margin-top: 6px;
margin-bottom: 12px;
}
#bpers-testimonials-{{ sid }} .bpers-avatar{
width: 100%;
height: 100%;
border-radius: 999px;
overflow: hidden;
display: grid;
place-items: center;
background: rgba(0,0,0,.04);
}
#bpers-testimonials-{{ sid }} .bpers-avatar img{
width: 100%;
height: 100%;
object-fit: cover;
display:block;
}
#bpers-testimonials-{{ sid }} .bpers-initial{
width: 100%;
height: 100%;
border-radius: 999px;
display: grid;
place-items: center;
font-weight: 700;
font-size: calc(var(--bpers-avatar) * .42);
background: var(--bpers-initial-bg);
color: var(--bpers-initial-text);
}
#bpers-testimonials-{{ sid }} .bpers-badge{
position: absolute;
right: -2px;
bottom: -2px;
width: 18px;
height: 18px;
border-radius: 999px;
background: #fff;
display: grid;
place-items: center;
box-shadow: 0 2px 6px rgba(0,0,0,.12);
}
#bpers-testimonials-{{ sid }} .bpers-badge svg{
width: 14px;
height: 14px;
display:block;
}
/* Name + date */
#bpers-testimonials-{{ sid }} .bpers-name{
margin: 0;
font-weight: 700;
font-size: 16px;
line-height: 1.25;
}
#bpers-testimonials-{{ sid }} .bpers-date{
margin: 6px 0 10px;
color: var(--bpers-muted);
font-size: 13px;
line-height: 1.2;
}
/* Stars */
#bpers-testimonials-{{ sid }} .bpers-stars-row{
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
#bpers-testimonials-{{ sid }} .bpers-stars{
display: inline-flex;
gap: 2px;
align-items: center;
}
#bpers-testimonials-{{ sid }} .bpers-star{
width: 16px;
height: 16px;
color: var(--bpers-star);
}
#bpers-testimonials-{{ sid }} .bpers-star svg{
width: 16px;
height: 16px;
fill: currentColor;
display:block;
}
#bpers-testimonials-{{ sid }} .bpers-verified{
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: var(--bpers-verified);
}
#bpers-testimonials-{{ sid }} .bpers-verified svg{
width: 18px;
height: 18px;
fill: currentColor;
display:block;
}
/* Content + clamp */
#bpers-testimonials-{{ sid }} .bpers-content{
width: 100%;
margin: 0;
color: var(--bpers-text);
font-size: 14px;
line-height: 1.7;
}
#bpers-testimonials-{{ sid }} .bpers-content-inner{
width: 100%;
}
#bpers-testimonials-{{ sid }} .bpers-content-inner > *:first-child{ margin-top: 0; }
#bpers-testimonials-{{ sid }} .bpers-content-inner > *:last-child{ margin-bottom: 0; }
#bpers-testimonials-{{ sid }} .bpers-content-inner p{ margin: 0 0 10px; }
#bpers-testimonials-{{ sid }} .bpers-content-inner.is-clamped{
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: var(--bpers-clamp);
}
#bpers-testimonials-{{ sid }} .bpers-readmore{
margin-top: 14px;
border: 0;
background: transparent;
padding: 0;
cursor: pointer;
color: var(--bpers-muted);
text-decoration: none;
font-size: 14px;
}
#bpers-testimonials-{{ sid }} .bpers-readmore:hover{
text-decoration: underline;
text-underline-offset: 3px;
}
#bpers-testimonials-{{ sid }} .bpers-readmore.is-hidden{ display:none; }
/* Arrows overlay (only slider mode) */
#bpers-testimonials-{{ sid }} .bpers-arrow{
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
border-radius: 999px;
border: 1px solid rgba(0,0,0,.12);
background: var(--bpers-arrow-bg);
color: var(--bpers-arrow-color);
display: grid;
place-items: center;
cursor: pointer;
z-index: 5;
transition: opacity .12s ease, transform .12s ease;
}
#bpers-testimonials-{{ sid }} .bpers-arrow:active{ transform: translateY(-50%) scale(.98); }
#bpers-testimonials-{{ sid }} .bpers-arrow[disabled]{ opacity: .45; cursor: not-allowed; }
#bpers-testimonials-{{ sid }} .bpers-prev{ left: -6px; }
#bpers-testimonials-{{ sid }} .bpers-next{ right: -6px; }
@media (max-width: 749px){
#bpers-testimonials-{{ sid }} .bpers-prev{ left: -2px; }
#bpers-testimonials-{{ sid }} .bpers-next{ right: -2px; }
}
/* Hide arrows/dots when grid mode */
#bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-arrow{ display:none !important; }
#bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-dots{ display:none !important; }
/* Optional dots */
#bpers-testimonials-{{ sid }} .bpers-dots{
display: none;
justify-content: center;
gap: 8px;
margin-top: 16px;
}
#bpers-testimonials-{{ sid }} .bpers-dot{
width: 9px; height: 9px;
border-radius: 999px;
border: 1px solid rgba(0,0,0,.25);
background: transparent;
cursor: pointer;
padding: 0;
}
#bpers-testimonials-{{ sid }} .bpers-dot.is-active{
background: {{ section.settings.dot_active }};
border-color: {{ section.settings.dot_active }};
}
{% if section.settings.show_dots %}
#bpers-testimonials-{{ sid }} .bpers-dots{ display:flex; }
{% endif %}
.visually-hidden{
position:absolute!important;
height:1px;width:1px;
overflow:hidden;
clip:rect(1px,1px,1px,1px);
white-space:nowrap;
}
</style>
<section
id="bpers-testimonials-{{ sid }}"
class="bpers-testimonials"
data-bpers-section
data-bpers-layout="{{ layout_mode }}"
>
<div class="bpers-wrap">
{% if section.settings.heading != blank or section.settings.subheading != blank %}
<header class="bpers-head">
{% if section.settings.heading != blank %}
<h2 class="bpers-title">{{ section.settings.heading }}</h2>
{% endif %}
{% if section.settings.subheading != blank %}
<div class="bpers-subtitle">{{ section.settings.subheading }}</div>
{% endif %}
</header>
{% endif %}
<div class="bpers-slider"
data-bpers-slider
data-autoplay="{{ section.settings.autoplay }}"
data-interval="{{ section.settings.autoplay_interval | times: 1000 }}"
data-loop="{{ section.settings.loop }}"
data-pause-on-hover="{{ section.settings.pause_on_hover }}"
data-announce="{{ section.settings.aria_announce }}"
>
{% if section.settings.show_arrows %}
<button class="bpers-arrow bpers-prev" type="button" data-bpers-prev aria-label="Previous testimonials">‹</button>
<button class="bpers-arrow bpers-next" type="button" data-bpers-next aria-label="Next testimonials">›</button>
{% endif %}
<div class="bpers-viewport" data-bpers-viewport>
<div class="bpers-track" data-bpers-track>
{% for block in section.blocks %}
{%- liquid
assign t_image = block.settings.image
assign t_name = block.settings.name | default: 'Client Name'
assign t_date = block.settings.date
assign t_rating = block.settings.rating | default: 5
assign t_content = block.settings.content
assign initial = t_name | strip | slice: 0, 1 | upcase
-%}
<article class="bpers-slide" data-bpers-slide {{ block.shopify_attributes }}>
<div class="bpers-card">
<div class="bpers-avatar-wrap" aria-hidden="true">
<div class="bpers-avatar">
{% if t_image != blank %}
<img
src="{{ t_image | image_url: width: 240 }}"
srcset="
{{ t_image | image_url: width: 120 }} 120w,
{{ t_image | image_url: width: 180 }} 180w,
{{ t_image | image_url: width: 240 }} 240w
"
sizes="(max-width: 749px) {{ section.settings.avatar_size }}px, {{ section.settings.avatar_size }}px"
alt="{{ t_name | escape }}"
loading="lazy"
width="{{ section.settings.avatar_size }}"
height="{{ section.settings.avatar_size }}"
>
{% else %}
<div class="bpers-initial">{{ initial }}</div>
{% endif %}
</div>
{% if section.settings.show_small_badge %}
<span class="bpers-badge" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false" aria-hidden="true">
<path fill="#4285F4" d="M21.35 11.1H12v2.96h5.35c-.24 1.5-1.75 4.4-5.35 4.4-3.22 0-5.85-2.66-5.85-5.93S8.78 6.6 12 6.6c1.83 0 3.06.78 3.76 1.45l2.56-2.46C16.74 4.1 14.58 3 12 3 6.98 3 2.9 7.08 2.9 12.53S6.98 22.06 12 22.06c5.2 0 8.65-3.65 8.65-8.8 0-.59-.07-1.04-.15-1.46z"/>
</svg>
</span>
{% endif %}
</div>
<p class="bpers-name">{{ t_name }}</p>
{% if t_date != blank %}
<p class="bpers-date">{{ t_date }}</p>
{% endif %}
<div class="bpers-stars-row">
{% if section.settings.show_stars %}
<div class="bpers-stars" aria-label="Rating: {{ t_rating }} out of 5">
{% for i in (1..5) %}
<span class="bpers-star" aria-hidden="true">
{% if i <= t_rating %}
<svg viewBox="0 0 24 24"><path d="M12 17.3l-6.18 3.73 1.64-7.03L2 9.24l7.19-.62L12 2l2.81 6.62 7.19.62-5.46 4.76 1.64 7.03z"/></svg>
{% else %}
<svg viewBox="0 0 24 24"><path d="M12 17.3l-6.18 3.73 1.64-7.03L2 9.24l7.19-.62L12 2l2.81 6.62 7.19.62-5.46 4.76 1.64 7.03z" style="fill: rgba(0,0,0,.15)"/></svg>
{% endif %}
</span>
{% endfor %}
</div>
{% endif %}
{% if section.settings.show_verified_icon %}
<span class="bpers-verified" aria-label="Verified">
<svg viewBox="0 0 24 24" focusable="false" aria-hidden="true">
<path d="M12 2a10 10 0 1 0 10 10A10.01 10.01 0 0 0 12 2Zm-1.2 14.2-3.6-3.6 1.4-1.4 2.2 2.2 4.8-4.8 1.4 1.4Z"/>
</svg>
</span>
{% endif %}
</div>
{% if t_content != blank %}
<div class="bpers-content" data-bpers-content>
<div class="bpers-content-inner is-clamped" data-bpers-content-inner>
{{ t_content }}
</div>
</div>
<button
class="bpers-readmore"
type="button"
data-bpers-readmore
aria-expanded="false"
>
{{ section.settings.read_more_label }}
</button>
{% endif %}
</div>
</article>
{% endfor %}
</div>
</div>
<div class="bpers-dots" data-bpers-dots aria-label="Testimonials pagination"></div>
{% if section.settings.aria_announce %}
<div class="visually-hidden" aria-live="polite" data-bpers-live></div>
{% endif %}
</div>
</div>
</section>
<script>
(() => {
const root = document.querySelector('#bpers-testimonials-{{ sid }}');
if (!root) return;
const layout = root.dataset.bpersLayout || 'slider';
const slider = root.querySelector('[data-bpers-slider]');
const viewport = root.querySelector('[data-bpers-viewport]');
const track = root.querySelector('[data-bpers-track]');
const slides = Array.from(root.querySelectorAll('[data-bpers-slide]'));
const prevBtn = root.querySelector('[data-bpers-prev]');
const nextBtn = root.querySelector('[data-bpers-next]');
const dotsWrap = root.querySelector('[data-bpers-dots]');
const live = root.querySelector('[data-bpers-live]');
if (!track || slides.length === 0) return;
// =========================
// READ MORE (works in grid + slider)
// =========================
const readMoreLabel = "{{ section.settings.read_more_label | escape }}";
const readLessLabel = "{{ section.settings.read_less_label | escape }}";
const measureReadMore = (inner, btn) => {
if (!inner || !btn) return;
const expanded = btn.getAttribute('aria-expanded') === 'true';
if (expanded) {
btn.classList.remove('is-hidden');
btn.textContent = readLessLabel;
return;
}
inner.classList.add('is-clamped');
btn.textContent = readMoreLabel;
btn.classList.remove('is-hidden');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const overflowing = inner.scrollHeight > inner.clientHeight + 2;
if (!overflowing) btn.classList.add('is-hidden');
});
});
};
const setupReadMore = () => {
root.querySelectorAll('[data-bpers-content]').forEach((wrap) => {
const inner = wrap.querySelector('[data-bpers-content-inner]');
const card = wrap.closest('.bpers-card');
const btn = card ? card.querySelector('[data-bpers-readmore]') : null;
if (!inner || !btn) return;
btn.setAttribute('aria-expanded', 'false');
inner.classList.add('is-clamped');
btn.textContent = readMoreLabel;
measureReadMore(inner, btn);
});
};
root.addEventListener('click', (e) => {
const btn = e.target.closest('[data-bpers-readmore]');
if (!btn || !root.contains(btn)) return;
e.preventDefault();
e.stopPropagation();
const card = btn.closest('.bpers-card');
const inner = card ? card.querySelector('[data-bpers-content-inner]') : null;
if (!inner) return;
const expanded = btn.getAttribute('aria-expanded') === 'true';
if (expanded) {
btn.setAttribute('aria-expanded', 'false');
inner.classList.add('is-clamped');
btn.textContent = readMoreLabel;
measureReadMore(inner, btn);
} else {
btn.setAttribute('aria-expanded', 'true');
inner.classList.remove('is-clamped');
btn.classList.remove('is-hidden');
btn.textContent = readLessLabel;
}
}, { passive: false });
if ('ResizeObserver' in window) {
if (root._bpersRO) root._bpersRO.disconnect();
root._bpersRO = new ResizeObserver(() => {
root.querySelectorAll('.bpers-card').forEach((card) => {
const inner = card.querySelector('[data-bpers-content-inner]');
const btn = card.querySelector('[data-bpers-readmore]');
if (inner && btn) measureReadMore(inner, btn);
});
});
root.querySelectorAll('[data-bpers-content-inner]').forEach((el) => root._bpersRO.observe(el));
}
// If GRID mode: no slider behavior needed
if (layout === 'grid') {
setupReadMore();
// also re-check after fonts load
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => setupReadMore()).catch(() => {});
}
window.addEventListener('resize', () => {
clearTimeout(root._bpersResizeT);
root._bpersResizeT = setTimeout(() => setupReadMore(), 120);
});
return;
}
// =========================
// SLIDER behavior (only when layout === 'slider')
// =========================
const opts = {
autoplay: slider?.dataset.autoplay === 'true',
interval: parseInt(slider?.dataset.interval || '5000', 10),
loop: slider?.dataset.loop === 'true',
pauseOnHover: slider?.dataset.pauseOnHover === 'true',
announce: slider?.dataset.announce === 'true'
};
const getSlidesPerView = () => {
const w = window.innerWidth;
const styles = getComputedStyle(root);
if (w <= 749) return parseFloat(styles.getPropertyValue('--bpers-slides-mobile')) || 1;
if (w <= 989) return parseFloat(styles.getPropertyValue('--bpers-slides-tablet')) || 2;
return parseFloat(styles.getPropertyValue('--bpers-slides-desktop')) || 4;
};
const getGap = () => parseFloat(getComputedStyle(root).getPropertyValue('--bpers-gap')) || 24;
let index = 0;
let autoplayTimer = null;
const maxIndex = () => {
const spv = getSlidesPerView();
return Math.max(0, Math.ceil(slides.length - spv));
};
const clampIndex = (i) => {
const max = maxIndex();
if (opts.loop) {
if (i < 0) return max;
if (i > max) return 0;
return i;
}
return Math.max(0, Math.min(i, max));
};
const slideWidth = () => slides[0].getBoundingClientRect().width;
const setTransform = (x) => { track.style.transform = `translate3d(${x}px,0,0)`; };
const updateButtons = () => {
if (!prevBtn || !nextBtn) return;
if (opts.loop) { prevBtn.disabled = false; nextBtn.disabled = false; return; }
const max = maxIndex();
prevBtn.disabled = index <= 0;
nextBtn.disabled = index >= max;
};
const buildDots = () => {
if (!dotsWrap) return;
dotsWrap.innerHTML = '';
const count = maxIndex() + 1;
for (let i = 0; i < count; i++) {
const b = document.createElement('button');
b.type = 'button';
b.className = 'bpers-dot' + (i === 0 ? ' is-active' : '');
b.setAttribute('aria-label', `Go to testimonials ${i + 1}`);
b.onclick = () => goTo(i);
dotsWrap.appendChild(b);
}
};
const setActiveDot = () => {
if (!dotsWrap) return;
const dots = Array.from(dotsWrap.querySelectorAll('button'));
dots.forEach(d => d.classList.remove('is-active'));
if (dots[index]) dots[index].classList.add('is-active');
};
const goTo = (i, { silent = false } = {}) => {
index = clampIndex(i);
const x = -1 * index * (slideWidth() + getGap());
setTransform(x);
updateButtons();
setActiveDot();
if (opts.announce && live && !silent) {
live.textContent = `Showing testimonials ${index + 1} of ${slides.length}`;
}
};
const next = () => goTo(index + 1);
const prev = () => goTo(index - 1);
const stopAutoplay = () => { if (autoplayTimer) clearInterval(autoplayTimer); autoplayTimer = null; };
const startAutoplay = () => {
stopAutoplay();
if (!opts.autoplay) return;
autoplayTimer = setInterval(next, Math.max(2000, opts.interval || 5000));
};
// Drag / Swipe (threshold-based)
let isPointerDown = false;
let isDragging = false;
let startX = 0;
let startTransform = 0;
const isInteractiveTarget = (t) => {
return !!(t && t.closest && t.closest('button, a, input, textarea, select, [data-bpers-readmore], [data-bpers-prev], [data-bpers-next], [data-bpers-dots]'));
};
const getCurrentTransformX = () => {
const tr = getComputedStyle(track).transform;
if (!tr || tr === 'none') return 0;
const m = new DOMMatrixReadOnly(tr);
return m.m41 || 0;
};
const pointerDown = (e) => {
if (!viewport) return;
if (isInteractiveTarget(e.target)) return;
isPointerDown = true;
isDragging = false;
startX = e.clientX;
startTransform = getCurrentTransformX();
};
const pointerMove = (e) => {
if (!isPointerDown) return;
const dx = e.clientX - startX;
if (!isDragging) {
if (Math.abs(dx) < 8) return;
isDragging = true;
track.style.transition = 'none';
viewport.setPointerCapture?.(e.pointerId);
}
setTransform(startTransform + dx);
};
const pointerUp = (e) => {
if (!isPointerDown) return;
isPointerDown = false;
if (!isDragging) return;
isDragging = false;
track.style.transition = '';
const dx = e.clientX - startX;
const threshold = Math.min(80, slideWidth() * 0.2);
if (dx > threshold) prev();
else if (dx < -threshold) next();
else goTo(index, { silent: true });
};
prevBtn?.addEventListener('click', (e) => { e.stopPropagation(); prev(); });
nextBtn?.addEventListener('click', (e) => { e.stopPropagation(); next(); });
viewport?.addEventListener('pointerdown', pointerDown, { passive: true });
viewport?.addEventListener('pointermove', pointerMove, { passive: true });
viewport?.addEventListener('pointerup', pointerUp, { passive: true });
viewport?.addEventListener('pointercancel', pointerUp, { passive: true });
if (opts.pauseOnHover) {
slider?.addEventListener('mouseenter', stopAutoplay);
slider?.addEventListener('mouseleave', startAutoplay);
slider?.addEventListener('focusin', stopAutoplay);
slider?.addEventListener('focusout', startAutoplay);
}
const init = () => {
buildDots();
setupReadMore();
goTo(0, { silent: true });
startAutoplay();
updateButtons();
};
let resizeT = null;
window.addEventListener('resize', () => {
clearTimeout(resizeT);
resizeT = setTimeout(() => {
buildDots();
goTo(index, { silent: true });
setupReadMore();
}, 120);
});
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
setupReadMore();
goTo(index, { silent: true });
}).catch(() => {});
}
init();
})();
</script>
{% schema %}
{
"name": "bpers Testimonials",
"tag": "section",
"class": "bpers-testimonials-section",
"settings": [
{ "type": "text", "id": "heading", "label": "Heading", "default": "Trusted by Luxury Travelers" },
{
"type": "text",
"id": "subheading",
"label": "Subheading (plain text)",
"default": "Discover why discerning guests trust your brand for their luxury stays in Los Angeles and beyond"
},
{
"type": "select",
"id": "layout_mode",
"label": "Layout mode",
"default": "slider",
"options": [
{ "value": "slider", "label": "Slider" },
{ "value": "grid", "label": "Grid" }
]
},
{ "type": "range", "id": "max_width", "label": "Max width", "min": 900, "max": 1600, "step": 50, "default": 1200 },
{ "type": "range", "id": "side_padding", "label": "Side padding", "min": 10, "max": 50, "step": 2, "default": 20 },
{ "type": "range", "id": "header_spacing", "label": "Space under header", "min": 10, "max": 70, "step": 2, "default": 34 },
{ "type": "range", "id": "subtitle_max_width", "label": "Subtitle max width", "min": 400, "max": 1000, "step": 20, "default": 760 },
{ "type": "header", "content": "Columns (also used for slider slides per view)" },
{ "type": "range", "id": "slides_desktop", "label": "Columns/slides (desktop)", "min": 1, "max": 6, "step": 1, "default": 4 },
{ "type": "range", "id": "slides_tablet", "label": "Columns/slides (tablet)", "min": 1, "max": 4, "step": 1, "default": 2 },
{ "type": "range", "id": "slides_mobile", "label": "Columns/slides (mobile)", "min": 1, "max": 3, "step": 1, "default": 1 },
{ "type": "range", "id": "card_gap", "label": "Gap between cards", "min": 10, "max": 40, "step": 1, "default": 28 },
{ "type": "range", "id": "transition_ms", "label": "Transition (ms)", "min": 120, "max": 900, "step": 20, "default": 360 },
{ "type": "checkbox", "id": "show_arrows", "label": "Show arrows (slider only)", "default": true },
{ "type": "checkbox", "id": "show_dots", "label": "Show dots (slider only)", "default": false },
{ "type": "checkbox", "id": "autoplay", "label": "Autoplay (slider only)", "default": false },
{ "type": "range", "id": "autoplay_interval", "label": "Autoplay interval (seconds)", "min": 2, "max": 12, "step": 1, "default": 5 },
{ "type": "checkbox", "id": "pause_on_hover", "label": "Pause autoplay on hover/focus (slider only)", "default": true },
{ "type": "checkbox", "id": "loop", "label": "Loop slides (slider only)", "default": true },
{ "type": "checkbox", "id": "aria_announce", "label": "Announce slide changes (a11y)", "default": true },
{ "type": "header", "content": "Card" },
{ "type": "range", "id": "card_padding", "label": "Card padding (Y)", "min": 14, "max": 44, "step": 1, "default": 22 },
{ "type": "range", "id": "card_padding_x", "label": "Card padding (X)", "min": 14, "max": 44, "step": 1, "default": 22 },
{ "type": "range", "id": "card_radius", "label": "Card radius", "min": 6, "max": 28, "step": 1, "default": 8 },
{ "type": "range", "id": "card_border_width", "label": "Card border width", "min": 0, "max": 3, "step": 1, "default": 0 },
{ "type": "color", "id": "card_border_color", "label": "Card border color", "default": "#E5E5E5" },
{ "type": "text", "id": "card_shadow", "label": "Card shadow (CSS)", "default": "0 10px 24px rgba(0,0,0,.06)" },
{ "type": "range", "id": "avatar_size", "label": "Avatar size", "min": 48, "max": 100, "step": 2, "default": 64 },
{ "type": "header", "content": "Stars / Verified" },
{ "type": "checkbox", "id": "show_stars", "label": "Show stars", "default": true },
{ "type": "checkbox", "id": "show_verified_icon", "label": "Show blue verified icon next to stars", "default": true },
{ "type": "checkbox", "id": "show_small_badge", "label": "Show small badge on avatar (like screenshot)", "default": true },
{ "type": "header", "content": "Read more" },
{ "type": "range", "id": "clamp_lines", "label": "Clamped lines", "min": 2, "max": 10, "step": 1, "default": 4 },
{ "type": "text", "id": "read_more_label", "label": "Read more label", "default": "Read more" },
{ "type": "text", "id": "read_less_label", "label": "Read less label", "default": "Read less" },
{ "type": "header", "content": "Colors" },
{ "type": "color", "id": "section_bg", "label": "Section background", "default": "#FFFFFF" },
{ "type": "color", "id": "card_bg", "label": "Card background", "default": "#F3F3F3" },
{ "type": "color", "id": "text_color", "label": "Text color", "default": "#111111" },
{ "type": "color", "id": "muted_color", "label": "Muted text", "default": "#7A7A7A" },
{ "type": "color", "id": "star_color", "label": "Star color", "default": "#F5B301" },
{ "type": "color", "id": "verified_color", "label": "Verified icon color", "default": "#1A73E8" },
{ "type": "color", "id": "initial_bg", "label": "Initial circle background", "default": "#2F6B2F" },
{ "type": "color", "id": "initial_text", "label": "Initial circle text", "default": "#FFFFFF" },
{ "type": "color", "id": "arrow_bg", "label": "Arrow background", "default": "#FFFFFF" },
{ "type": "color", "id": "arrow_color", "label": "Arrow color", "default": "#111111" },
{ "type": "color", "id": "dot_active", "label": "Active dot color", "default": "#111111" },
{ "type": "header", "content": "Section spacing" },
{ "type": "range", "id": "padding_top", "label": "Padding top", "min": 0, "max": 120, "step": 4, "default": 40 },
{ "type": "range", "id": "padding_bottom", "label": "Padding bottom", "min": 0, "max": 120, "step": 4, "default": 40 }
],
"blocks": [
{
"type": "testimonial",
"name": "Testimonial",
"settings": [
{ "type": "image_picker", "id": "image", "label": "Client image (optional)" },
{ "type": "text", "id": "name", "label": "Client name", "default": "Client Name" },
{ "type": "text", "id": "date", "label": "Date label", "default": "4 months ago" },
{ "type": "range", "id": "rating", "label": "Rating", "min": 1, "max": 5, "step": 1, "default": 5 },
{
"type": "richtext",
"id": "content",
"label": "Testimonial content",
"default": "<p>I was in LA for 6 weeks and didn’t want a hotel. The service was smooth, the property was spotless, and everything felt five-star.</p>"
}
]
}
],
"presets": [
{
"name": "bpers Testimonials (Slider/Grid)",
"blocks": [
{ "type": "testimonial" },
{ "type": "testimonial" },
{ "type": "testimonial" },
{ "type": "testimonial" }
]
}
]
}
{% endschema %}
