<quote-carousel aria-roledescription="Karussell" aria-label="Kundenstimmen">
    <div class="quote-carousel__track" data-carousel-track role="group">
        <div class="quote-carousel__slide" role="tabpanel" aria-roledescription="Folie" aria-label="Zitat 0 von 3">
            <long-quote>
                <blockquote class="longquote">
                    <span class="longquote__zigzag"></span>
                    <p>Mit INNOQ haben wir einen innovativen Partner gefunden, der uns mit seiner kompetenten Beratung und professionellen Umsetzung eine wichtige Stütze unseres Geschäfts ist.</p>
                    <cite class="longquote__author" itemprop="name">Heribert Innoq</cite>
                    <span class="longquote__role" itemprop="jobTitle">Senior Consultant</span>
                </blockquote>
            </long-quote>
        </div>
        <div class="quote-carousel__slide" role="tabpanel" aria-roledescription="Folie" aria-label="Zitat 1 von 3">
            <long-quote>
                <blockquote class="longquote">
                    <span class="longquote__zigzag"></span>
                    <p>Die Zusammenarbeit mit INNOQ war von Anfang an durch hohe Professionalität und partnerschaftliches Miteinander geprägt.</p>
                    <cite class="longquote__author" itemprop="name">Maria Beispiel</cite>
                    <span class="longquote__role" itemprop="jobTitle">CTO, Beispiel GmbH</span>
                </blockquote>
            </long-quote>
        </div>
        <div class="quote-carousel__slide" role="tabpanel" aria-roledescription="Folie" aria-label="Zitat 2 von 3">
            <long-quote>
                <blockquote class="longquote">
                    <span class="longquote__zigzag"></span>
                    <p>INNOQ hat uns dabei geholfen, unsere Systemlandschaft nachhaltig zu modernisieren und zukunftssicher aufzustellen.</p>
                    <cite class="longquote__author" itemprop="name">Thomas Muster</cite>
                    <span class="longquote__role" itemprop="jobTitle">Head of IT, Muster AG</span>
                </blockquote>
            </long-quote>
        </div>
    </div>
</quote-carousel>
<quote-carousel aria-roledescription="Karussell" aria-label="Kundenstimmen">
    <div class="quote-carousel__track" data-carousel-track role="group">
        {{#each quotes}}
        <div class="quote-carousel__slide" role="tabpanel" aria-roledescription="Folie" aria-label="Zitat {{@index}} von {{@root.quotes.length}}">
            <long-quote>
                <blockquote class="longquote">
                    <span class="longquote__zigzag"></span>
                    <p>{{this.text}}</p>
                    <cite class="longquote__author" itemprop="name">{{this.author}}</cite>
                    <span class="longquote__role" itemprop="jobTitle">{{this.role}}</span>
                </blockquote>
            </long-quote>
        </div>
        {{/each}}
    </div>
</quote-carousel>
{
  "quotes": [
    {
      "text": "Mit INNOQ haben wir einen innovativen Partner gefunden, der uns mit seiner kompetenten Beratung und professionellen Umsetzung eine wichtige Stütze unseres Geschäfts ist.",
      "author": "Heribert Innoq",
      "role": "Senior Consultant"
    },
    {
      "text": "Die Zusammenarbeit mit INNOQ war von Anfang an durch hohe Professionalität und partnerschaftliches Miteinander geprägt.",
      "author": "Maria Beispiel",
      "role": "CTO, Beispiel GmbH"
    },
    {
      "text": "INNOQ hat uns dabei geholfen, unsere Systemlandschaft nachhaltig zu modernisieren und zukunftssicher aufzustellen.",
      "author": "Thomas Muster",
      "role": "Head of IT, Muster AG"
    }
  ]
}
  • Content:
    // Quote Carousel – CSS Scroll Snap based
    .quote-carousel__track {
      scroll-behavior: smooth;
      scrollbar-width: none;
      scroll-snap-type: x mandatory;
    
      overflow-x: auto;
      display: flex;
      gap: $spacer-lg;
    
      -webkit-overflow-scrolling: touch;
    
      &::-webkit-scrollbar {
        display: none;
      }
    }
    
    .quote-carousel__slide {
      scroll-snap-align: center;
      flex: 0 0 100%;
    }
    
    .quote-carousel__nav {
      display: flex;
      gap: $spacer-sm;
      align-items: center;
      justify-content: center;
    
      margin-top: $spacer-base;
    }
    
    .quote-carousel__btn {
      cursor: pointer;
    
      display: flex;
      align-items: center;
      justify-content: center;
    
      width: 2.5rem;
      height: 2.5rem;
    
      font-size: 1.25rem;
      line-height: 1;
      color: $brand-secondary;
    
      appearance: none;
      background: none;
      border: 2px solid $brand-secondary;
      border-radius: 50%;
    
      transition: opacity 0.2s;
    
      &:hover:not(:disabled) {
        color: $brand-white;
        background-color: $brand-secondary;
      }
    
      &:disabled {
        cursor: default;
        opacity: 0.3;
      }
    }
    
    .quote-carousel__dots {
      display: flex;
      gap: $spacer-xxs;
    }
    
    .quote-carousel__dot {
      cursor: pointer;
    
      width: 0.625rem;
      height: 0.625rem;
      padding: 0;
    
      appearance: none;
      opacity: 0.3;
      background: $brand-secondary;
      border: 0;
      border-radius: 50%;
    
      transition: opacity 0.2s;
    
      &[aria-selected='true'] {
        opacity: 1;
      }
    }
    
  • URL: /components/raw/quote-carousel/_quote-carousel.scss
  • Filesystem Path: components/04-molecules/quotes/quote-carousel/_quote-carousel.scss
  • Size: 1.4 KB
  • Content:
    export default class QuoteCarousel extends HTMLElement {
      connectedCallback() {
        this.track = this.querySelector('[data-carousel-track]')
        this.items = [...this.track.children]
        if (this.items.length <= 1) return
    
        this.currentIndex = 0
        this.createControls()
        this.observeScroll()
      }
    
      createControls() {
        const nav = document.createElement('nav')
        nav.setAttribute('aria-label', 'Zitate Navigation')
        nav.classList.add('quote-carousel__nav')
    
        const prevBtn = document.createElement('button')
        prevBtn.setAttribute('aria-label', 'Vorheriges Zitat')
        prevBtn.classList.add('quote-carousel__btn', 'quote-carousel__btn--prev')
        prevBtn.textContent = '\u2190'
        prevBtn.addEventListener('click', () => this.go(-1))
    
        const nextBtn = document.createElement('button')
        nextBtn.setAttribute('aria-label', 'Nächstes Zitat')
        nextBtn.classList.add('quote-carousel__btn', 'quote-carousel__btn--next')
        nextBtn.textContent = '\u2192'
        nextBtn.addEventListener('click', () => this.go(1))
    
        const dots = document.createElement('div')
        dots.classList.add('quote-carousel__dots')
        dots.setAttribute('role', 'tablist')
        this.items.forEach((_, i) => {
          const dot = document.createElement('button')
          dot.classList.add('quote-carousel__dot')
          dot.setAttribute('role', 'tab')
          dot.setAttribute('aria-label', `Zitat ${i + 1}`)
          dot.setAttribute('aria-selected', i === 0 ? 'true' : 'false')
          dot.addEventListener('click', () => this.goTo(i))
          dots.appendChild(dot)
        })
    
        nav.appendChild(prevBtn)
        nav.appendChild(dots)
        nav.appendChild(nextBtn)
        this.appendChild(nav)
    
        this.prevBtn = prevBtn
        this.nextBtn = nextBtn
        this.dots = [...dots.children]
        this.updateControls()
      }
    
      observeScroll() {
        const observer = new IntersectionObserver(
          (entries) => {
            for (const entry of entries) {
              if (entry.isIntersecting) {
                this.currentIndex = this.items.indexOf(entry.target)
                this.updateControls()
              }
            }
          },
          { root: this.track, threshold: 0.5 },
        )
        this.items.forEach((item) => observer.observe(item))
      }
    
      go(direction) {
        this.goTo(this.currentIndex + direction)
      }
    
      goTo(index) {
        const target = this.items[Math.max(0, Math.min(index, this.items.length - 1))]
        target.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
      }
    
      updateControls() {
        this.prevBtn.disabled = this.currentIndex === 0
        this.nextBtn.disabled = this.currentIndex === this.items.length - 1
        this.dots.forEach((dot, i) => {
          dot.setAttribute('aria-selected', i === this.currentIndex ? 'true' : 'false')
        })
      }
    }
    
  • URL: /components/raw/quote-carousel/quote-carousel.js
  • Filesystem Path: components/04-molecules/quotes/quote-carousel/quote-carousel.js
  • Size: 2.7 KB
  • Handle: @quote-carousel
  • Preview:
  • Filesystem Path: components/04-molecules/quotes/quote-carousel/quote-carousel.html

CSS Scroll Snap based carousel with progressive JS enhancement. Works as a scrollable container without JavaScript. The <quote-carousel> custom element adds prev/next buttons and dot navigation. Uses IntersectionObserver to track the visible slide.