How to Use the Astryx Carousel Component for Image/Content Rotation

The Astryx Carousel component is a flexible, accessible horizontal-scroll container that automatically adds fade-edge masks, navigation buttons, and RTL support while exposing imperative methods like scrollNext() and scrollTo() for programmatic control.

The Astryx Carousel component in the facebook/astryx repository provides a production-ready solution for displaying horizontally scrollable content rows without consuming full viewport width. Built with StyleX for theme-aware styling and comprehensive accessibility features, this component handles overflow indication, navigation controls, and keyboard interactions out of the box.

The Carousel is exported from the core package entry point defined in packages/core/src/Carousel/index.ts. Import the component and its TypeScript definitions directly from the package:

import {Carousel} from '@astryxdesign/core';
import type {CarouselHandle} from '@astryxdesign/core';

Basic Implementation

At its core, the Carousel wraps children in a scrollable container. In packages/core/src/Carousel/Carousel.tsx, the component renders a div with overflow-x: auto (referenced internally as styles.scroller) that enables native horizontal scrolling:

import {Carousel} from '@astryxdesign/core';
import {Thumbnail} from '@astryxdesign/core';

export function ImageCarousel() {
  return (
    <Carousel gap={1} aria-label="Featured photos">
      <Thumbnail src="/images/a.jpg" alt="Photo A" />
      <Thumbnail src="/images/b.jpg" alt="Photo B" />
      <Thumbnail src="/images/c.jpg" alt="Photo C" />
    </Carousel>
  );
}

The gap prop applies spacing between items using StyleX tokens, while aria-label defines the accessible name for the carousel region.

Core Architecture and Scroll Mechanics

The component's architecture centers on the useScrollOverflow hook, which monitors the scrollable container's dimensions to drive UI state. This hook determines when content overflows its bounds, triggering the visibility of fade-edge masks and navigation buttons.

Overflow Indicators and Fade Masks

When content exceeds the visible area, the Carousel applies CSS-based masks to indicate scrollability. The implementation in Carousel.tsx utilizes three StyleX style objects:

  • styles.fadeStart: Gradient mask at the left edge
  • styles.fadeEnd: Gradient mask at the right edge
  • styles.fadeBoth: Masks applied to both edges when scrolling is possible in either direction

These masks provide visual affordances without requiring additional DOM elements.

Input Handling and Scroll Behavior

The Carousel supports multiple input methods through event handlers defined in the source:

  • Native trackpad gestures leverage the browser's standard horizontal scroll behavior on the styles.scroller element.
  • Mouse wheel support maps vertical wheel deltas to horizontal scrolling via the handleWheel callback, enabling horizontal navigation when users hold Shift while scrolling.

Navigation buttons trigger the scrollBy(direction) function, which respects RTL layouts and calculates scroll distance based on item dimensions and the gap prop.

By default, the Carousel renders Previous and Next buttons that appear only when scrolling is possible in the respective direction, as determined by canScrollPrev() and canScrollNext() logic within the useScrollOverflow hook.

Enable infinite looping by setting the hasLoop prop to true. When activated, the scrollBy function wraps around: clicking Next at the end scrolls to the first item, and Previous at the start jumps to the last item, keeping both buttons persistently visible.

import {Carousel} from '@astryxdesign/core';
import {Card} from '@astryxdesign/core';

export function ProductCarousel() {
  return (
    <Carousel
      gap={2}
      hasSnap
      hasLoop
      padding={2}
      aria-label="Featured products"
    >
      <Card padding={4}>Product 1</Card>
      <Card padding={4}>Product 2</Card>
      <Card padding={4}>Product 3</Card>
      <Card padding={4}>Product 4</Card>
    </Carousel>
  );
}

The hasSnap prop enables CSS scroll-snap for precise item alignment during scrolling.

Imperative Control with Handle Refs

For programmatic navigation, the Carousel exposes an imperative handle via the handleRef prop. This ref provides a CarouselHandle interface with five methods defined in packages/core/src/Carousel/Carousel.tsx:

  • scrollNext(): Advances to the next item (respects hasLoop)
  • scrollPrev(): Returns to the previous item
  • scrollTo(index): Jumps to a specific zero-based index
  • canScrollNext(): Returns boolean indicating if next scroll is possible
  • canScrollPrev(): Returns boolean indicating if previous scroll is possible
import {useRef} from 'react';
import {Carousel, type CarouselHandle} from '@astryxdesign/core';

export function ControlledCarousel() {
  const carouselRef = useRef<CarouselHandle>(null);

  const goToThird = () => {
    carouselRef.current?.scrollTo(2); // zero-based index
  };

  return (
    <>
      <button onClick={goToThird}>Go to slide 3</button>
      <Carousel handleRef={carouselRef} hasButtons={false} aria-label="Gallery">
        {/* slides */}
      </Carousel>
    </>
  );
}

This pattern is useful for syncing external controls or deep-linking to specific slides.

Accessibility Implementation

The Carousel implements the WAI-ARIA Carousel pattern with semantic attributes set in packages/core/src/Carousel/Carousel.tsx:

  • The root element carries role="region", aria-roledescription="carousel", and an aria-label (defaulting to "Carousel")
  • Each child slide wrapper receives role="group", aria-roledescription="slide", and an accessible label like "Slide 2 of 5" generated via the translator utility
  • Navigation buttons maintain visible focus indicators and respect reduced-motion preferences

These attributes ensure screen reader users understand the component's structure and their current position within the rotation.

Configuration Options

Control the Carousel's appearance and behavior through these props:

  • gap: Space between items (number, theme token multiplier)
  • padding: Internal container padding (number, theme token multiplier)
  • hasSnap: Boolean enabling CSS scroll-snap alignment
  • hasLoop: Boolean enabling infinite scroll wrapping
  • hasButtons: Boolean toggling Prev/Next navigation buttons (default: true)
  • hasEdgeFade: Boolean toggling overflow gradient masks (default: true)
  • handleRef: React ref exposing CarouselHandle for imperative control

To create a minimal carousel without auxiliary UI elements:

import {Carousel} from '@astryxdesign/core';

export function SimpleCarousel() {
  return (
    <Carousel hasButtons={false} hasEdgeFade={false} aria-label="Team members">
      {/* slides */}
    </Carousel>
  );
}

Summary

  • The Astryx Carousel component provides a horizontal-scroll container with automatic overflow detection and visual fade masks defined in packages/core/src/Carousel/Carousel.tsx.
  • Navigation supports trackpad gestures, Shift+mouse wheel, and optional Prev/Next buttons with RTL adaptation and hasLoop wrapping.
  • Imperative control is available via handleRef exposing scrollNext(), scrollPrev(), scrollTo(index), and state-checking methods.
  • Accessibility includes role="region", aria-roledescription="carousel", and slide-specific labels for screen reader compatibility.
  • StyleX drives all styling, making the component theme-aware and token-driven.

Frequently Asked Questions

Use the imperative handleRef API. Create a ref with useRef<CarouselHandle>(null), pass it to the handleRef prop, and call ref.current?.scrollTo(index) where index is zero-based. This method is defined in packages/core/src/Carousel/Carousel.tsx and respects the hasLoop configuration.

Yes, the Carousel automatically adapts to RTL layouts. The scrollBy function and navigation button chevrons adjust their direction based on document direction. The underlying scroll behavior and fade masks (styles.fadeStart/styles.fadeEnd) swap positions to match the reading direction.

How do I disable the navigation buttons and edge fade effects?

Set the hasButtons and hasEdgeFade props to false. This removes the Prev/Next button overlay and the CSS gradient masks (styles.fadeBoth) while preserving the horizontal scroll functionality and accessibility attributes.

According to the source code in packages/core/src/Carousel/Carousel.tsx, the root element has role="region" and aria-roledescription="carousel", while each slide receives role="group" and aria-roledescription="slide" with generated labels indicating position (e.g., "Slide 2 of 5"). The component also respects reduced-motion preferences and provides keyboard-navigable controls.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →