ContentSwitch
Überblick
In Figma wird eine neue Version genannt, jedoch ohne Referenz. Diese Komponente entspricht dem Stand vom 10.08.26 aus Figma; die kompakte Variante wurde am 28.08.26 nach „Content Switch Compact” ergänzt. Zustandsmatrix, Varianten und das responsive Verhalten wurden am 15.09.26 nachgezogen.
mmm-content-switch ist eine Komponente zum Umschalten zwischen Inhalten oder zur Navigation — „wie eine Radio-Group, nur hübscher”.
Die Komponente ist kein Formular-Control. Sie besitzt weder ControlValueAccessor noch eine Signal-Forms-Anbindung und rendert keine versteckten input-Elemente. Je nach Einsatzzweck rendert sie das semantisch passende Element und die dazugehörige ARIA-Struktur:
| Einsatzzweck | Konfiguration | Container-Rolle | Item-Element | Zustand |
|---|---|---|---|---|
| Inhalte umschalten (eine Auswahl) | Standard | tablist | button | aria-selected |
| Mehrfachauswahl (Filter, Optionen) | mode="multi" | group | button | aria-pressed |
| Navigation (Links) | navigation + href | navigation + list | a | aria-current |
Ein Content-Switch sollte entweder aus Links oder aus Buttons bestehen. Eine Mischung erzeugt eine Liste mit einem verirrten Button — dafür gibt es keine sinnvolle Screenreader-Ansage.
Zwischen Host und Items liegt immer ein Wrapper div.mmm-content-switch__list. Im Navigations-Modus trägt er role="list", sonst role="presentation" — er ist dort also nicht im Accessibility-Tree, die Buttons hängen für Screenreader weiterhin direkt an tablist bzw. group. Gebraucht wird er fürs Layout: Die Gruppe ist ein CSS-Container, und eine Container-Query kann das Element, das sie misst, nicht selbst umschalten (siehe Responsives Verhalten). Eigene Styles sollten Items daher mit mmm-content-switch-item ansprechen, nicht mit mmm-content-switch > *.
Zustände
| Zustand | Darstellung |
|---|---|
| Default | Weißer Hintergrund, Rahmen in der Akzentfarbe, Text in grey2 |
| Hover | Transparente Akzent-Schicht über dem Item (--mmm-content-switch-hover-layer) |
| Selected | Füllung in der Akzentfarbe, Text in on-primary |
| Selected Hover | Zusätzliche dunkle Schicht über der Füllung (--mmm-content-switch-selected-hover-layer) |
| Focus | Ring innerhalb des Items; auf der Füllung wechselt er auf die Textfarbe |
| Disabled | Rahmen und Trennlinien auf 40 %, Text in grey3, keine Pointer-Events |
Hover und Selected Hover sind transparente Schichten, keine Hintergrund-Wechsel. Dieselbe Regel funktioniert damit sowohl auf dem Standard-Hintergrund als auch auf der Füllung des ausgewählten Items.
Der Fokusring liegt bewusst innerhalb des Items statt als outline außen herum: Die Items stoßen ohne Abstand aneinander, ein äußerer Ring würde von den Nachbarn überlagert.
mmm-content-switch
Inputs
| Input | Typ | Default | Beschreibung |
|---|---|---|---|
label | string (required) | – | Barrierefreier Name der Gruppe bzw. des Navigations-Landmarks (aria-label) |
value | string | string[] | '' | Ausgewählter Key. Two-Way-Binding via [(value)]. Bei mode="multi" ein Array |
mode | single | multi | 'single' | Auswahl-Semantik. single erzeugt eine Tab-Liste, multi eine Gruppe von Toggle-Buttons |
navigation | boolean | false | Rendert die Gruppe als Navigations-Landmark mit Liste. Für Items mit href zu verwenden |
disabled | boolean | false | Deaktiviert alle Items |
vertical | boolean | false | Erzwingt vertikales Layout |
horizontal | boolean | false | Erzwingt horizontales Layout in jeder Breite — schaltet auch das automatische Stapeln enger Zeilen ab (siehe Responsives Verhalten) |
compact | boolean | false | Kompakte Darstellung als Segmented Control statt Karten (siehe Kompakt) |
name | string | generiert | Stabiles ID-Präfix der Gruppe |
Outputs
| Output | Typ | Beschreibung |
|---|---|---|
change | CustomEvent<{ value: string | string[] }> | Wird nur bei Nutzerinteraktion ausgelöst |
valueChange | string | string[] | Wird bei jeder Wertänderung ausgelöst, also auch bei programmatischem Setzen |
mmm-content-switch-item
Inputs
| Input | Typ | Default | Beschreibung |
|---|---|---|---|
value | string (required) | – | Identität des Items innerhalb der Gruppe |
disabled | boolean | false | Deaktiviert dieses Item |
icon | boolean | false | Icon-Only-Darstellung (kompaktes Padding, größere Schrift) |
as | div | span | section | article | 'div' | Tag des Content-Wrappers |
controls | string | – | ID des zugehörigen Panels (aria-controls) — nur im Tab-Modus relevant |
href | string | (string | number)[] | – | Aktiviert den Link-Modus. Siehe Navigation |
queryParams | Params | – | Query-Parameter des Links |
fragment | string | – | URL-Fragment des Links |
target | string | – | Link-Target. Umgeht bewusst den Router |
rel | string | berechnet | Bei target="_blank" automatisch noopener noreferrer |
activeMatch | exact | prefix | IsActiveMatchOptions | 'exact' | Wie der Link gegen die aktuelle URL gematcht wird |
ariaCurrent | page | step | location | true | 'page' | aria-current-Wert des aktiven Links (nur ohne Router relevant) |
Outputs
| Output | Typ | Beschreibung |
|---|---|---|
itemClick | MouseEvent | Klick auf ein Link-Item vor der Navigation. preventDefault() übernimmt die Navigation selbst |
Öffentliche Properties
| Property | Typ | Beschreibung |
|---|---|---|
tabId | string | ID des Tab-Buttons — für aria-labelledby des zugehörigen role="tabpanel" |
Slots
| Direktive | Legacy-Alternative | Beschreibung |
|---|---|---|
mmmTitle | slot="title" | Titelzeile des Items |
mmmDescription | slot="description" | Beschreibungstext unter dem Titel |
mmmIcon | slot="icon" | Icon rechts neben dem Text |
Kompakt
compact schaltet den Switch von der Karten- auf die Segmented-Control-Darstellung um (Figma: „Content Switch Compact”).
<mmm-content-switch compact label="Zeitraum" [(value)]="range"> <mmm-content-switch-item value="month"><span mmmTitle>Monat</span></mmm-content-switch-item> <mmm-content-switch-item value="quarter"><span mmmTitle>Quartal</span></mmm-content-switch-item> <mmm-content-switch-item value="year"><span mmmTitle>Jahr</span></mmm-content-switch-item></mmm-content-switch>Unterschiede zur Standard-Darstellung:
| Aspekt | Standard | Kompakt |
|---|---|---|
| Breite | Items teilen sich die Zeile gleichmäßig | Items richten sich nach ihrem Label, Gruppe schrumpft mit |
| Label | Immer fett, linksbündig | Normal, zentriert — fett nur bei Auswahl |
| Höhe | Ergibt sich aus dem Inhalt | 44 px |
| Icon | 48 px neben dem Text | 24 px; Icon-Only ergibt ein 44 px großes Quadrat |
| Langer Text | Umbruch | Abschneiden mit … |
| Mobil | Stapelt unterhalb von m (600 px) vertikal | Bleibt horizontal |
compact sitzt bewusst auf der Gruppe, nicht auf dem Item: Die Größe ist eine Eigenschaft des gesamten Switches, und gemischte Größen innerhalb einer Zeile gibt es im Design nicht.
Kompakt ist für reine Labels gedacht — also nur mmmTitle, optional zusammen mit icon am Item für die Icon-Only-Darstellung. Ein zusätzlich projizierter mmmDescription wird nicht ausgeblendet (stillschweigend verworfener Inhalt wäre schlimmer als falsches Aussehen), macht das Item aber höher als im Design vorgesehen.
Icon-Only
icon am Item entfernt das Text-Padding und rendert einen quadratischen Button. Ein barrierefreier Name muss dann vom projizierten Inhalt kommen — ein Icon allein hat keinen.
<mmm-content-switch compact label="Ansicht" [(value)]="view"> <mmm-content-switch-item value="list" icon> <mmm-icon mmmIcon aria-label="Liste">liste_24</mmm-icon> </mmm-content-switch-item></mmm-content-switch>Das projizierte Icon wird über --mmm-content-switch-icon-size vermaßt — die Regel setzt sowohl font-size (für Icon-Fonts und em-basierte SVGs) als auch --mmm-icon-size (für mmm-icon) auf diesen Wert.
Responsives Verhalten
Der Switch ist mobile-first aufgebaut: Ohne weitere Angabe stapeln sich die Items vertikal und werden ab dem Standard-Breakpoint m (600 px) nebeneinander gelegt. Die Media Query stammt aus dem viewport-Mixin (src/styling/layouts/_viewport.scss), verwendet also dieselben Breakpoints wie der Rest der Bibliothek.
| Konfiguration | < 600 px | ≥ 600 px |
|---|---|---|
| Standard | vertikal | horizontal |
horizontal | horizontal | horizontal |
vertical | vertikal | vertikal |
compact | horizontal | horizontal |
Gestapelte Items nehmen ihre eigene Inhaltshöhe an; nebeneinander teilen sie sich die Zeile zu gleichen Teilen. Beides steuert die vererbte Property --mmm-content-switch-item-flex, die die Gruppe je Richtung setzt.
Automatisches Stapeln bei zu enger Zeile
Eine Karte braucht Platz für ihr Icon (48 px), den Abstand, das Innen-Padding und ein lesbares Label — zusammen rund 180 px. Wird eine Zeile schmaler als Anzahl Items × 180 px, stapelt der Switch von selbst vertikal, unabhängig vom Viewport. Vier Karten reichen dafür schon in einer bildschirmbreiten Gruppe aus; ohne diese Regel würde jedes Item unter die Breite seines eigenen Inhalts gedrückt und das Icon am rechten Rand vom overflow: hidden der Gruppe abgeschnitten.
Gemessen wird dabei die Breite der Gruppe selbst (CSS-Container-Query), nicht die des Fensters: Ein Switch in einer schmalen Seitenleiste stapelt also auch auf einem breiten Bildschirm.
| Items | Stapelt unterhalb von |
|---|---|
| 2 | 360 px |
| 3 | 540 px |
| 4 | 720 px |
| 5 | 900 px |
| … | Anzahl × 180 px (bis 8 Items) |
Ausgenommen sind:
| Fall | Warum |
|---|---|
horizontal | Ausdrücklicher Opt-out aus jedem responsiven Umbruch — die explizite Angabe schlägt die Heuristik |
compact | Segmente richten sich nach ihrem Label und werden nicht gequetscht |
| Icon-Only | Ein [icon]-Item ist ein ~40 px großes Quadrat; ein einziges davon nimmt die ganze Liste aus der Regel |
Die Schwellen stecken in den Sass-Variablen $stack-item-min-width (11.25 rem) und $stack-max-items (8) in src/styling/components/_content-switch.scss. Container-Queries können keine Custom Properties lesen, die Werte sind daher nur beim Build über @use ... with (...) anpassbar, nicht zur Laufzeit über CSS.
Unabhängig davon bricht ein zu langes Wort im Titel um, statt das Icon aus der Karte zu schieben: Die Textspalte des Item-Grids ist minmax(0, 1fr). Das Icon kann damit in keiner Breite abgeschnitten werden — auch dann nicht, wenn horizontal gesetzt ist.
Browser ohne Container-Query-Unterstützung ignorieren die Regel und verhalten sich wie bisher (nur Breakpoint m). Es geht dabei nichts kaputt, die Items werden dort lediglich wieder eng.
vertical hat dabei das letzte Wort: Ein Switch mit vertical und horizontal stapelt: Ein Umbruch ist die verträglichere Auslegung einer widersprüchlichen Konfiguration.
Die Trennlinien zwischen den Items folgen der Richtung automatisch — sie werden über die vererbten Properties --mmm-content-switch-divider-block und --mmm-content-switch-divider-inline gesetzt, von denen die Gruppe je Richtung genau eine füllt.
Tastaturbedienung
| Modus | Verhalten |
|---|---|
Tabs (single) | Die gesamte Tab-Liste ist ein Tabstopp. ←/→ (bzw. ↑/↓ bei vertical) wechseln, Home/End springen ans Ende. Die Auswahl folgt dem Fokus |
| Mehrfachauswahl | Jeder Button ist ein eigener Tabstopp, Leertaste/Enter schaltet um |
| Navigation | Jeder Link ist ein eigener Tabstopp, Enter folgt dem Link |
Navigation
Im Navigations-Modus wird href an RouterLink weitergereicht: interne Links navigieren ohne Full-Page-Reload, und RouterLinkActive markiert automatisch den aktiven Eintrag anhand der echten URL. Es sind keine Provider nötig.
Die Bibliothek benötigt dafür @angular/router als Peer-Dependency. Der Router wird optional injiziert — in Anwendungen ohne Routing rendert die Komponente stattdessen einfache Anker.
Erlaubte Pfade:
| Wert | Auflösung |
|---|---|
/reports | Absolut, ausgehend vom App-Root |
detail bzw. ./detail | Relativ zur aktuellen Route |
../geschwister | Relativ zur Elternroute |
['/reports', id()] | Command-Array für dynamische Segmente |
https://…, mailto:… | Extern — umgeht den Router, rendert einen reinen Anker |
Query-Parameter und Fragmente dürfen nicht in den Pfad geschrieben werden (/reports?tab=1), da der Router sie in ein Pfadsegment kodiert. Stattdessen [queryParams] und [fragment] verwenden.
activeMatch="exact" markiert nur bei exakter Pfadgleichheit, activeMatch="prefix" auch bei Unterrouten (/reports bleibt auf /reports/42/detail aktiv). Beide Presets ignorieren Query-Parameter und Fragmente, damit ein Eintrag auch auf /reports?page=2 aktiv bleibt.
Nicht abgefangen werden — und damit wie bei jedem normalen Link — Klicks mit Modifier-Taste (Strg/Cmd/Shift/Alt, Mittelklick), externe URLs und Links mit target.
CSS-Variablen
Die Gruppe deklariert den kompletten --mmm-content-switch-*-Satz; Items erben ihn. Überschrieben wird er auf dem Element, einem Wrapper oder :root — die generierten Theme-Dateien setzen genau so die vier Basis-Farben.
Farben
| Variable | Default | Beschreibung |
|---|---|---|
--mmm-content-switch-default-background | --mmm-greyscale-white | Hintergrund nicht ausgewählter Items |
--mmm-content-switch-default-text | --mmm-greyscale-grey2 | Textfarbe nicht ausgewählter Items |
--mmm-content-switch-active-background | --mmm-main-colors-primary | Füllung des ausgewählten Items, Rahmenfarbe |
--mmm-content-switch-active-text | --mmm-main-colors-on-primary | Textfarbe des ausgewählten Items |
--mmm-content-switch-hover-layer | --mmm-state-default-elements-hover | Transparente Hover-Schicht (nicht ausgewählt) |
--mmm-content-switch-selected-hover-layer | --mmm-state-primary-elements-hover | Transparente Hover-Schicht (ausgewählt) |
--mmm-content-switch-disabled-text | --mmm-color-fg-disabled | Textfarbe deaktivierter Items |
--mmm-content-switch-disabled-border-color | Rahmenfarbe auf 40 % | Rahmen und Trennlinien im deaktivierten Zustand |
--mmm-content-switch-focus-ring-color | Rahmenfarbe, ausgewählt: Textfarbe | Farbe des Fokusrings |
--mmm-content-switch-icon-fill | currentColor | Füllung des projizierten Icons |
Maße
| Variable | Standard | Kompakt |
|---|---|---|
--mmm-content-switch-border-width | 2px | 2px |
--mmm-content-switch-border-radius | Theme-abhängig | Theme-abhängig |
--mmm-content-switch-padding-block | 1rem | 0.625rem |
--mmm-content-switch-padding-inline | 1rem | 1rem |
--mmm-content-switch-gap | 0.5rem | 0.5rem |
--mmm-content-switch-icon-size | 3rem | 1.5rem |
--mmm-content-switch-icon-only-size | 1.25rem | 1.5rem |
--mmm-content-switch-focus-inset | 0.625rem | 0.25rem |
--mmm-content-switch-focus-width | 1px | 1px |
Die sichtbare Innenkante eines Items verteilt sich auf zwei Elemente, damit der Fokusring dazwischen liegen kann: padding − focus-inset − focus-width am Button, dann der Ring, dann focus-inset. Eine Variante muss deshalb nur diese drei Werte neu setzen, um das gesamte Item umzumessen.
Beispiel
### Tabs mit Panels
<mmm-content-switch label="Ansicht" [(value)]="view">
<mmm-content-switch-item #listTab value="list" controls="panel-list">
<span mmmTitle>Liste</span>
<span mmmDescription>Alle Einträge untereinander</span>
</mmm-content-switch-item>
<mmm-content-switch-item #mapTab value="map" controls="panel-map">
<span mmmTitle>Karte</span>
</mmm-content-switch-item>
</mmm-content-switch>
<div
id="panel-list"
role="tabpanel"
[attr.aria-labelledby]="listTab.tabId"
[hidden]="view() !== 'list'"
>
…
</div>
<div
id="panel-map"
role="tabpanel"
[attr.aria-labelledby]="mapTab.tabId"
[hidden]="view() !== 'map'"
>
…
</div>
### Mehrfachauswahl
<mmm-content-switch mode="multi" label="Filter" [(value)]="filters">
<mmm-content-switch-item value="open"
><span mmmTitle>Offen</span></mmm-content-switch-item
>
<mmm-content-switch-item value="done"
><span mmmTitle>Erledigt</span></mmm-content-switch-item
>
</mmm-content-switch>
### Mit Icon
<mmm-content-switch label="Produkt" [(value)]="product">
<mmm-content-switch-item value="giro">
<span mmmTitle>Girokonto</span>
<span mmmDescription>Für den täglichen Zahlungsverkehr</span>
<mmm-icon mmmIcon>bank_48</mmm-icon>
</mmm-content-switch-item>
<mmm-content-switch-item value="bau">
<span mmmTitle>Baufinanzierung</span>
<span mmmDescription>Für Ihr Eigenheim</span>
<mmm-icon mmmIcon>hochhaus_48</mmm-icon>
</mmm-content-switch-item>
</mmm-content-switch>
### Vier Karten — stapeln automatisch, wenn die Zeile zu eng wird
<mmm-content-switch label="Konto" [(value)]="account">
<mmm-content-switch-item value="giro">
<span mmmTitle>Girokonto</span>
<span mmmDescription>Täglicher Zahlungsverkehr</span>
<mmm-icon mmmIcon>bank_48</mmm-icon>
</mmm-content-switch-item>
<mmm-content-switch-item value="tages">
<span mmmTitle>Tagesgeld</span>
<span mmmDescription>Jederzeit verfügbar</span>
<mmm-icon mmmIcon>bank_48</mmm-icon>
</mmm-content-switch-item>
<mmm-content-switch-item value="depot">
<span mmmTitle>Wertpapierdepot</span>
<span mmmDescription>Fonds und Aktien</span>
<mmm-icon mmmIcon>hochhaus_48</mmm-icon>
</mmm-content-switch-item>
<mmm-content-switch-item value="bau">
<span mmmTitle>Baufinanzierung</span>
<span mmmDescription>Für Ihr Eigenheim</span>
<mmm-icon mmmIcon>hochhaus_48</mmm-icon>
</mmm-content-switch-item>
</mmm-content-switch>
### Kompakt
<mmm-content-switch compact label="Zeitraum" [(value)]="range">
<mmm-content-switch-item value="month"
><span mmmTitle>Monat</span></mmm-content-switch-item
>
<mmm-content-switch-item value="quarter"
><span mmmTitle>Quartal</span></mmm-content-switch-item
>
<mmm-content-switch-item value="year"
><span mmmTitle>Jahr</span></mmm-content-switch-item
>
<mmm-content-switch-item value="custom" disabled
><span mmmTitle>Frei wählbar</span></mmm-content-switch-item
>
</mmm-content-switch>
### Kompakt, Icon-Only
<mmm-content-switch compact label="Darstellung" [(value)]="display">
<mmm-content-switch-item value="list" icon>
<mmm-icon mmmIcon aria-label="Liste">liste_24</mmm-icon>
</mmm-content-switch-item>
<mmm-content-switch-item value="map" icon>
<mmm-icon mmmIcon aria-label="Karte">allgemeine_karte_24</mmm-icon>
</mmm-content-switch-item>
</mmm-content-switch>
### Navigation
<mmm-content-switch navigation label="Bereiche">
<mmm-content-switch-item value="overview" href="/reports">
<span mmmTitle>Übersicht</span>
</mmm-content-switch-item>
<mmm-content-switch-item
value="detail"
href="/reports/detail"
activeMatch="prefix"
>
<span mmmTitle>Details</span>
</mmm-content-switch-item>
</mmm-content-switch>