Adding Dynamic UiBlocks as children for custom component

I am creating a carousel component where I want to give user option to drag drop component in carousel container from builder or also in code too. I added list of uiBlocks as input but when I drag and drop any other component page gets blank. component registration code as below:

Builder.registerComponent(Carousel, {
  name: 'Carousel',
  canHaveChildren: true,
  inputs: [
    {
      name: 'useBuilderComponents',
      type: 'boolean',
      defaultValue: false,
      friendlyName: 'Use Manual Slides Mode',
      helperText: 'When enabled, add slide components manually as children. When disabled, use the slides list below.',
    },
    {
      name: 'slides',
      type: 'list',
      showIf: 'options.get("useBuilderComponents") !== true',
      helperText: 'Configure slides with content - each slide will be a container for components',
      defaultValue: [
        {
          slideTitle: 'Slide 1',
          slideContent: [],
        },
        {
          slideTitle: 'Slide 2',
          slideContent: [],
        },
      ],
      subFields: [
        {
          name: 'slideTitle',
          type: 'string',
          defaultValue: 'Slide Title',
          helperText: 'Title for this slide (for reference)',
        },
        {
          name: 'slideContent',
          type: 'uiBlocks',
          hideFromUI: false,
          defaultValue: [],
          helperText: 'Drop components here for this slide',
        },
      ],
    },
    {
      name: 'autoplay',
      type: 'boolean',
      defaultValue: false,
      helperText: 'Enable automatic slideshow',
    },
    {
      name: 'autoplaySpeed',
      type: 'number',
      defaultValue: 3000,
      min: 1000,
      max: 10000,
      helperText: 'Autoplay interval in milliseconds',
      showIf: 'options.get("autoplay") === true',
    },
    {
      name: 'showArrows',
      type: 'boolean',
      defaultValue: true,
      helperText: 'Show navigation arrows',
    },
    {
      name: 'showDots',
      type: 'boolean',
      defaultValue: true,
      helperText: 'Show dot indicators',
    },
    {
      name: 'infinite',
      type: 'boolean',
      defaultValue: true,
      helperText: 'Enable infinite loop',
    },
    {
      name: 'slidesToShow',
      type: 'number',
      defaultValue: 1,
      min: 1,
      max: 4,
      helperText: 'Number of slides to show at once',
    },
    {
      name: 'slidesToScroll',
      type: 'number',
      defaultValue: 1,
      min: 1,
      max: 4,
      helperText: 'Number of slides to scroll at once',
    },
    {
      name: 'effect',
      type: 'string',
      enum: [
        { label: 'Slide', value: 'slide' },
        { label: 'Fade', value: 'fade' },
      ],
      defaultValue: 'slide',
      helperText: 'Transition effect between slides',
    },
    {
      name: 'height',
      type: 'string',
      enum: [
        { label: 'Small (200px)', value: 'small' },
        { label: 'Medium (300px)', value: 'medium' },
        { label: 'Large (400px)', value: 'large' },
        { label: 'Extra Large (500px)', value: 'extra-large' },
        { label: 'Full Height (100vh)', value: 'full-height' },
      ],
      defaultValue: 'medium',
      helperText: 'Height of the carousel',
    },
    {
      name: 'pauseOnHover',
      type: 'boolean',
      defaultValue: true,
      helperText: 'Pause autoplay when hovering',
      showIf: 'options.get("autoplay") === true',
    },
    {
      name: 'swipeable',
      type: 'boolean',
      defaultValue: true,
      helperText: 'Enable touch/swipe gestures',
    },
    {
      name: 'speed',
      type: 'number',
      defaultValue: 300,
      min: 100,
      max: 1000,
      helperText: 'Speed of transitions in milliseconds',
    },
    {
      name: 'initialSlide',
      type: 'number',
      defaultValue: 0,
      min: 0,
      helperText: 'Starting slide index',
    },
    {
      name: 'ariaLabel',
      type: 'string',
      defaultValue: 'Carousel',
      helperText: 'Accessibility label for screen readers',
    },
    {
      name: 'testId',
      type: 'string',
      helperText: 'Test ID for automated testing (optional)',
    },
  ],
  childRequirements: {
    message: 'You can drag and drop any components here to create carousel slides',
    query: {},
  },
})




And Carousel Component is as below:

export const Carousel: React.FC<CarouselProps> = ({
  useBuilderComponents = false,
  slides = [],
  builderBlock,
  children,
  autoplay = false,
  autoplaySpeed = 3000,
  showArrows = true,
  showDots = true,
  infinite = true,
  slidesToShow = 1,
  slidesToScroll = 1,
  effect = 'slide',
  height = 'medium',
  pauseOnHover = true,
  swipeable = true,
  speed = 300,
  initialSlide = 0,
  className = '',
  ariaLabel = 'Carousel',
  testId,
}) => {
  const [currentSlide, setCurrentSlide] = useState(initialSlide)
  const [isAutoplayPaused, setIsAutoplayPaused] = useState(false)
  const [isTransitioning, setIsTransitioning] = useState(false)
  const [isDragging, setIsDragging] = useState(false)
  const [dragStart, setDragStart] = useState(0)
  const [dragOffset, setDragOffset] = useState(0)

  const carouselRef = useRef<HTMLDivElement>(null)
  const wrapperRef = useRef<HTMLDivElement>(null)
  const autoplayRef = useRef<NodeJS.Timeout | null>(null)
  const touchStartRef = useRef(0)

  // Determine slide data based on mode
  const slideData = useBuilderComponents ? React.Children.toArray(children) : slides || []
  const totalSlides = slideData.length
  const maxSlide = infinite ? totalSlides : Math.max(0, totalSlides - slidesToShow)

  

  

  

  
 

  
  

  

  

  if (!totalSlides || totalSlides === 0) {
    return (
      <div className={carouselClasses} data-testid={testId}>
        <div className="carousel__container">
          <div className="carousel__empty">No slides to display</div>
        </div>
      </div>
    )
  }
  console.log('Carousel Debug:', {
    builderBlock: builderBlock?.id,
    slidesCount: slides?.length,
    childrenCount: React.Children.count(children),
    rawSlides: slides,
    mode: useBuilderComponents ? 'Manual Children' : 'Slides List',
    slideData: useBuilderComponents
      ? React.Children.toArray(children).map((child, idx) => ({
          index: idx,
          type: 'Manual Child',
        }))
      : slides?.map((slide, idx) => ({
          index: idx,
          slideTitle: slide.slideTitle,
          hasSlideContent: !!slide.slideContent,
          slideContentCount: slide.slideContent?.length || 0,
        })),
    useBuilderComponents,
  })
  return (
    <div
      ref={carouselRef}
      className={carouselClasses}
      role="region"
      aria-label={ariaLabel}
      aria-live="polite"
      tabIndex={0}
      onKeyDown={handleKeyDown}
      data-testid={testId}
    >
      <div className="carousel__container">
        <div
          ref={wrapperRef}
          className={`carousel__wrapper ${isDragging ? 'carousel__wrapper--no-transition' : ''}`}
          style={wrapperStyle}
          onTouchStart={handleTouchStart}
          onTouchMove={handleTouchMove}
          onTouchEnd={handleTouchEnd}
          onMouseDown={handleMouseDown}
          onMouseMove={handleMouseMove}
          onMouseUp={handleMouseUp}
        >
          {slideData.map((slide, index) => {
            const slideKey = useBuilderComponents ? `manual-slide-${index}` : `builder-slide-${index}`

            return (
              <div
                key={slideKey}
                className={`carousel__slide ${
                  effect === 'fade' && index === currentSlide ? 'carousel__slide--active' : ''
                }`}
                aria-hidden={effect === 'fade' ? index !== currentSlide : false}
              >
                {useBuilderComponents ? (
                  // Render manual React children when useBuilderComponents = true
                  React.isValidElement(slide) ? (
                    React.cloneElement(slide as React.ReactElement<any>, {
                      'data-slide-index': index,
                      key: slideKey,
                    })
                  ) : (
                    <div data-slide-index={index}>{slide as React.ReactNode}</div>
                  )
                ) : (
                  // Render Builder.io UI blocks from slides list when useBuilderComponents = false
                  <div className="carousel__slide-content" key={`slide-content-${index}`}>
                    {(slide as any)?.slideContent && (slide as any).slideContent.length > 0 ? (
                      <BuilderBlocks
                        parentElementId={builderBlock.id}
                        dataPath={`slides.${index}.slideContent`}
                        blocks={(slide as any).slideContent}
                      />
                    ) : (
                      <div className="carousel__slide-placeholder">
                        <p>{(slide as any)?.slideTitle || `Slide ${index + 1}`}</p>
                        <small>Drop components here</small>
                      </div>
                    )}
                  </div>
                )}
              </div>
            )
          })}
        </div>

        {/* Navigation Arrows */}
        {showArrows && totalSlides > 1 && (
          <>
            <button
              className={`carousel__nav carousel__nav--prev ${!canGoPrev ? 'carousel__nav--disabled' : ''}`}
              onClick={prevSlide}
              disabled={!canGoPrev}
              aria-label="Previous slide"
              type="button"
            >
              <svg className="carousel__nav-icon" viewBox="0 0 24 24">
                <path d="M15 18l-6-6 6-6v12z" />
              </svg>
            </button>
            <button
              className={`carousel__nav carousel__nav--next ${!canGoNext ? 'carousel__nav--disabled' : ''}`}
              onClick={nextSlide}
              disabled={!canGoNext}
              aria-label="Next slide"
              type="button"
            >
              <svg className="carousel__nav-icon" viewBox="0 0 24 24">
                <path d="M9 6l6 6-6 6V6z" />
              </svg>
            </button>
          </>
        )}

        {/* Dot Indicators */}
        {showDots && totalSlides > 1 && (
          <div className="carousel__dots" role="tablist">
            {Array.from({ length: Math.ceil(totalSlides / slidesToScroll) }).map((_, index) => (
              <button
                key={index}
                className={`carousel__dot ${
                  Math.floor(currentSlide / slidesToScroll) === index ? 'carousel__dot--active' : ''
                }`}
                onClick={() => goToSlide(index * slidesToScroll)}
                aria-label={`Go to slide ${index + 1}`}
                role="tab"
                aria-selected={Math.floor(currentSlide / slidesToScroll) === index}
                type="button"
              />
            ))}
          </div>
        )}
      </div>
    </div>
  )
}

export default Carousel

What I am missing due to which it breaking the page. Please provide some solution for the same.

Hello @Vinay

You may find help at the following article Adding Children to Custom Components

Let us know if the above doesn’t help!

Thanks,

hi @manish-sharma I already checked that but its for static number of ui blocks and fixed component. But in my scenario I want dynamic number of ui-blocks can be added and properly mapped with builder blocks in carousel. Also for right now I want any component can be mapped to thoe uiblocks. If you have any idea how to achieve that will be helpful.

@Vinay

“Would it be possible to use subfields to add a dynamic number of uiBlocks?

Here’s an example we tested:

"use client";
import { 
  Builder, 
  BuilderBlocks, 
  type BuilderElement 
} from '@builder.io/react';

// Define the component
type ColumnData = {
  blocks: BuilderElement[];
};

type BuilderProps = {
  columns: ColumnData[];
  builderBlock: BuilderElement;
};

const CustomColumns = (props: BuilderProps) => {
  const columns = props.columns || [];
  
  return (
    <div style={{ display: 'flex', gap: '20px', width: '100%' }}>
      {columns.map((column, index) => (
        <div key={index} style={{ flex: 1 }}>
          <BuilderBlocks
            parentElementId={props.builderBlock.id}
            dataPath={`columns.${index}.blocks`}
            blocks={column?.blocks || []}
          />
        </div>
      ))}
    </div>
  );
};

// Register with Builder
Builder.registerComponent(CustomColumns, {
  name: 'DynamicColumns',
  canHaveChildren: true,
  inputs: [
    {
      name: 'columns',
      type: 'list',
      defaultValue: [
        { blocks: [] },
        { blocks: [] }
      ],
      subFields: [
        {
          name: 'blocks',
          type: 'uiBlocks',
          defaultValue: {
            blocks: [],
          },
        },
      ],
    },
    {
      name: 'gap',
      type: 'string',
      defaultValue: '20px',
      helperText: 'Gap between columns (e.g., 20px, 1rem)',
    },
    {
      name: 'columnWidth',
      type: 'string',
      enum: [
        { label: 'Equal Width', value: 'equal' },
        { label: 'Auto Width', value: 'auto' },
        { label: 'Custom Width', value: 'custom' },
      ],
      defaultValue: 'equal',
      helperText: 'How columns should be sized',
    },
  ],
  childRequirements: {
    message: 'You can drag and drop any components here to create column content',
    query: {},
  },
});

export default CustomColumns;