PixelCarousel

Embla-powered surface-aware carousel with horizontal or vertical orientation, optional arrows and dot pagination, keyboard navigation and reduced-motion support.

  • Built on embla-carousel with full opts and plugins pass-through
  • Horizontal or vertical orientation with arrow and dot navigation
  • Keyboard navigation (Arrow keys) and aria-roledescription="carousel" landmark
  • Surface-aware (pixel or linear) with focus-ring tokens
  • Honors prefers-reduced-motion by zeroing transition duration
Status
stable
Category
data
Since
v1.9.0
import { PixelCarousel } from '@pxlkit/ui-kit';

PixelCarousel

PropTypeDefaultDescription
optsOmit<EmblaOptionsType, 'axis'>—Full embla options surface (see embla-carousel docs). Common subset: loop, align, slidesToScroll, startIndex, dragFree, containScroll, inViewThreshold. axis is set internally from orientation.
pluginsEmblaPluginType[]—Optional embla plugins (autoplay, autoScroll, etc.).
setApi(api: EmblaCarouselType | undefined) => void—Receives the embla API once ready; called again with undefined on unmount.
orientation'horizontal' | 'vertical''horizontal'Slides side by side, or stacked.
showArrowsbooleantruePrevious and next buttons.
showDotsbooleanfalseA dot per slide, to go to it.
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.
aria-labelstring—Accessible name for the carousel region (required for landmark navigation).
children (required)React.ReactNode—The slides (PixelCarousel.Item).

Also takes the native attributes and event handlers of <div> (HTMLAttributes<HTMLDivElement>).

ref points to <div>.

PixelCarousel.Item

PropTypeDefaultDescription
children (required)React.ReactNode—The slide's content.

Also takes the native attributes and event handlers of <div> (HTMLAttributes<HTMLDivElement>).

ref points to <div>.

WCAG target: 2.1 AA

  • carousel

Root element is role="region" with aria-roledescription="carousel" and a focusable tabIndex. Each item is role="group" with aria-roledescription="slide" and aria-label "Slide N of M". Previous/Next buttons declare aria-label and aria-controls pointing to the viewport id. A polite live region announces the current slide index without reading slide content (per APG carousel pattern). Dots expose aria-current="true" on the active slide.

Keyboard

KeyActionWhen
ArrowLeftScrolls to the previous slideorientation is horizontal
ArrowRightScrolls to the next slideorientation is horizontal
ArrowUpScrolls to the previous slideorientation is vertical
ArrowDownScrolls to the next slideorientation is vertical
TabMoves focus into and out of the carousel region—
import { PixelCarousel } from '@pxlkit/ui-kit';

function Slide({ label, tone }: { label: string; tone: string }) {
  return (
    <div
      className="flex h-32 items-center justify-center border border-retro-border bg-retro-surface text-retro-text"
      style={{ background: tone }}
    >
      <span className="text-xs">{label}</span>
    </div>
  );
}

export function Default() {
  return (
    <PixelCarousel aria-label="Featured items">
      <PixelCarousel.Item>
        <Slide label="Slide 1" tone="rgba(14,165,233,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Slide 2" tone="rgba(168,85,247,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Slide 3" tone="rgba(34,197,94,0.15)" />
      </PixelCarousel.Item>
    </PixelCarousel>
  );
}

Examples

Default

import { PixelCarousel } from '@pxlkit/ui-kit';

function Slide({ label, tone }: { label: string; tone: string }) {
  return (
    <div
      className="flex h-32 items-center justify-center border border-retro-border bg-retro-surface text-retro-text"
      style={{ background: tone }}
    >
      <span className="text-xs">{label}</span>
    </div>
  );
}

export function Default() {
  return (
    <PixelCarousel aria-label="Featured items">
      <PixelCarousel.Item>
        <Slide label="Slide 1" tone="rgba(14,165,233,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Slide 2" tone="rgba(168,85,247,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Slide 3" tone="rgba(34,197,94,0.15)" />
      </PixelCarousel.Item>
    </PixelCarousel>
  );
}

With dots

import { PixelCarousel } from '@pxlkit/ui-kit';

function Slide({ label, tone }: { label: string; tone: string }) {
  return (
    <div
      className="flex h-32 items-center justify-center border border-retro-border bg-retro-surface text-retro-text"
      style={{ background: tone }}
    >
      <span className="text-xs">{label}</span>
    </div>
  );
}

export function WithDots() {
  return (
    <PixelCarousel aria-label="Featured items with dots" showDots>
      <PixelCarousel.Item>
        <Slide label="One" tone="rgba(14,165,233,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Two" tone="rgba(168,85,247,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Three" tone="rgba(34,197,94,0.15)" />
      </PixelCarousel.Item>
    </PixelCarousel>
  );
}

Looping

import { PixelCarousel } from '@pxlkit/ui-kit';

function Slide({ label, tone }: { label: string; tone: string }) {
  return (
    <div
      className="flex h-32 items-center justify-center border border-retro-border bg-retro-surface text-retro-text"
      style={{ background: tone }}
    >
      <span className="text-xs">{label}</span>
    </div>
  );
}

export function Looping() {
  return (
    <PixelCarousel aria-label="Looping carousel" opts={{ loop: true }} showDots>
      <PixelCarousel.Item>
        <Slide label="Alpha" tone="rgba(14,165,233,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Beta" tone="rgba(168,85,247,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Gamma" tone="rgba(34,197,94,0.15)" />
      </PixelCarousel.Item>
    </PixelCarousel>
  );
}

Vertical

import { PixelCarousel } from '@pxlkit/ui-kit';

function Slide({ label, tone }: { label: string; tone: string }) {
  return (
    <div
      className="flex h-32 items-center justify-center border border-retro-border bg-retro-surface text-retro-text"
      style={{ background: tone }}
    >
      <span className="text-xs">{label}</span>
    </div>
  );
}

export function Vertical() {
  return (
    <div style={{ height: 240 }}>
      <PixelCarousel aria-label="Vertical carousel" orientation="vertical" showDots>
        <PixelCarousel.Item>
          <Slide label="Top" tone="rgba(14,165,233,0.15)" />
        </PixelCarousel.Item>
        <PixelCarousel.Item>
          <Slide label="Middle" tone="rgba(168,85,247,0.15)" />
        </PixelCarousel.Item>
        <PixelCarousel.Item>
          <Slide label="Bottom" tone="rgba(34,197,94,0.15)" />
        </PixelCarousel.Item>
      </PixelCarousel>
    </div>
  );
}

Linear surface

import { PixelCarousel } from '@pxlkit/ui-kit';

function Slide({ label, tone }: { label: string; tone: string }) {
  return (
    <div
      className="flex h-32 items-center justify-center border border-retro-border bg-retro-surface text-retro-text"
      style={{ background: tone }}
    >
      <span className="text-xs">{label}</span>
    </div>
  );
}

export function LinearSurface() {
  return (
    <PixelCarousel aria-label="Linear surface carousel" surface="linear" showDots>
      <PixelCarousel.Item>
        <Slide label="Slide 1" tone="rgba(14,165,233,0.15)" />
      </PixelCarousel.Item>
      <PixelCarousel.Item>
        <Slide label="Slide 2" tone="rgba(168,85,247,0.15)" />
      </PixelCarousel.Item>
    </PixelCarousel>
  );
}

Related