Skip to main content
Gremorie

Carousel

Horizontal or vertical scrollable slide region built on Embla. Touch, drag, keyboard, plugins.

Overview

Carousel is the sliding-content primitive: a region of slides that scrolls horizontally (default) or vertically, with drag, touch, keyboard arrows, and optional plugins (autoplay, wheel, fade). Built on Embla Carousel, it exposes a controlled API via setApi so you can read state (current slide, can-scroll-prev/next) and call methods (scrollTo, scrollNext) from outside.

Carousels hide content past the first slide - use only when order matters less than presence (galleries, testimonials, partner logos). For prioritized content, prefer a vertical list. Always pair arrow controls with keyboard navigation; any auto-rotation must be pausable.

Preview

1
2
3
4
5
'use client';import {  Card,  CardContent,  Carousel,  CarouselContent,  CarouselItem,  CarouselNext,  CarouselPrevious,} from '@gremorie/rx-display';export function CarouselPreview() {  return (    <Carousel className="w-full max-w-sm">      <CarouselContent>        {Array.from({ length: 5 }).map((_, i) => (          <CarouselItem key={i}>            <Card>              <CardContent className="flex aspect-square items-center justify-center p-6">                <span className="text-4xl font-semibold">{i + 1}</span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselPrevious />      <CarouselNext />    </Carousel>  );}

Anatomy

Carousel                       Embla root + context provider (orientation, opts, plugins)
├─ CarouselContent             overflow viewport + flex track
│  └─ CarouselItem             a single slide (basis-full by default)
├─ CarouselPrevious            previous-slide arrow button, auto-disabled at the start
└─ CarouselNext                next-slide arrow button, auto-disabled at the end

Installation

bash npx gremorie@latest add rx-carousel
bash pnpm dlx gremorie@latest add rx-carousel
bash yarn dlx gremorie@latest add rx-carousel
bash bunx --bun gremorie@latest add rx-carousel

Usage

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
} from "@gremorie/rx-display";

export function Example() {
  return (
    <Carousel className="w-full max-w-sm">
      <CarouselContent>
        {Array.from({ length: 5 }).map((_, i) => (
          <CarouselItem key={i}>
            <div className="p-1">Slide {i + 1}</div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  );
}
npx gremorie@latest add ng-carousel
import { Component } from '@angular/core';
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
} from '@gremorie/ng-display';

@Component({
  selector: 'app-example',
  standalone: true,
  imports: [
    Carousel,
    CarouselContent,
    CarouselItem,
    CarouselPrevious,
    CarouselNext,
  ],
  template: `
    <gr-carousel class="w-full max-w-xs">
      <gr-carousel-content>
        @for (slide of slides; track slide) {
          <gr-carousel-item>
            <div class="p-1">Slide {{ slide }}</div>
          </gr-carousel-item>
        }
      </gr-carousel-content>
      <gr-carousel-previous />
      <gr-carousel-next />
    </gr-carousel>
  `,
})
export class ExampleComponent {
  readonly slides = [1, 2, 3, 4, 5];
}

API

Root container. Sets up Embla, exposes context for CarouselContent, CarouselItem, CarouselPrevious, and CarouselNext.

PropTypeDefaultDescription
optsEmblaOptionsType-Embla options. Common keys: align: "start" | "center" | "end", loop: boolean, dragFree: boolean. See Embla docs.
pluginsEmblaPluginType[]-Embla plugins. Common: Autoplay, WheelGestures, ClassNames, Fade.
orientation"horizontal" | "vertical""horizontal"Sets the Embla axis and styles CarouselContent and arrow positions accordingly.
setApi(api: CarouselApi) => void-Receives the Embla API instance once initialized. Use to control the carousel from outside (call api.scrollTo(2), read api.selectedScrollSnap()).
classNamestring-Extra classes on the root div.

The root renders with role="region" and aria-roledescription="carousel" so AT announces it correctly.

<CarouselContent>

The viewport's scrolling track. Internal structure: an outer div.overflow-hidden (the viewport) plus an inner flex track. Uses -ml-4 (horizontal) or -mt-4 flex-col (vertical) to compensate for the per-item left/top padding.

<CarouselItem>

A single slide. Renders with role="group" and aria-roledescription="slide". By default each slide takes basis-full (one slide per view); change basis-1/2, basis-1/3, etc. on className to show multiple slides at once.

<CarouselPrevious> and <CarouselNext>

Pre-styled arrow buttons (built on Button from @gremorie/rx-forms with variant="outline" and size="icon"). Auto-disable when there is no previous/next slide. Position absolutely outside the carousel (-left-12 / -right-12).

PropTypeDefaultDescription
variantinherited from Button"outline"Visual style.
sizeinherited from Button"icon"Size token.
classNamestring-Extra classes - useful when the default -left-12 position clips.

All other Button props are forwarded.

CarouselApi

Type alias for the Embla API. Use with setApi:

const [api, setApi] = useState<CarouselApi | null>(null);

useEffect(() => {
  if (!api) return;
  api.on('select', () => {
    console.log('Current slide:', api.selectedScrollSnap());
  });
}, [api]);

<Carousel setApi={setApi}>...</Carousel>;

Composition

  1. <Carousel> owns Embla and provides context.
  2. <CarouselContent> is the scrolling track. Required wrapper around items.
  3. <CarouselItem> is each slide. Customize basis-* for multi-slide views.
  4. <CarouselPrevious> / <CarouselNext> are optional but recommended for keyboard and mouse users.

The Carousel registers a keydown handler on the root that scrolls on ArrowLeft / ArrowRight regardless of where focus is inside.

Variations

Multi-slide view

1
2
3
4
5
6
'use client';import {  Card,  CardContent,  Carousel,  CarouselContent,  CarouselItem,  CarouselNext,  CarouselPrevious,} from '@gremorie/rx-display';export function CarouselSizesPreview() {  return (    <Carousel className="w-full max-w-sm" opts={{ align: 'start' }}>      <CarouselContent className="-ml-2">        {Array.from({ length: 6 }).map((_, i) => (          <CarouselItem key={i} className="basis-1/3 pl-2">            <Card>              <CardContent className="flex aspect-square items-center justify-center p-3">                <span className="text-xl font-semibold">{i + 1}</span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselPrevious />      <CarouselNext />    </Carousel>  );}

Each CarouselItem defaults to basis-full (one slide per view). Set basis-1/2, basis-1/3, etc. to show multiple slides at once, and pair opts={{ align: 'start' }} so the track aligns cleanly.

1
2
3
4
5
'use client';import {  Card,  CardContent,  Carousel,  CarouselContent,  CarouselItem,  CarouselNext,  CarouselPrevious,} from '@gremorie/rx-display';export function CarouselVerticalPreview() {  return (    <Carousel      orientation="vertical"      opts={{ align: 'start' }}      className="w-full max-w-xs"    >      <CarouselContent className="-mt-2 h-[300px]">        {Array.from({ length: 5 }).map((_, i) => (          <CarouselItem key={i} className="basis-1/3 pt-2">            <Card>              <CardContent className="flex items-center justify-center p-6">                <span className="text-2xl font-semibold">{i + 1}</span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselPrevious />      <CarouselNext />    </Carousel>  );}

When orientation="vertical", set an explicit height on the inner CarouselContent - Embla needs bounded dimensions on the scroll axis. The arrow controls reposition to the top and bottom automatically.

With Embla plugin (autoplay)

import Autoplay from 'embla-carousel-autoplay';

<Carousel
  plugins={[Autoplay({ delay: 4000, stopOnInteraction: true })]}
  opts={{ loop: true }}
>
  <CarouselContent>...</CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>;

stopOnInteraction: true is non-negotiable - users who interact with the carousel should not have it keep moving under them.

Accessibility

  • Roles: root is role="region" with aria-roledescription="carousel"; each slide is role="group" with aria-roledescription="slide". Add an aria-label on the root identifying what the carousel shows ("Customer testimonials", "Product gallery").
  • Keyboard: ArrowLeft and ArrowRight scroll between slides (handled by the root keydown capture). Tab moves focus through the arrow controls and any focusable content inside slides.
  • Arrow controls: CarouselPrevious and CarouselNext ship with visually hidden text ("Previous slide", "Next slide") so AT users know what each button does. They auto-disable when no more slides are available.
  • Auto-rotation: if you use Autoplay, always pass stopOnInteraction: true and ensure prefers-reduced-motion users get a static experience (skip the plugin entirely or set delay: 0).
  • Live announcements: for galleries where the active slide changes meaning, wrap a sibling aria-live="polite" region that announces the current slide's title.
  • Card - frequent slide content.
  • Tabs - use when slides represent distinct contexts that users navigate to deliberately rather than browse.
  • Embla Carousel docs - full plugin and options reference.

On this page