Aufbau einer React-Komponente
Meine Vorstellung davon, wie jede React-Komponente aussehen sollte
422
8 min.
Der SSS-Standard legt Anforderungen an die Semantik, Barrierefreiheit und Funktionslogik von Slidern fest.
Die Anforderungen richten sich nach dem Verhalten der jeweiligen Komponente: endliche oder zyklische Navigation, manuelles oder automatisches Weiterschalten sowie die Anzahl gleichzeitig sichtbarer Slides.
In den Implementierungsbeispielen wird Embla Carousel mit dem React-Wrapper verwendet. Die Bibliothek dient als Beispiel und ist keine vorgeschriebene Abhängigkeit; die API der Beispiele entspricht Embla v8.
<button>-Elemente verwendet. Links dienen der Navigation zu einer anderen Adresse, nicht der Steuerung eines Sliders.disabled angegeben. Zusätzlich muss sie sich visuell vom aktiven Zustand unterscheiden, etwa durch eine angepasste opacity und den Cursor not-allowed. Eine rein visuelle Kennzeichnung vermittelt den Zustand nicht an assistive Technologien.ResizeObserver; ein Listener nur für window.resize reicht nicht aus, da sich Elementgrößen auch ohne Änderung des Fensters ändern können.select und reInit mit canScrollPrev() und canScrollNext() synchronisiert. Die Schaltflächen werden außerhalb des Viewport-Elements platziert, das Drag-Eingaben verarbeitet.gap richten sich nach der verfügbaren Breite der Komponente. Je nachdem, ob das Layout auf die Viewport- oder die Breite des übergeordneten Containers reagiert, eignen sich Media Queries oder Container Queries.role="region", wenn er einen bedeutenden Seitenbereich darstellt, oder role="group", wenn kein eigener Landmark-Bereich erforderlich ist. aria-roledescription kennzeichnet den Komponententyp mit einer lokalisierten Beschreibung.aria-labelledby oder, falls keine sichtbare Überschrift vorhanden ist, über aria-label. Bei Verwendung von aria-roledescription muss das Wort „Karussell“ nicht im zugänglichen Namen wiederholt werden.role="group" und einem lokalisierten Wert für aria-roledescription ausgezeichnet und erhält einen aussagekräftigen Namen: über seine Überschrift oder, wenn keine eigene Überschrift vorhanden ist, über seine Position, etwa „2 von 5“. Bei gesetztem aria-roledescription wird das Wort „Folie“ nicht im Namen wiederholt.aria-label="Vorheriger Slide" und aria-label="Nächster Slide". Sichtbarer Schaltflächentext kann den zugänglichen Namen ohne aria-label bereitstellen.aria-disabled="true" für eine Gruppe von Auswahlbuttons nach APG oder aria-selected="true" bei einer Tabs-Implementierung. aria-current="true" wird nicht automatisch auf den Slide-Container gesetzt; die aktuelle Position wird am Steuerelement mit der Semantik des gewählten Musters angegeben.aria-hidden="true" wird nur verwendet, wenn Inhalte sowohl aus dem Accessibility Tree als auch aus der Fokusnavigation konsistent ausgeschlossen werden. Ein teilweise sichtbarer Slide gilt nicht als verborgen.scrollSnapList() ermittelt und über scrollTo(index) angesteuert.aria-disabled="true" ausgezeichnet; eine Tabs-Implementierung erfordert das vollständige Tabs-Muster einschließlich aria-selected und der zugehörigen Tastatursteuerung. Farbe oder Transparenz allein reichen nicht aus.aria-label="Zu Position 2 von 5 wechseln". Bei gruppierten Slides nennt der Name den Bereich oder Inhalt der Gruppe.<button>-Elemente mit sichtbarem :focus-visible umgesetzt. Ihr Pointer-Ziel muss WCAG 2.2 SC 2.5.8 entsprechen: mindestens 24 × 24 CSS-px oder eine anwendbare Ausnahme, etwa ausreichender Abstand oder ein gleichwertiges Steuerelement.useEmblaCarousel übergebenen Viewports platziert. Der Viewport verarbeitet Zeiger- und Drag-Eingaben, was Klicks auf darin platzierte Buttons beeinträchtigen kann.select und reInit mit Embla synchronisiert. Nach Änderungen der Containerbreite oder der Slide-Menge können sich die Anzahl der Snap-Punkte und der Navigationspunkte ändern.grab und während des Drückens und Ziehens grabbing anzeigen. Dieser visuelle Hinweis ersetzt weder Navigationsbuttons noch Tastatur- oder Touch-Bedienung.aria-live="polite" verwenden, damit Änderungen ohne Unterbrechung der aktuellen Sprachausgabe angekündigt werden. Bei automatischer Rotation wird aria-live="off" verwendet, um fortlaufende Ansagen zu vermeiden.prefers-reduced-motion: Bei angeforderter Bewegungsreduktion werden Übergänge verkürzt oder deaktiviert, sofern sie nicht zum Verständnis einer Zustandsänderung erforderlich sind.height: auto ist nicht grundsätzlich verboten. Die automatische Höhenbestimmung verursacht für sich genommen keinen Sprung; Layoutverschiebungen entstehen, wenn sich die Höhe des Sliders beim Inhaltswechsel unberücksichtigt ändert.min-height oder eine einheitliche Slide-Höhe reserviert. Eine feste Höhe darf Inhalte bei längerem Text, einem Sprachwechsel oder beim Zoomen nicht abschneiden.transform und, wo passend, opacity bevorzugt, da Änderungen an left oder margin eine Neuberechnung des Layouts auslösen können. Die Auswahl wird durch Profiling überprüft; keine CSS-Eigenschaft garantiert allein flüssige Animationen.will-change: transform wird nicht standardmäßig allen Slides hinzugefügt. Die Eigenschaft wird nur bei einem bestätigten Performance-Problem eingesetzt, da dauerhaft reservierte Ressourcen für viele Ebenen die Performance verschlechtern können.loading="lazy" nicht gesetzt. Lazy Loading eignet sich für Bilder, die anfangs außerhalb des Viewports liegen.loading="lazy" legt keinen exakten Abstand von ein oder zwei Slides fest: Der Browser bestimmt den Ladebeginn anhand der Nähe zum Viewport und weiterer Bedingungen. Ein früheres Laden des nächsten Slides erfordert eine separate, durch Messungen und das Netzwerkbudget begründete Entscheidung.width und height oder ein bekanntes Seitenverhältnis angegeben. Dadurch wird vor dem Laden Platz reserviert und das Risiko von Layoutverschiebungen reduziert.srcset und sizes müssen die Werte der responsiven Bildbreite innerhalb des Slides entsprechen. Vollformatige Bilder werden nicht ohne Not für Vorschaubilder geladen.disabled ist für assistive Technologien verfügbar und visuell erkennbar.aria-live im manuellen und automatischen Modus, die Einstellung zur Bewegungsreduktion und Slides außerhalb des sichtbaren Bereichs werden geprüft.transform oder will-change abgeleitet.Manuell bedienter Embla-Slider mit Navigation und Punkten:
'use client'
import { useCallback, useEffect, useState } from 'react'
import useEmblaCarousel from 'embla-carousel-react'
import type { EmblaCarouselType } from 'embla-carousel'
const slides = [
{ id: 'article-1', title: 'Erster Artikel', image: '/images/article-1.webp' },
{ id: 'article-2', title: 'Zweiter Artikel', image: '/images/article-2.webp' },
{ id: 'article-3', title: 'Dritter Artikel', image: '/images/article-3.webp' },
]
export const ArticleCarousel = () => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false, slidesToScroll: 1 })
const [scrollSnaps, setScrollSnaps] = useState<number[]>([])
const [selectedSnap, setSelectedSnap] = useState(0)
const onSelect = useCallback((api: EmblaCarouselType) => {
setSelectedSnap(api.selectedScrollSnap())
}, [])
const onReInit = useCallback((api: EmblaCarouselType) => {
setScrollSnaps(api.scrollSnapList())
setSelectedSnap(api.selectedScrollSnap())
}, [])
useEffect(() => {
if (!emblaApi) return
onReInit(emblaApi)
emblaApi.on('select', onSelect)
emblaApi.on('reInit', onReInit)
return () => {
emblaApi.off('select', onSelect)
emblaApi.off('reInit', onReInit)
}
}, [emblaApi, onReInit, onSelect])
return (
<section
className='embla'
role='region'
aria-roledescription='Karussell'
aria-labelledby='featured-title'
>
<h2 id='featured-title'>Empfohlene Artikel</h2>
<div
className='embla__viewport'
ref={emblaRef}
aria-live='polite'
aria-atomic='false'
>
<div className='embla__container'>
{slides.map((slide) => (
<article
className='embla__slide'
key={slide.id}
role='group'
aria-roledescription='Folie'
aria-labelledby={`slide-title-${slide.id}`}
>
<img
src={slide.image}
width='960'
height='640'
alt={`Titelbild: ${slide.title}`}
/>
<h3 id={`slide-title-${slide.id}`}>{slide.title}</h3>
</article>
))}
</div>
</div>
{scrollSnaps.length > 1 && (
<div className='embla__controls'>
<button
type='button'
aria-label='Vorheriger Slide'
disabled={!emblaApi?.canScrollPrev()}
onClick={() => emblaApi?.scrollPrev()}
>
<span aria-hidden='true'>←</span>
</button>
<button
type='button'
aria-label='Nächster Slide'
disabled={!emblaApi?.canScrollNext()}
onClick={() => emblaApi?.scrollNext()}
>
<span aria-hidden='true'>→</span>
</button>
<div className='embla__dots' role='group' aria-label='Slider-Position auswählen'>
{scrollSnaps.map((_, index) => {
const isSelected = index === selectedSnap
return (
<button
className='embla__dot'
type='button'
key={index}
aria-label={`Zu Position ${index + 1} von ${scrollSnaps.length} wechseln`}
aria-disabled={isSelected}
onClick={() => {
if (!isSelected) emblaApi?.scrollTo(index)
}}
>
<span aria-hidden='true' />
</button>
)
})}
</div>
</div>
)}
</section>
)
};Das Beispiel verwendet manuelle Navigation; deshalb hat der Slide-Bereich aria-live="polite". Die Steuerelemente befinden sich außerhalb von .embla__viewport, damit Drag-Eingaben das Aktivieren der Buttons nicht beeinträchtigen. scrollSnapList() liefert die Navigationspunkte für jede Scroll-Position; ihr Zustand wird bei select und reInit aktualisiert. Das Attribut loading ist nicht angegeben, da die Anzahl der beim initialen Laden sichtbaren Bilder vom responsiven Layout abhängt. In einer produktiven Komponente werden anfangs sichtbare Bilder nicht verzögert geladen; Bilder außerhalb des initialen Viewports können lazy geladen werden. Bei pnpm muss embla-carousel zusätzlich als direkte Entwicklungsabhängigkeit eingetragen werden, wenn EmblaCarouselType importiert wird.
Responsives Embla-Layout mit teilweise sichtbarer nächster Karte und ausreichend großer Zielfläche für Navigationspunkte:
.embla__viewport {
overflow: hidden;
cursor: grab;
touch-action: pan-y pinch-zoom;
}
.embla__viewport:active {
cursor: grabbing;
}
.embla__container {
display: flex;
gap: 12px;
}
.embla__slide {
flex: 0 0 calc(100% - 28px);
min-width: 0;
}
.embla__dot {
display: inline-grid;
min-width: 24px;
min-height: 24px;
place-items: center;
border: 0;
background: transparent;
cursor: pointer;
}
.embla__dot span {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.embla__dot[aria-disabled='true'] span {
width: 12px;
height: 12px;
}
.embla__dot:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (min-width: 768px) {
.embla__slide {
flex-basis: calc((100% - 24px) / 3);
}
}Bei einer Kartenbreite von calc(100% - 28px) und einem gap von 12px bleiben im Viewport etwa 16 px der nächsten Karte sichtbar. Dies ist ein Layoutbeispiel: Anzahl sichtbarer Karten, gap, Vorschau des Nachbar-Slides und Zielfläche der Navigationspunkte werden anhand der tatsächlichen Containerbreite geprüft. Bei Embla v8 werden API-Beispiele mit der Dokumentation dieser Major-Version abgeglichen.
Ähnliche Kategorien:
Meine Vorstellung davon, wie jede React-Komponente aussehen sollte
422
8 min.
Der SST-Standard legt Anforderungen an die Semantik, Barrierefreiheit und Funktionslogik von Registerkarten fest
62
6 min.
Der SSF-U-Standard legt Anforderungen an die Semantik, Barrierefreiheit und Funktionslogik von Vollbildmodus fest
248
2 min.
Der SSA-Standard legt Anforderungen an die Semantik, Barrierefreiheit und Funktionslogik von Akkordeons fest
258
2 min.
Analyse kritischer Fehler im Webdesign. Warum Slider, Autoplay und schwerfällige Seiten die Konversionsrate sowie die Platzierungen bei Google verschlechtern
232
2 min.
Der SSP-Standard legt Anforderungen an die Semantik, Barrierefreiheit und Funktionslogik von Paginierung fest
240
1 min.