Angular + Builder.io: How to render 10+ custom cards with arrow navigation (carousel) and make the card internals responsive?

I’m using Angular with @builder.io/sdk-angular and a custom component that we registered in Builder as NewsTileCard. Right now I can drop 3 cards into a grid from Builder and they render fine in my page. I’d like to:

  1. Render 10 (or more) custom cards without duplicating them manually in the editor.

  2. Navigate between them with side arrow buttons (carousel/slider behavior).

  3. Make the card internals responsive (image height, min-height, fonts) with per‑breakpoint styles, not just the container.

What I have

  • A registered custom component NewsTileCard with inputs: imageSrc, title, description.

  • A container component that renders <builder-content> and sets a 3‑column grid with ::ng-deep.

  • Media queries on the container work, but I’m struggling to apply breakpoint styles to the internal card elements from Builder (I’m currently only able to style the wrapper, not the child component internals).

import type { RegisteredComponent } from '@builder.io/sdk-angular';
import { Component, Input } from '@angular/core';

@Component({
  selector: 'lib-builder-news-tile-card',
  standalone: true,
  template: `
    <article class="newsCard">
      <div class="newsCard__imageWrap">
        @if (imageSrc) {
        <img class="newsCard__img" [src]="imageSrc" alt="" />
        } @else {
        <div class="newsCard__imgPlaceholder"></div>
        }
      </div>

      @if (title) {
      <h3 class="newsCard__title">{{ title }}</h3>
      } @if (description) {
      <p class="newsCard__desc">{{ description }}</p>
      }
    </article>
  `,
  styles: [
    `
      .newsCard {
        display: flex;
        width: 100%;
        max-width: none;
        min-height: var(--news-card-min-h, 620px);
        height: 100%;
        padding: 10px 10px 22px 10px;
        flex-direction: column;
        align-items: center;
        gap: 12px;
        border-radius: 16px;
        background: transparent;
        backdrop-filter: blur(10px) saturate(180%);
        -webkit-backdrop-filter: blur(10px) saturate(180%);
        border: 1px solid rgba(255, 255, 255, 0.10);
        box-shadow: 1px 1px 0 0 rgba(255, 255, 255, 0.35) inset;
        box-sizing: border-box;
        transition:
          box-shadow 200ms ease,
          transform 200ms ease,
          background 200ms ease,
          border-color 200ms ease;
      }

      .newsCard:hover {
        transform: translateY(-1px);
        border-color: rgba(255, 255, 255, 0.22);
        background: transparent;
        box-shadow:
          1px 1px 0 0 rgba(255, 255, 255, 0.45) inset,
          0 0 22px rgba(34, 121, 224, 0.35),
          0 0 10px rgba(34, 121, 224, 0.22);
      }

      .newsCard__imageWrap {
        height: var(--news-card-image-h, 100px);
        align-self: stretch;
        border-radius: 12px;
        overflow: hidden;
        background: rgba(255, 255, 255, 0.08);
        position: relative;
      }

      .newsCard__img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
        transition: transform 250ms ease;
      }

      .newsCard:hover .newsCard__img {
        transform: scale(1.02);
      }

      .newsCard__imgPlaceholder {
        width: 100%;
        height: 100%;
        background: #d9d9d9;
      }

      .newsCard__title {
        align-self: stretch;
        margin: 0;
        color: #fff;
        font-family: Inter, sans-serif;
        font-size: 24px;
        font-weight: 500;
        line-height: 24px;
        transition:
          text-shadow 200ms ease,
          opacity 200ms ease;
      }

      .newsCard:hover .newsCard__title {
        text-shadow:
          0 0 18px rgba(34, 121, 224, 0.55),
          0 0 8px rgba(34, 121, 224, 0.35);
      }

      .newsCard__desc {
        align-self: stretch;
        margin: 0;
        color: #fff;
        font-family: Inter, sans-serif;
        font-size: 16px;
        font-weight: 300;
        line-height: 24px;
        max-height: 96px;
        overflow-y: auto;
        overflow-x: hidden;
        padding-right: 8px;
        scrollbar-width: thin;
        scrollbar-color: rgba(255, 255, 255, 0.35) rgba(255, 255, 255, 0.1);
        transition:
          text-shadow 200ms ease,
          opacity 200ms ease;
      }

      .newsCard:hover .newsCard__desc {
        text-shadow: 0 0 10px rgba(34, 121, 224, 0.25);
      }

      .newsCard__desc::-webkit-scrollbar {
        width: 8px;
      }

      .newsCard__desc::-webkit-scrollbar-track {
        background: rgba(255, 255, 255, 0.1);
        border-radius: 999px;
      }

      .newsCard__desc::-webkit-scrollbar-thumb {
        background: rgba(255, 255, 255, 0.35);
        border-radius: 999px;
        border: 2px solid rgba(255, 255, 255, 0.1);
        background-clip: padding-box;
      }

      .newsCard__desc::-webkit-scrollbar-thumb:hover {
        background: rgba(255, 255, 255, 0.55);
      }
    `
  ]
})
export class BuilderNewsTileCardComponent {
  @Input() imageSrc = '';
  @Input() title = '';
  @Input() description = '';
}

export const CUSTOM_COMPONENTS: RegisteredComponent[] = [
  {
    component: BuilderNewsTileCardComponent,
    name: 'NewsTileCard',
    inputs: [
      {
        name: 'imageSrc',
        type: 'file',
        allowedFileTypes: ['jpeg', 'jpg', 'png', 'svg', 'webp']
      },
      { name: 'title', type: 'string', defaultValue: 'Title' },
      { name: 'description', type: 'longText' }
    ]
  }
];

What I’m trying to achieve

  1. Load 10+ cards (ideally from a list input or repeat) instead of adding cards one by one in the editor.

  2. Side arrows to move next/prev (either scroll‑snap or translateX is fine).

  3. Responsive card internals controlled per breakpoint from Builder (e.g., different image height/min-height on mobile), not only with global CSS or container media queries.

Questions

  1. Repeating cards: What’s the best practice in Builder (Angular) to render many instances of a custom component?

    • Could I use a custom wrapper with an items list input registered in Builder?
  2. Carousel/Arrows: Is there a recommended way to add arrow navigation around repeated custom components inside Builder content?

    • Any example of a custom carousel wrapper registered with Builder?
  3. Responsive styling inside the card: Since Builder styles apply to the wrapper element, what’s your recommended pattern to let editors control inner component styles per breakpoint?

    • Should we expose CSS variables (e.g., --news-card-image-h) and let editors set those per breakpoint on the block containing the custom component?

    • Are there other patterns that work well with Angular’s ViewEncapsulation?

Hello @LaviniaGavrilescu21,

The recommended approach here is to register a wrapper component (e.g. NewsCarousel) with a list input instead of dropping NewsTileCard individually in the editor. That way editors manage one block, fill in as many items as they need, and your component handles the rendering:

{
  component: NewsCarouselComponent,
  name: 'NewsCarousel',
  inputs: [
    {
      name: 'items',
      type: 'list',
      subFields: [
        { name: 'imageSrc', type: 'file', allowedFileTypes: ['jpeg', 'jpg', 'png', 'svg', 'webp'] },
        { name: 'title', type: 'string', defaultValue: 'Title' },
        { name: 'description', type: 'longText' },
      ],
    },
    { name: 'visibleCount', type: 'number', defaultValue: 3 },
    { name: 'imageHeight',  type: 'string', defaultValue: '200px' },
    { name: 'cardMinHeight', type: 'string', defaultValue: '620px' },
  ],
}

Arrow navigation
No external library needed for basic carousel behavior. A visibleCount input + slice() on the list gets you there cleanly:

index = 0;

get visibleCards() {
  return this.items.slice(this.index, this.index + this.visibleCount);
}
prev() { this.index = Math.max(0, this.index - 1); }
next() { this.index = Math.min(this.items.length - this.visibleCount, this.index + 1); }

Responsive card internals
This is a common pain point with Angular’s ViewEncapsulation. Builder’s per-breakpoint styles are written to the wrapper element it generates (e.g. .builder-block-xxxxx), so they won’t reach inside your component’s encapsulation boundary with Emulated mode.

The pattern we’d recommend is CSS variables — which you’re already using (--news-card-image-h, --news-card-min-h). Just expose them as string inputs and bind them inline:

@Input() imageHeight = '200px';
@Input() cardMinHeight = '620px';
<article class="newsCard"
  [style.--news-card-image-h]="imageHeight"
  [style.--news-card-min-h]="cardMinHeight">

Editors can then set imageHeight to 200px on desktop and 120px on mobile using the breakpoint switcher in the inputs panel; no CSS knowledge required on their end.

This keeps encapsulation intact and avoids the class collision risk you’d get with ViewEncapsulation.None.

Hope that helps - let us know if you run into anything!