Carousel
oge-carousel is a slide show — an image gallery, a hero banner, a product row — with previous / next buttons, a dot or thumbnail picker, looping and auto-rotation.
It follows the WAI-ARIA APG carousel pattern: a region with aria-roledescription="carousel", slides named "2 of 5", the off-screen slides inert, a tab-list picker with one slide per view and a button row with several. Rotation pauses on hover, stops when keyboard focus enters, and always has a visible rotation control (WCAG 2.2.2). The track is a scroll-snap container moved by script and by swipes on the shared pointer gesture, so touch, mouse and RTL all land on whole slides.
Basics
Data-driven slides draw an image and a frosted caption. The previous / next buttons are aria-disabled at the ends (they keep focus), the dots are an APG tab list (arrow keys, Home, End — mirrored in RTL), and a swipe or a mouse drag lands on the nearest slide past a fifth of its width.
Showing slide 1
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeCarousel } from '@oge-ui/layout';
import type { OgeCarouselItem } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCarousel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- A named region with aria-roledescription="carousel"; every slide is a
group named "Turquoise Coast, 1 of 3", and the slides out of view are
inert. The dots are an APG tab list: arrows, Home and End. -->
<oge-carousel
[items]="destinations"
[(selectedIndex)]="index"
[height]="320"
ariaLabel="Featured destinations"
/>
<p>Showing slide {{ index() + 1 }}</p>
`,
})
export class Demo {
protected readonly index = signal(0);
protected readonly destinations: OgeCarouselItem[] = [
{
key: 'coast',
image: '/demo/carousel/coast.svg',
imageAlt: 'A palm tree on a sandy beach by a calm blue sea',
title: 'Turquoise Coast',
description: 'Seven days of coves, ruins and slow mornings.',
},
{
key: 'mountains',
image: '/demo/carousel/mountains.svg',
imageAlt: 'Snow-capped violet peaks under a pink evening sky',
title: 'Kaçkar Mountains',
description: 'Highland villages and the trail to the glacier lakes.',
},
{
key: 'city',
image: '/demo/carousel/city.svg',
imageAlt: 'A city skyline with lit windows at sunset',
title: 'Istanbul by night',
description: 'Rooftops, ferries and the Bosphorus after dark.',
},
];
}Thumbnails & loop
indicators="thumbnails" draws a scrollable strip of decorative thumbnails — each tab is still named “Slide 2”. loop wraps the buttons, the picker keys and swipes from the last slide back to the first; the carousel rewinds rather than cloning slides, so assistive technology always sees the real count.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCarousel } from '@oge-ui/layout';
import type { OgeCarouselItem } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCarousel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Thumbnails come from each item's thumbnail (or its image) and are
decorative: the tab is named "Slide 2". loop wraps the buttons, the
picker keys and swipes from the last slide back to the first. -->
<oge-carousel
[items]="destinations"
indicators="thumbnails"
[loop]="true"
[height]="300"
ariaLabel="Destinations with thumbnails"
/>
`,
})
export class Demo {
protected readonly destinations: OgeCarouselItem[] = [
{
key: 'coast',
image: '/demo/carousel/coast.svg',
imageAlt: 'A palm tree on a sandy beach by a calm blue sea',
title: 'Turquoise Coast',
description: 'Seven days of coves, ruins and slow mornings.',
},
{
key: 'mountains',
image: '/demo/carousel/mountains.svg',
imageAlt: 'Snow-capped violet peaks under a pink evening sky',
title: 'Kaçkar Mountains',
description: 'Highland villages and the trail to the glacier lakes.',
},
{
key: 'city',
image: '/demo/carousel/city.svg',
imageAlt: 'A city skyline with lit windows at sunset',
title: 'Istanbul by night',
description: 'Rooftops, ferries and the Bosphorus after dark.',
},
];
}Autoplay & rotation control
The rotation control is rendered first and always visible. Hovering pauses the rotation, a hidden tab holds it, and keyboard focus entering the carousel stops it until the user presses play — the APG rule. While it rotates the slides are aria-live="off"; once stopped, polite. Under prefers-reduced-motion it starts stopped.
No events yet
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeCarousel } from '@oge-ui/layout';
import type { OgeCarouselAutoplayChangedEvent, OgeCarouselItem } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCarousel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- WCAG 2.2.2: the rotation control is always rendered and comes first.
Hovering pauses; keyboard focus entering the carousel stops it until the
user presses play. The slides region is aria-live="off" while it
rotates and "polite" once stopped. Under prefers-reduced-motion it
starts stopped. -->
<oge-carousel
#carousel
[items]="destinations"
[autoplay]="true"
[autoplayInterval]="4000"
[height]="300"
ariaLabel="Rotating highlights"
(slideChanged)="log('slide ' + ($event.index + 1) + ' via ' + $event.source)"
(autoplayChanged)="onAutoplay($event)"
/>
<button type="button" (click)="carousel.play()">play()</button>
<button type="button" (click)="carousel.pause()">pause()</button>
<button type="button" (click)="carousel.goTo(0)">goTo(0)</button>
<p>{{ last() }}</p>
`,
})
export class Demo {
protected readonly last = signal('No events yet');
protected readonly destinations: OgeCarouselItem[] = [
{
key: 'coast',
image: '/demo/carousel/coast.svg',
imageAlt: 'A palm tree on a sandy beach by a calm blue sea',
title: 'Turquoise Coast',
description: 'Seven days of coves, ruins and slow mornings.',
},
{
key: 'mountains',
image: '/demo/carousel/mountains.svg',
imageAlt: 'Snow-capped violet peaks under a pink evening sky',
title: 'Kaçkar Mountains',
description: 'Highland villages and the trail to the glacier lakes.',
},
{
key: 'city',
image: '/demo/carousel/city.svg',
imageAlt: 'A city skyline with lit windows at sunset',
title: 'Istanbul by night',
description: 'Rooftops, ferries and the Bosphorus after dark.',
},
];
protected log(message: string): void {
this.last.set(message);
}
protected onAutoplay(event: OgeCarouselAutoplayChangedEvent): void {
this.log((event.playing ? 'playing' : 'stopped') + ' by ' + event.source);
}
}Several slides per view
With slidesPerView above one the picker becomes the APG grouped carousel's row of buttons — one per reachable position, the current one aria-current — because a dot no longer maps to one slide. [ogeCarouselSlideTemplate] replaces the image and caption; its context says whether the slide is in view.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCarousel, OgeCarouselSlideTemplate } from '@oge-ui/layout';
import type { OgeCarouselItem } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCarousel, OgeCarouselSlideTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Several slides per view: the picker becomes a row of buttons (one
per reachable position, aria-current on the current one), because a
dot no longer maps to one slide. The template draws the content only;
the carousel keeps the group role and the "N of M" name. -->
<oge-carousel
[items]="products"
[slidesPerView]="3"
[gap]="16"
ariaLabel="Recommended products"
>
<ng-template ogeCarouselSlideTemplate let-item let-active="active">
<div class="product">
<strong>{{ item.title }}</strong>
<span>{{ item.description }}</span>
<button type="button" [disabled]="!active">Add to basket</button>
</div>
</ng-template>
</oge-carousel>
`,
})
export class Demo {
protected readonly products: OgeCarouselItem[] = [
{ key: 'p1', title: 'Desk lamp', description: '€49 · warm white' },
{ key: 'p2', title: 'Notebook', description: '€12 · dotted, A5' },
{ key: 'p3', title: 'Headphones', description: '€129 · noise cancelling' },
{ key: 'p4', title: 'Mug', description: '€15 · stoneware' },
{ key: 'p5', title: 'Backpack', description: '€89 · 20 litres' },
];
}Declarative slides
<oge-carousel-slide> children project any content and render before the items slides. Their label names the slide (“Version 1.3, 1 of 3”). Off-screen slides are inert, so the link in a hidden slide never takes a Tab stop.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCarousel, OgeCarouselSlide } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCarousel, OgeCarouselSlide],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Declarative slides project any content. Off-screen slides are inert,
so the link in a hidden slide never steals a Tab stop. -->
<oge-carousel ariaLabel="Release notes" [height]="200">
<oge-carousel-slide label="Version 1.3">
<h3>Version 1.3</h3>
<p>Carousel, action sheet, list and data views, tile layouts.</p>
<a href="#declarative-slides">Read the notes</a>
</oge-carousel-slide>
<oge-carousel-slide label="Version 1.2">
<h3>Version 1.2</h3>
<p>Avatars, badges, chips, alerts, timelines and app bars.</p>
</oge-carousel-slide>
<oge-carousel-slide label="Version 1.1">
<h3>Version 1.1</h3>
<p>The React render layer.</p>
</oge-carousel-slide>
</oge-carousel>
`,
})
export class Demo {}