I have stored a list of testimonials in the testimonials data model and accessed then on the page through data binding. I want to display 4 testimonials per carousel slide, so that 12 testimonials appear across 3 slides. How can I show 4 testimonials on each carousel slide?
Hello @Dhanashree
Are you currently using the Builder Carousel component to display your testimonials?
If so, could you please share the Builder content link where you are testing this so we can take a closer look?
Hello @manish-sharma
Yes. I am currently using the Builder Carousel component.
Here is the content link : https://builder.io/content/d8a77e28733b46e3923e66f5a899c702_f2d97cd8207f4c659940d135a795e52c
Thank You!
Hello @Dhanashree
We reviewed your content and identified an issue with the carousel width. This has now been fixed using the following CSS:
.builder-carousel{
width: 100%;
}
Additionally, we updated the text binding code to:
state.resultsItem.data.displayName
This resolved the issue with carousel items not displaying correctly.
If you’d like to display four items in a single slide, you can configure this by using the Responsive option under Advanced Options in the Carousel settings.
Hope this helps!
Thanks,
@manish-sharma Thanks !
Could you please suggest that How can I display the name in one below another, not in a single row?
e.g: first David A below that Johns B, then the remaining two names . This way 4 names in one slide remaining 4 on other like that.
Hello @Dhanashree,
With the Builder carousel, it is possible to override the CSS to achieve this. However, a more robust approach would be to use a custom component solution instead.
Here is an example for your reference:
"use client";
import React, { useState, useEffect } from 'react';
import { BuilderContent } from '@builder.io/sdk-react-nextjs';
interface SampleItem {
id: string;
title: string;
category: string;
price: string;
image: string;
description: string;
}
interface VerticalCarouselProps {
items?: BuilderContent[];
itemsPerSlide?: number;
title?: string;
subtitle?: string;
showNavigation?: boolean;
autoPlay?: boolean;
autoPlayInterval?: number;
className?: string;
useSampleData?: boolean;
}
const VerticalCarousel: React.FC<VerticalCarouselProps> = ({
items = [],
itemsPerSlide = 4,
title = "Vertical Carousel",
subtitle,
showNavigation = true,
autoPlay = false,
autoPlayInterval = 5000,
className = "",
useSampleData = false
}) => {
// Sample data with 12 items
const sampleItems: SampleItem[] = [
{
id: "1",
title: "Laptop Pro X1",
category: "Electronics",
price: "1,299.99",
image: "https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=200&h=200&fit=crop",
description: "High-performance laptop for professionals"
},
{
id: "2",
title: "Wireless Headphones",
category: "Audio",
price: "199.99",
image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=200&h=200&fit=crop",
description: "Premium noise-canceling headphones"
},
{
id: "3",
title: "Smart Watch Series 5",
category: "Wearables",
price: "399.99",
image: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=200&h=200&fit=crop",
description: "Advanced fitness and health tracking"
},
{
id: "4",
title: "4K Gaming Monitor",
category: "Displays",
price: "599.99",
image: "https://images.unsplash.com/photo-1547082299-de196ea013d6?w=200&h=200&fit=crop",
description: "Ultra-high definition gaming experience"
},
{
id: "5",
title: "Mechanical Keyboard",
category: "Accessories",
price: "149.99",
image: "https://images.unsplash.com/photo-1541140532154-b024d705b90a?w=200&h=200&fit=crop",
description: "Professional mechanical switches"
},
{
id: "6",
title: "Gaming Mouse",
category: "Accessories",
price: "79.99",
image: "https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?w=200&h=200&fit=crop",
description: "High-precision gaming mouse"
},
{
id: "7",
title: "Bluetooth Speaker",
category: "Audio",
price: "89.99",
image: "https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=200&h=200&fit=crop",
description: "Portable wireless speaker"
},
{
id: "8",
title: "Tablet Pro",
category: "Electronics",
price: "799.99",
image: "https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=200&h=200&fit=crop",
description: "Professional tablet for creative work"
},
{
id: "9",
title: "Webcam HD",
category: "Accessories",
price: "129.99",
image: "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=200&h=200&fit=crop",
description: "High-definition video calling"
},
{
id: "10",
title: "External SSD",
category: "Storage",
price: "199.99",
image: "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=200&h=200&fit=crop",
description: "Ultra-fast portable storage"
},
{
id: "11",
title: "USB-C Hub",
category: "Accessories",
price: "49.99",
image: "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=200&h=200&fit=crop",
description: "Multi-port connectivity solution"
},
{
id: "12",
title: "Wireless Charger",
category: "Accessories",
price: "39.99",
image: "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=200&h=200&fit=crop",
description: "Fast wireless charging pad"
}
];
// Use sample data if no items provided or if useSampleData is true
const displayItems = useSampleData || items.length === 0 ? sampleItems : items;
const [currentSlide, setCurrentSlide] = useState(0);
const totalSlides = Math.ceil(displayItems.length / itemsPerSlide);
// Auto-play functionality
useEffect(() => {
if (!autoPlay || totalSlides <= 1) return;
const interval = setInterval(() => {
setCurrentSlide((prev) => (prev + 1) % totalSlides);
}, autoPlayInterval);
return () => clearInterval(interval);
}, [autoPlay, autoPlayInterval, totalSlides]);
// Reset to first slide when items change
useEffect(() => {
setCurrentSlide(0);
}, [items]);
const goToSlide = (slideIndex: number) => {
setCurrentSlide(slideIndex);
};
const goToNextSlide = () => {
setCurrentSlide((prev) => (prev + 1) % totalSlides);
};
const goToPrevSlide = () => {
setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides);
};
const getCurrentSlideItems = () => {
const startIndex = currentSlide * itemsPerSlide;
const endIndex = startIndex + itemsPerSlide;
return displayItems.slice(startIndex, endIndex);
};
if (!displayItems || displayItems.length === 0) {
return (
<div className={`text-center py-12 ${className}`}>
<p className="text-gray-600">No items to display.</p>
</div>
);
}
return (
<div className={`py-8 px-4 ${className}`}>
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2">{title}</h2>
{subtitle && (
<p className="text-xl text-gray-600">{subtitle}</p>
)}
</div>
{/* Carousel Container */}
<div className="relative">
{/* Main Carousel */}
<div className="bg-white rounded-lg shadow-lg p-6 min-h-[400px]">
<div className="grid grid-cols-1 gap-4">
{getCurrentSlideItems().map((item, index) => {
// Handle BuilderContent items
if ('data' in item && item.data) {
return (
<div
key={item.id || `item-${index}`}
className="flex items-center space-x-4 p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow duration-200"
>
{/* Item Image */}
{item.data.productImage && (
<div className="flex-shrink-0">
<img
src={item.data.productImage}
alt={item.data.title || 'Product'}
className="w-16 h-16 object-cover rounded-md"
/>
</div>
)}
{/* Item Info */}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{item.data.title || 'Untitled Item'}
</h3>
{item.data.category && (
<p className="text-sm text-blue-600">
{item.data.category}
</p>
)}
{item.data.specification?.desktop?.aspectRatio && (
<p className="text-sm text-gray-600">
Aspect Ratio: {item.data.specification.desktop.aspectRatio}
</p>
)}
</div>
{/* Price */}
<div className="flex-shrink-0">
<span className="text-xl font-bold text-green-600">
${item.data.price || '0.00'}
</span>
</div>
</div>
);
}
// Handle SampleItem items
if ('title' in item) {
return (
<div
key={item.id}
className="flex items-center space-x-4 p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow duration-200"
>
{/* Item Image */}
<div className="flex-shrink-0">
<img
src={item.image}
alt={item.title}
className="w-16 h-16 object-cover rounded-md"
/>
</div>
{/* Item Info */}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{item.title}
</h3>
<p className="text-sm text-blue-600">
{item.category}
</p>
<p className="text-sm text-gray-600">
{item.description}
</p>
</div>
{/* Price */}
<div className="flex-shrink-0">
<span className="text-xl font-bold text-green-600">
${item.price}
</span>
</div>
</div>
);
}
return null;
})}
</div>
</div>
{/* Navigation Arrows */}
{showNavigation && totalSlides > 1 && (
<>
{/* Previous Button */}
<button
onClick={goToPrevSlide}
className="absolute left-0 top-1/2 transform -translate-y-1/2 -translate-x-12 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-shadow duration-200"
aria-label="Previous slide"
>
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Next Button */}
<button
onClick={goToNextSlide}
className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-12 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-shadow duration-200"
aria-label="Next slide"
>
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</>
)}
</div>
{/* Slide Indicators */}
{totalSlides > 1 && (
<div className="flex justify-center mt-6 space-x-2">
{Array.from({ length: totalSlides }, (_, index) => (
<button
key={index}
onClick={() => goToSlide(index)}
className={`w-3 h-3 rounded-full transition-colors duration-200 ${
index === currentSlide
? 'bg-blue-600'
: 'bg-gray-300 hover:bg-gray-400'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
)}
{/* Slide Info */}
<div className="text-center mt-4 text-sm text-gray-600">
Slide {currentSlide + 1} of {totalSlides}
{displayItems.length > 0 && (
<span className="ml-2">
(Showing {getCurrentSlideItems().length} of {displayItems.length} items)
</span>
)}
</div>
</div>
</div>
);
};
export default VerticalCarousel;
@manish-sharma Thanks! for your response.
I would like to know if there is an option to achieve this using Builder only, without code or minimal coding?
Hello @Dhanashree,
It may be possible to achieve this with custom CSS, but I’d recommend a simpler approach using Builder’s AI Generate option. You can quickly create a carousel with AI, and then save it as a Symbol for easy reuse across different content.
This way, you can minimize coding while still maintaining flexibility and consistency.
Thanks,
@manish-sharma
Thank You!