<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"
}
]
}
// 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;
}
}
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')
})
}
}
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.