· Webentwicklung · 11 minuten Lesezeit
Produktionsreife Carousel-Komponente mit Astro und TypeScript
Lerne, wie du eine voll ausgestattete, responsive Carousel-Komponente von Grund auf mit Astro und TypeScript entwickelst, ohne externe Abhängigkeiten

Inhalt
- Warum einen eigenen Carousel ohne Abhängigkeiten bauen?
- Live-Demo
- Was wir bauen werden
- Voraussetzungen
- Projektstruktur
- Architektur-Übersicht
- Datenfluss
- Implementierung der Carousel-Komponente
- Verwendung der Carousel-Komponente
- Performance-Optimierungen
- Accessibility Features
- Zusammenfassung
- Weitere Demo-Beispiele
- Fazit
Warum einen eigenen Carousel ohne Abhängigkeiten bauen?
In der modernen Webentwicklung stehen Entwickler oft vor der Wahl: eine externe Bibliothek verwenden oder eine eigene Lösung entwickeln. Bei Carousel-Komponenten gibt es zahlreiche Optionen wie Swiper.js, Slick Carousel oder Embla Carousel. Doch diese Bibliotheken bringen Nachteile mit sich:
- Bundle-Größe: Externe Bibliotheken erhöhen die JavaScript-Bundle-Größe erheblich (oft 20-200 KB zusätzlich)
- Abhängigkeiten: Wartung und Updates der Abhängigkeiten erfordern kontinuierliche Aufmerksamkeit
- Overengineering: Die meisten Projekte benötigen nur einen Bruchteil der angebotenen Features
- Anpassbarkeit: Styling und Verhalten zu überschreiben ist oft kompliziert
- Performance: Ungenutzte Features belasten die Performance
Mit Astro und modernem TypeScript können wir eine schlanke, performante Carousel-Komponente entwickeln, die exakt auf unsere Anforderungen zugeschnitten ist. In diesem Artikel zeige ich Schritt für Schritt, wie man eine produktionsreife Carousel-Komponente ohne externe Abhängigkeiten erstellt.
Live-Demo
Bevor wir in die technischen Details eintauchen, hier eine Live-Demo der fertigen Carousel-Komponente:
Voll ausgestattetes Carousel
Interaktive Demonstration aller Features
Video-Unterstützung mit automatischem Abspielen
Bilder mit automatischem Lazy Loading und responsiven Größen
Smooth Transitions mit CSS Animations
Tastaturnavigation mit Pfeiltasten
Responsive Design für alle Bildschirmgrößen
Tipp: Nutze die Pfeiltasten ←/→, die Tab-Taste, die Vor/Zurück-Buttons oder klicke auf die Indikator-Punkte zur Navigation.
Was wir bauen werden
Unsere Carousel-Komponente wird folgende Features unterstützen:
- Bilder und Videos als Carousel-Items
- Autoplay mit konfigurierbarem Intervall
- Navigation über Buttons (Vor/Zurück)
- Indikator-Punkte für aktuelle Position
- Tastaturnavigation (Pfeiltasten)
- Responsive Design mit konfigurierbaren Seitenverhältnissen
- Smooth Transitions und Animationen
- Accessibility (ARIA-Labels, Keyboard Support)
- Video-Autoplay mit automatischem Pausieren
- Caption-Unterstützung für jedes Item
- TypeScript für Type-Safety
Voraussetzungen
Bevor wir beginnen, solltest du folgende Technologien kennen:
- Astro: Grundlegendes Verständnis von Astro-Komponenten und Props
- TypeScript: Interfaces, Type Annotations und Classes
- Tailwind CSS: Utility-First CSS Framework (optional, aber empfohlen)
- JavaScript: ES6+ Features, DOM-Manipulation, Event Handling
Projektstruktur
Unser Projekt verwendet folgende Struktur:
src/
├── components/
│ ├── ui/
│ │ ├── WidgetWrapper.astro
│ │ ├── Headline.astro
│ │ └── Button.astro
│ ├── common/
│ │ └── Image.astro
│ └── widgets/
│ └── Carousel.astro # Unsere Carousel-Komponente
├── types.d.ts # TypeScript Type Definitions
└── assets/
└── styles/
└── tailwind.cssArchitektur-Übersicht
Unsere Carousel-Komponente besteht aus drei Hauptkomponenten:
- Astro Component (Carousel.astro): Rendering der HTML-Struktur und Einbindung von Child-Components
- TypeScript Class (Carousel): Logik für Navigation, Autoplay und Event Handling
- CSS Transitions: Smooth Animationen zwischen Slides
Datenfluss
Props (items, autoplay, etc.)
↓
Astro Component (Rendering)
↓
HTML Structure mit data-* Attributen
↓
TypeScript Class (Client-Side Logic)
↓
DOM Manipulation und Event HandlingImplementierung der Carousel-Komponente
Um eine vollständige Carousel-Komponente zu entwickeln, denken wir vom Endziel rückwärts. Was brauchen wir alles, damit das Carousel funktioniert?
Wir zerlegen die Entwicklung in einzelne, aufeinander aufbauende Schritte und beginnen vom Typsystem über die Struktur bis zur Interaktivität. So behalten wir den Überblick und können uns vollständig auf eine Aufgabe konzentrieren, bevor wir zur nächsten Schicht übergehen.
Schritt 1: TypeScript Interfaces definieren
Zuerst definieren wir die TypeScript-Interfaces für unsere Carousel-Komponente. Das sorgt für Type-Safety und bessere IDE-Unterstützung.
CarouselItem Interface
// src/types.d.ts
export interface Video {
src: string;
type?: string;
}
export interface Image {
src: string | ImageMetadata;
alt?: string;
}
export interface CarouselItem {
image?: Image | ImageMetadata | string;
video?: Video;
caption?: string;
}Carousel Props Interface
export interface Carousel extends Omit<Headline, 'classes'>, Widget {
items?: Array<CarouselItem>;
autoplay?: boolean;
autoplayInterval?: number;
showIndicators?: boolean;
showControls?: boolean;
aspectRatio?: string;
}Erklärung der Props:
| Prop | Typ | Default | Beschreibung |
|---|---|---|---|
| items | CarouselItem[] | [] | Array von Bildern/Videos mit optionalen Captions |
| autoplay | boolean | false | Aktiviert automatisches Durchlaufen der Slides |
| autoplayInterval | number | 5000 | Intervall in Millisekunden zwischen Slides |
| showIndicators | boolean | true | Zeigt Indikator-Punkte am unteren Rand |
| showControls | boolean | true | Zeigt Vor/Zurück-Buttons |
| aspectRatio | string | ’1/1’ | CSS aspect-ratio Wert (z.B. ‘16/9’, ‘4/3’) |
Schritt 2: HTML-Struktur erstellen
Jetzt erstellen wir die grundlegende HTML-Struktur unserer Carousel-Komponente.
---
// src/components/widgets/Carousel.astro
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
import Headline from '~/components/ui/Headline.astro';
import Image from '~/components/common/Image.astro';
import Button from '~/components/ui/Button.astro';
import { twMerge } from 'tailwind-merge';
const {
title = await Astro.slots.render('title'),
subtitle = await Astro.slots.render('subtitle'),
tagline = await Astro.slots.render('tagline'),
items = [],
autoplay = false,
autoplayInterval = 5000,
showIndicators = true,
showControls = true,
aspectRatio = '1/1',
id,
isDark = false,
classes = {},
bg = await Astro.slots.render('bg'),
} = Astro.props;
const carouselId = id || `carousel-${Math.random().toString(36).substr(2, 9)}`;
---
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
{
(title || subtitle || tagline) && (
<Headline
title={title}
subtitle={subtitle}
tagline={tagline}
classes={{
container: 'text-center mb-8 md:mb-12',
title: 'text-4xl md:text-5xl font-bold tracking-tighter mb-4 font-heading',
subtitle: 'text-xl text-muted dark:text-slate-400',
}}
/>
)
}
<div class="relative w-full mx-auto group" data-carousel={carouselId}>
<!-- Carousel container -->
<div
class="relative w-full overflow-hidden rounded-lg shadow-lg bg-gray-500 dark:bg-slate-700"
style={`aspect-ratio: ${aspectRatio};`}
>
<!-- Slides werden hier eingefügt -->
</div>
<!-- Controls und Indicators folgen -->
</div>
</WidgetWrapper>Wichtige Designentscheidungen
- data-carousel Attribut: Eindeutige ID für die JavaScript-Initialisierung
- aspect-ratio: Dynamisches Seitenverhältnis über CSS Custom Properties
- group: Tailwind CSS Klasse für hover-basierte Control-Anzeige
- WidgetWrapper: Wiederverwendbarer Container für konsistentes Layout
Schritt 3: Carousel Items rendern
Jetzt fügen wir die einzelnen Slides innerhalb des Carousel-Containers ein:
<div
class="relative w-full overflow-hidden rounded-lg shadow-lg bg-gray-500 dark:bg-slate-700"
style={`aspect-ratio: ${aspectRatio};`}
>
{
items.map((item, index) => (
<div
class={twMerge(
'carousel-item absolute inset-0 transition-all duration-500 ease-in-out',
index === 0 ? 'opacity-100 scale-100 z-10' : 'opacity-0 scale-105 z-0'
)}
data-carousel-item={index}
>
{/* Bild-Rendering */}
{item.image &&
(typeof item.image === 'string' ? (
<Fragment set:html={item.image} />
) : (
<Image
class="w-full h-full object-contain"
widths={[400, 768, 1024, 2040]}
sizes="(max-width: 767px) 100vw, (max-width: 1023px) 50vw, 33vw"
loading="lazy"
alt={`Slide ${index + 1}`}
{...item.image}
/>
))}
{/* Video-Rendering */}
{item.video && (
<video class="w-full h-full object-contain" muted loop playsinline>
<source src={item.video.src} type={item.video.type || 'video/mp4'} />
</video>
)}
{/* Caption */}
{item.caption && (
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent px-6 py-12 md:px-10 md:py-14">
<p class="text-white text-center text-base md:text-lg lg:text-xl font-medium leading-relaxed max-w-4xl">
{item.caption}
</p>
</div>
)}
</div>
))
}
</div>CSS-Klassen Erklärung
| Klasse | Zweck |
|---|---|
absolute inset-0 | Positioniert alle Slides übereinander (Stack) |
transition-all duration-500 | Smooth Transitions für opacity und scale |
opacity-100 scale-100 z-10 | Aktives Slide (sichtbar, normal skaliert, oben) |
opacity-0 scale-105 z-0 | Inaktive Slides (unsichtbar, leicht vergrößert, unten) |
object-contain | Erhält Seitenverhältnis, verhindert Verzerrung |
Schritt 4: Navigation Controls implementieren
Als Nächstes fügen wir die Vor/Zurück-Buttons hinzu:
{
showControls && items.length > 1 && (
<>
{/* Previous button */}
<Button
type="button"
variant="icon-circle"
icon="tabler:chevron-left"
iconClass="w-5 h-5 md:w-7 md:h-7 m-0"
class="carousel-prev absolute top-1/2 left-3 md:left-6 z-20 -translate-y-1/2 opacity-0 invisible group-hover:opacity-100 group-hover:visible focus:opacity-100 focus:visible"
aria-label="Previous slide"
/>
{/* Next button */}
<Button
type="button"
variant="icon-circle"
icon="tabler:chevron-right"
iconClass="w-5 h-5 md:w-7 md:h-7 m-0"
class="carousel-next absolute top-1/2 right-3 md:right-6 z-20 -translate-y-1/2 opacity-0 invisible group-hover:opacity-100 group-hover:visible focus:opacity-100 focus:visible"
aria-label="Next slide"
/>
</>
)
}Wichtige Features
- Conditional Rendering: Buttons werden nur bei mehr als einem Item angezeigt
- Hover-basierte Sichtbarkeit:
group-hover:opacity-100zeigt Buttons nur bei Hover - Accessibility:
aria-labelfür Screen Reader - Responsive Positioning:
left-3 md:left-6für unterschiedliche Bildschirmgrößen
Schritt 5: Indicator Dots hinzufügen
Die Indikator-Punkte zeigen die aktuelle Position im Carousel an:
{
showIndicators && items.length > 1 && (
<div class="absolute bottom-2 md:bottom-4 left-1/2 -translate-x-1/2 z-20 flex gap-2.5 md:gap-3 bg-black/20 backdrop-blur-md rounded-full px-4 py-2.5">
{items.map((_, index) => (
<button
type="button"
class={twMerge(
'carousel-indicator h-2.5 md:h-3 rounded-full transition-all duration-500 ease-out',
'hover:scale-110 active:scale-95',
'focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-black/20',
index === 0 ? 'bg-white shadow-lg w-8 md:w-10 scale-105' : 'bg-white/60 hover:bg-white/80 w-2.5 md:w-3'
)}
aria-label={`Go to slide ${index + 1}`}
aria-current={index === 0 ? 'true' : 'false'}
data-carousel-indicator={index}
/>
))}
</div>
)
}Design-Details
- Glass-Morphism:
bg-black/20 backdrop-blur-mdfür modernen Look - Aktiver Indikator: Größerer, hellerer Punkt mit Shadow
- Smooth Transitions:
transition-all duration-500für weiche Übergänge - Interaktive States: Hover und Active States für besseres UX
Schritt 6: TypeScript Carousel Class implementieren
Jetzt implementieren wir die gesamte Client-Side Logik in einer TypeScript-Klasse:
<script define:vars={{ carouselId, autoplay, autoplayInterval, itemsLength: items.length }}>
class Carousel {
constructor(carouselId, autoplay, autoplayInterval, itemsLength) {
this.carousel = document.querySelector(`[data-carousel="${carouselId}"]`);
if (!this.carousel || itemsLength <= 1) return;
this.items = this.carousel.querySelectorAll('[data-carousel-item]');
this.indicators = this.carousel.querySelectorAll('[data-carousel-indicator]');
this.prevButton = this.carousel.querySelector('.carousel-prev');
this.nextButton = this.carousel.querySelector('.carousel-next');
this.videos = this.carousel.querySelectorAll('video');
this.currentIndex = 0;
this.autoplay = autoplay;
this.autoplayInterval = autoplayInterval;
this.intervalId = null;
this.init();
}
init() {
// Event listeners für Buttons
if (this.prevButton) {
this.prevButton.addEventListener('click', () => this.prev());
}
if (this.nextButton) {
this.nextButton.addEventListener('click', () => this.next());
}
// Event listeners für Indicators
this.indicators.forEach((indicator, index) => {
indicator.addEventListener('click', () => this.goToSlide(index));
});
// Tastaturnavigation
this.carousel.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') this.prev();
if (e.key === 'ArrowRight') this.next();
});
// Pause Autoplay bei Hover
this.carousel.addEventListener('mouseenter', () => this.pauseAutoplay());
this.carousel.addEventListener('mouseleave', () => this.resumeAutoplay());
// Autoplay starten
if (this.autoplay) {
this.startAutoplay();
}
// Video abspielen wenn vorhanden
this.playCurrentVideo();
}
goToSlide(index) {
// Alle Videos pausieren
this.pauseAllVideos();
// Aktiven State vom aktuellen Item entfernen
this.items[this.currentIndex].classList.remove('opacity-100', 'scale-100', 'z-10');
this.items[this.currentIndex].classList.add('opacity-0', 'scale-105', 'z-0');
if (this.indicators[this.currentIndex]) {
this.indicators[this.currentIndex].classList.remove('bg-white', 'shadow-lg', 'w-8', 'md:w-10', 'scale-105');
this.indicators[this.currentIndex].classList.add('bg-white/60', 'w-2.5', 'md:w-3');
this.indicators[this.currentIndex].setAttribute('aria-current', 'false');
}
// Index aktualisieren
this.currentIndex = index;
// Aktiven State zum neuen Item hinzufügen
this.items[this.currentIndex].classList.remove('opacity-0', 'scale-105', 'z-0');
this.items[this.currentIndex].classList.add('opacity-100', 'scale-100', 'z-10');
if (this.indicators[this.currentIndex]) {
this.indicators[this.currentIndex].classList.remove('bg-white/60', 'w-2.5', 'md:w-3');
this.indicators[this.currentIndex].classList.add('bg-white', 'shadow-lg', 'w-8', 'md:w-10', 'scale-105');
this.indicators[this.currentIndex].setAttribute('aria-current', 'true');
}
// Video abspielen wenn vorhanden
this.playCurrentVideo();
}
next() {
const nextIndex = (this.currentIndex + 1) % this.items.length;
this.goToSlide(nextIndex);
}
prev() {
const prevIndex = (this.currentIndex - 1 + this.items.length) % this.items.length;
this.goToSlide(prevIndex);
}
startAutoplay() {
this.intervalId = setInterval(() => this.next(), this.autoplayInterval);
}
pauseAutoplay() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
resumeAutoplay() {
if (this.autoplay && !this.intervalId) {
this.startAutoplay();
}
}
playCurrentVideo() {
const currentItem = this.items[this.currentIndex];
const video = currentItem.querySelector('video');
if (video) {
video.play().catch(() => {
// Autoplay failed, which is fine
});
}
}
pauseAllVideos() {
this.videos.forEach((video) => {
video.pause();
video.currentTime = 0;
});
}
}
// Carousel initialisieren
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
new Carousel(carouselId, autoplay, autoplayInterval, itemsLength);
});
} else {
new Carousel(carouselId, autoplay, autoplayInterval, itemsLength);
}
</script>Methoden-Übersicht
| Methode | Beschreibung |
|---|---|
init() | Initialisiert Event Listeners und Autoplay |
goToSlide(index) | Navigiert zu einem bestimmten Slide |
next() | Navigiert zum nächsten Slide (zyklisch) |
prev() | Navigiert zum vorherigen Slide (zyklisch) |
startAutoplay() | Startet automatisches Durchlaufen |
pauseAutoplay() | Pausiert Autoplay (bei Hover) |
resumeAutoplay() | Setzt Autoplay fort (nach Hover) |
playCurrentVideo() | Spielt Video im aktuellen Slide ab |
pauseAllVideos() | Pausiert alle Videos und setzt sie zurück |
Schritt 7: Custom CSS Styles
Für zusätzliche Animationen und Touch-Device Optimierungen fügen wir Custom CSS hinzu:
<style>
/* Smooth fade-in animation für Controls */
.carousel-prev.visible,
.carousel-next.visible {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Buttons bleiben über anderen Inhalten */
.carousel-prev,
.carousel-next {
-webkit-tap-highlight-color: transparent;
}
/* Active Effect für Touch Devices */
@media (hover: none) and (pointer: coarse) {
.carousel-prev:active,
.carousel-next:active {
transform: translateY(-50%) scale(0.95);
}
}
/* Interaktive Indicators */
.carousel-indicator {
-webkit-tap-highlight-color: transparent;
}
</style>Verwendung der Carousel-Komponente
Jetzt können wir unsere Carousel-Komponente in beliebigen Astro-Seiten verwenden:
Einfaches Beispiel
---
import Carousel from '~/components/widgets/Carousel.astro';
const carouselItems = [
{
image: {
src: '~/assets/images/slide1.jpg',
alt: 'Slide 1',
},
caption: 'Erstes Bild im Carousel',
},
{
image: {
src: '~/assets/images/slide2.jpg',
alt: 'Slide 2',
},
caption: 'Zweites Bild im Carousel',
},
{
video: {
src: '/videos/demo.mp4',
},
caption: 'Ein Video im Carousel',
},
];
---
<Carousel
title="Projekt Galerie"
subtitle="Unsere besten Arbeiten"
items={carouselItems}
autoplay={true}
autoplayInterval={3000}
aspectRatio="16/9"
/>Erweiterte Konfiguration
<Carousel
title="Portfolio Showcase"
tagline="Featured Projects"
subtitle="Explore our latest work"
items={carouselItems}
autoplay={true}
autoplayInterval={5000}
showIndicators={true}
showControls={true}
aspectRatio="4/3"
isDark={true}
classes={{
container: 'py-16 md:py-20',
}}
/>Performance-Optimierungen
1. Lazy Loading für Bilder
Unsere Image-Komponente verwendet bereits loading="lazy", was bedeutet, dass Bilder erst geladen werden, wenn sie in den Viewport kommen.
2. Responsive Images
Mit dem widths und sizes Attribut generieren wir mehrere Bildversionen:
<Image widths={[400, 768, 1024, 2040]} sizes="(max-width: 767px) 100vw, (max-width: 1023px) 50vw, 33vw" />3. Video Optimierung
Videos werden mit muted, loop und playsinline Attributen optimiert für bessere Performance und Mobile Support.
4. CSS Transitions vs. JavaScript Animations
Wir nutzen CSS Transitions statt JavaScript für Animationen, was von der GPU beschleunigt wird und smoother läuft.
Accessibility Features
Unsere Carousel-Komponente erfüllt wichtige Accessibility-Standards:
- ARIA Labels: Alle interaktiven Elemente haben aussagekräftige Labels
- Keyboard Navigation: Pfeiltasten für Navigation
- aria-current: Zeigt aktuellen Slide für Screen Reader an
- Focus States: Sichtbare Focus Rings für Tastaturnutzer
- Semantic HTML: Button-Elemente statt div mit onClick
Zusammenfassung
Wir haben eine vollständige, produktionsreife Carousel-Komponente entwickelt mit:
Zero Dependencies – Keine externen Bibliotheken
TypeScript – Vollständige Type-Safety
Responsive Design – Mobile-First Ansatz
Performance – Lazy Loading, CSS Transitions
Accessibility – ARIA Labels, Keyboard Support
Video Support – Automatisches Abspielen und Pausieren
Flexible Props – Einfache Anpassung an verschiedene Szenarien
Modern UI – Glass-Morphism, Smooth Animations
Vorteile gegenüber externen Bibliotheken
| Aspekt | Custom Carousel | Externe Bibliothek (z.B. Swiper) |
|---|---|---|
| Bundle-Größe | ~2-3 KB | ~70-120 KB |
| Dependencies | 0 | 0 |
| Anpassbarkeit | 100% | Eingeschränkt |
| Learning Curve | Niedrig | Mittel-Hoch |
| Maintenance | Volle Kontrolle | Abhängig von Maintainern |
Weitere Demo-Beispiele
Hier sind einige weitere Beispiele, wie die Carousel-Komponente in verschiedenen Szenarien verwendet werden kann:
Quadratisches Carousel
Ideal für Profile, Produkte oder Social Media Inhalte
Quadratisches Format (1:1). Ideal für Profile oder Produkte
Perfekt für Social Media Content
Responsive und Mobile-First
Carousel ohne Autoplay
Manuelle Navigation für wichtige Inhalte die Aufmerksamkeit erfordern
Eingebettetes Video im Carousel
Kein Autoplay. Nutzer haben volle Kontrolle
Hover über das Carousel um die Controls zu sehen
Vollbild Carousel
Ideal für beeindruckende visuelle Präsentationen
Atemberaubende Landschaften im Vollbildmodus
Perfekt für Portfolio-Präsentationen
Vollbild Video im Carousel
Hinweis: Alle Demos sind vollständig interaktiv! Probiere Tastaturnavigation, Hover-Effects und Indicator-Clicks aus.
Fazit
Das Entwickeln einer eigenen Carousel-Komponente ohne externe Dependencies ist nicht nur lehrreich, sondern bietet auch erhebliche Vorteile in Bezug auf Performance, Bundle-Größe und Wartbarkeit. Mit Astro und TypeScript haben wir eine moderne, typsichere Lösung geschaffen, die sich nahtlos in unser Projekt integriert.
Weiterführende Links:



