Engineering Inclusivity: Wie wir das MediaMarktSaturn Design System für zwei Marken und Millionen Nutzer neu erfanden

Eine technische Fallstudie über die Transformation eines E-Commerce-Monolithen zu einem barrierefreien, skalierbaren White-Label-System im Nx Monorepo.Alex FriedlDec 18, 2025
cardproduct

1. Der strategische Rahmen & Die Architektur

Im High-Traffic E-Commerce ist das Design System mehr als eine UI-Bibliothek; es ist das Betriebssystem der User Experience. Zwischen Oktober 2021 und August 2022 hatte ich als Senior Software Engineer (Frontend) bei MediaMarktSaturn (MMS) die technische Verantwortung, dieses Fundament zu modernisieren.

Die Herausforderung: Ein System zu bauen, das MediaMarkt.de (Rot/Leidenschaftlich) und Saturn.de (Blau/Technisch) bedient, ohne Code zu duplizieren, und dabei die strengen WCAG 2.1 Accessibility-Standards erfüllt.

1.1 Der Tech-Stack: Skalierung im Nx Monorepo

Wir setzten auf eine Architektur, die Modularität und Typsicherheit erzwingt:

  • Nx Monorepo: Das Design System lebte als isolierte Library. Nx ermöglichte uns, Abhängigkeiten zu visualisieren und Caching für Builds zu nutzen. Änderungen an einer Komponente konnten sofort gegen Webshop, Checkout und App getestet werden.
  • TypeScript: Bei hunderten Entwicklern ist any keine Option. Wir nutzten strikte Typisierung für Props und Theme-Interfaces, um Integrationsfehler zur Build-Zeit abzufangen.
  • Styled Components: Die Basis für unser White-Labeling. Über ThemeProvider injizierten wir markenspezifische Tokens (Farben, Spacing, Typografie) in die Komponenten.
  • Storybook & Chromatic: Dokumentation war unser Vertrag mit den anderen Teams. Chromatic sicherte uns gegen visuelle Regressionen ab – wenn ein Pixel im Saturn-Theme verrutschte, blockierte die Pipeline.

2: Die Evolution der Produktkachel – Architektur-Refactoring

Das Herzstück jedes Shops ist die Produktkachel. Unsere Analyse der bestehenden ProductTile offenbarte massive architektonische Schulden.

2.1 Status Quo: Die "Prop-Hölle" (Legacy Code)

Die alte ProductTile.tsx war ein klassischer Monolith. Sie versuchte, jede Business-Logik (Suche, Warenkorb, Empfehlungen) intern zu lösen.

Das Problem: Das Interface war auf über 50 Props aufgebläht. Um ein Element tief im Baum zu ändern, musste man Props durch 4-5 Ebenen reichen ("Prop Drilling").

typescript
// Legacy: ProductTile.interface.ts (Auszug) // Ein Interface, das alles wissen muss -> Wartbarkeits-Albtraum export interface ProductTileProps extends BorderProps { product: SharedProduct; brandPrefixLabel?: string; calculatedBasePrice?: (price: PriceType, basePrice: BasePriceProps) => string | undefined; dataSheetLabel?: string; eelSize?: EnergyEfficiencySize; elevationHover?: ElevationLevel; energyEfficiencySlot?: JSX.Element | null; expanded?: boolean; isCompact?: boolean; isDisabled?: boolean; isSoldOut?: boolean; layout?: ProductTileLayout; // 'listItem' | 'gridItem' | 'sponsored' // ... +40 weitere Props }

Die Implementierung war imperativ und starr:

typescript
// Legacy: ProductTile.tsx export const ProductTile: FC<ProductTileProps> = (props) => { // Versteckte Magie: Eine Hook entscheidet, welche riesige Sub-Komponente geladen wird const LayoutComponent = useLayoutComponent(props.layout); return ( <LayoutComponent // Wir müssen ALLES weiterreichen. Spread-Operator macht den Datenfluss undurchsichtig. brandPrefixLabel={props.brandPrefixLabel} calculatedBasePrice={props.calculatedBasePrice} isDisabled={props.isDisabled} isSoldOut={props.isSoldOut} // ... 50 Zeilen Prop-Passing {...props}> {props.children} </LayoutComponent> ) }

2.2 Der Paradigmenwechsel: React Compound Components

Wir haben die Komponente nicht refactored, sondern neu gedacht. Wir wechselten zum Compound Component Pattern.

  • Prinzip: Parent (CardProduct) stellt State bereit. Children (Title, Price) konsumieren ihn.
  • Vorteil: Keine Props mehr durchreichen. Deklarativer Code.

Der neue Core (Context Provider):

typescript
// Modern: CardProduct.tsx // 1. Context für impliziten State definieren const CardProductContext = createContext({ states: {} as CardProductStateProps, grid: Grids.COLUMN, }); // 2. Die Parent-Komponente export const CardProduct: FC<CardProductProps> & CardProductElements = ({ grid = Grids.COLUMN, states = {}, // Hier kommen globale Zustände rein (Disabled, Loading, SoldOut) children, }) => { // Destructuring der States für die Wrapper-Logik const { isDisabled, isSoldOut, isBlank, elevationHover, onClick } = states || {}; return ( <CardProductContext.Provider value={{ states, grid }}> <Card variant={isBlank ? 'seamless' : 'white'} // Intelligente UI-Logik direkt im Core: Keine Elevation bei Disabled/SoldOut elevationHover={isBlank || isDisabled || isSoldOut ? 0 : elevationHover} disabled={isDisabled} // Accessibility & UX: Klicks verhindern, wenn SoldOut onClick={!isSoldOut ? onClick : undefined} data-test="mms-product-card"> {/* Layout wird komplett an CSS Grid delegiert */} <StyledGrid grid={grid}>{children}</StyledGrid> </Card> </CardProductContext.Provider> ) }

3: Deep Dive Accessibility & White-Labeling

Die technische Architektur war Mittel zum Zweck. Das Ziel war Inclusive Design in einem Multi-Brand-System.

3.1 White-Labeling: Theming Architecture

Wie bedient man MediaMarkt (Rot) und Saturn (Blau) mit einer Codebasis? Wir nutzten Theme Modes.

Theme Definition (MediaMarkt): Wir definierten semantische Farb-Token. Statt #df0000 nutzten wir color.product.price.

typescript
// ProductTile.theme.mm.ts const productTileModeLight: ProductTileMode = { color: { // Spezifischer Grauwert für MM, der WCAG Kontrast erfüllt raeeText: theme.light.color.grey6, product: { manufacturer: { heading: { default: theme.light.color.grey8 }, text: theme.light.color.grey6, }, title: { heading: { default: theme.light.color.black }, }, }, // ... }, }

Implementation in Styled Components: Eine Helper-Funktion productTileTheme wählt zur Laufzeit das korrekte Theme aus dem Context.

typescript
// ProductTile.styled.tsx // Wählt basierend auf dem globalen ThemeProvider das MM oder SE Theme export const productTileTheme = (theme: Theme): ProductTileMode => componentTheme({ theme, componentThemeMM, componentThemeSE }) export const RaeeText = styled(CopyText)<{ isMarginEnabled: boolean }>` ${({ theme, isMarginEnabled }) => css` // Zugriff auf die semantische Farbe color: ${productTileTheme(theme).color.raeeText}; ${isMarginEnabled && `margin-right: ${theme.spacing.base.xxl};`} `} `

3.2 Accessibility (WCAG 2.1) im Detail

Barrierefreiheit war "eingebacken". Hier drei konkrete Beispiele aus dem Code:

1. Semantische Ratings statt "Div-Suppe" Visuelle Sterne sind für Screenreader unsichtbar. Unsere Komponente erzwingt ein ariaLabel.

typescript
// CardProduct.mock.tsx export const Rating: RatingProps = { // Screenreader liest dies vor, statt "Star Star Star" ariaLabel: 'Bewertung: 4.5 von 5 Sternen', value: 4.5, description: '25 Rezensionen', }

2. Intelligente "Sold Out" & "Disabled" Zustände Wir verlassen uns nicht nur auf graue Farbe. Wenn der Context isSoldOut meldet, greifen CSS-Regeln, die auch die Interaktion für assistive Technologien unterbinden.

typescript
// CardProduct.styled.tsx export const soldOut = css` filter: grayscale(1); // Visueller Hinweis für Sehende opacity: 0.5; ` export const disabled = css` ${soldOut} // Kritisch für A11y: Verhindert, dass Screenreader/Tastaturfokus hier landen pointer-events: none; `

3. Energieeffizienz & Datenblätter Die EU-Vorgaben für Energielabels sind strikt. Wir implementierten eine Logik, die Klicks auf das Logo (Overlay) und den Text (Datenblatt-PDF) sauber trennt und keyboard-accessible macht.

typescript
// ProductTileGridItem.tsx // Nur rendern, wenn Daten da sind UND Produkt verfügbar ist (Vermeidung toter Links) {showEnergyEfficiency && product.energyEfficiency && ( <ProductEnergyEfficiencyInfo energyEfficiency={product.energyEfficiency} dataSheetTitle={dataSheetLabel} // Separate Event-Handler für verschiedene Ziele handleDataSheetClick={(e) => handleCustomClicks(e, 'datasheet')} handleLogoClick={(e) => handleCustomClicks(e, 'eel')} hideLogo={isPermanentlyNotAvailable(onlineStatus)} // A11y: Kein Logo bei EOL Produkten /> )}

3.3 Automatisierte Qualitätssicherung

Vertrauen ist gut, CI/CD ist besser.

  • Jest-Axe: Jeder Commit wurde gegen die WCAG-Regeln geprüft.
typescript
// ProductTile.test.tsx it('has no A11y violations', async () => { const { container } = render(<ProductTile {...rowProps} />) // Dieser Test bricht den Build, wenn Labels fehlen oder Kontraste zu gering sind expect(await axe(container)).toHaveNoViolations() })
  • Chromatic: Da wir Styles dynamisch injizieren, sind visuelle Regressionstests Pflicht. Chromatic renderte jede Story in MM-Light, MM-Dark, SE-Light und SE-Dark, um sicherzustellen, dass Accessibility-Fixes in Marke A nicht das Layout in Marke B zerstören.

4: Developer Experience (DX) & Integration

Das beste System nützt nichts, wenn Entwickler es falsch nutzen. Wir legten maximalen Wert auf DX.

4.1 Layout-Flexibilität durch CSS Grid Areas

Wir entkoppelten die DOM-Reihenfolge vom visuellen Layout. Über Enums definierten wir Grid Areas.

typescript
// CardProduct.interface.tsx export enum CardProductGridAreas { TITLE = 'title', PRICE = 'price', PRODUCTIMAGE = 'productimage', BASKET = 'basket', // ... sprechende Namen statt magische Zahlen }

Das CSS Grid Template erlaubt radikale Layout-Änderungen (z.B. für den Warenkorb), ohne die Komponenten-Logik anzufassen.

typescript
// CardProduct.styled.tsx // Layout für die Listenansicht / Warenkorb const CartLayout = css` grid-template-columns: fit-content(200px) 1fr 1fr; grid-template-areas: '${Areas.PRODUCTIMAGE} ${Areas.TITLE} ${Areas.TITLE}' '${Areas.PRODUCTIMAGE} ${Areas.PRICE} ${Areas.EMPTY}' '${Areas.PRODUCTIMAGE} ${Areas.AMOUNTPICKER} ${Areas.AMOUNTPICKER}' // Hier kommt der AmountPicker dazu '${Areas.ACCORDION} ${Areas.ACCORDION} ${Areas.ACCORDION}'; gap: 0.5rem; `;

4.2 Integration: Deklarativ & Typsicher

Dank des Compound Patterns ist der Integrations-Code selbsterklärend. Hier ein Beispiel, wie eine spezialisierte Warenkorb-Kachel gebaut wird.

typescript
// CardProduct.story.tsx // Nutzung des 'Cart' Grids und Injection des AmountPickers export const ExampleCart: Story = (args) => { return ( <CardProduct {...args} grid={Grids.CART}> {/* Area: Image - Links */} <CardProduct.ProductImage src={src} /> {/* Area: Title - Oben Rechts */} <CardProduct.Title title={title} /> {/* Area: Price - Mitte Rechts */} <CardProduct.Price prefix={prefix} price={price} currency={currency} shippingInfo={shippingInfo} /> {/* Area: AmountPicker - Unten Rechts (Spezifisch für Cart) */} <CardProduct.AmountPicker amountPicker={{ min, max, quantity, onQuantityChange }} /> {/* Area: Accordion - Ganz unten (Service Infos) */} <CardProduct.Accordion entries={entries} /> </CardProduct> ) }

Typsicherheit: Das CardProductElements Interface stellt sicher, dass Entwickler über CardProduct. Zugriff auf alle validen Sub-Komponenten haben (Autocompletion in VS Code).

4.3 Fazit & Lessons Learned

Nach 10 Monaten intensiver Architektur-Arbeit bei MediaMarktSaturn stehen drei Erkenntnisse fest:

  1. Architektur schlägt Geschwindigkeit: Der Wechsel auf Compound Components war initial aufwendiger als ein Hack, hat aber die Wartungskosten langfristig massiv gesenkt und die "Prop-Hölle" eliminiert.
  2. Inklusion ist Code-Qualität: Accessibility ist keine UX-Entscheidung, sondern eine Engineering-Disziplin. Durch die Integration von A11y in den Core (Context, Styled Components) wurde Barrierefreiheit zum Standard.
  3. Monorepo-Skalierung: Nx in Kombination mit Storybook und Chromatic war der Schlüssel, um ein Design System über hunderte Entwickler und zwei Marken hinweg konsistent zu halten.

Wir haben nicht nur eine Komponente neu geschrieben. Wir haben eine inklusive Sprache für das Frontend von MediaMarktSaturn entwickelt.

Über den Autor

Alex Friedl

Alex Friedl

Experte für digitale Produkte

Ich vereine User Research, Prototyping und TypeScript Frontend-/API-Entwicklung um exzellente digitale Lösungen zu gestalten.

Alexander Friedl
Checking...
Checking...

Lassen Sie uns über Ihr Projekt sprechen

Ich verwandle komplexe Anforderungen in elegante Lösungen.

Design
Design & Dev
Development

+49 157 581 508 46