Testimonials using Data models

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!