Component

carousel

dx components add carousel

A slideshow you page through with prev/next buttons, arrow keys, or by dragging/scrolling the track.

Installation

Use the CLI command for the common path, or copy the component files manually.

Manual installation files

Copy the component source and CSS into your app. Import the shared theme CSS once near your app root.

use dioxus::prelude::*;
use dioxus_primitives::carousel::{self, CarouselContentProps, CarouselItemProps, CarouselPreviousProps};
use dioxus_primitives::direction::Direction;
use dioxus_primitives::{dioxus_attributes::attributes, merge_attributes};

// Re-exported so a demo (or a consumer's own page) can write
// `use crate::components::carousel::*;` and reach the orientation enum
// and the `use_carousel()`/`CarouselApi` escape hatch (for a custom
// indicator row, see the `indicators` variant) without a second import
// from `dioxus_primitives` directly -- the same convention `resizable`'s
// themed wrapper already follows for `ResizableDirection`.
#[allow(unused_imports)] // `CarouselApi` is only ever named as `use_carousel()`'s inferred
// return type in this file's own `CarouselIndicators` -- re-exported anyway so a
// consumer building their own custom picker can name the type explicitly.
pub use dioxus_primitives::carousel::{CarouselApi, CarouselOrientation, use_carousel};

/// The props for the [`Carousel`] component.
#[derive(Props, Clone, PartialEq)]
pub struct CarouselProps {
    /// The class of the carousel component.
    #[props(default)]
    pub class: String,

    /// The axis the carousel pages along.
    #[props(default)]
    pub orientation: ReadSignal<CarouselOrientation>,

    /// The controlled selected slide index.
    pub value: ReadSignal<Option<usize>>,

    /// The initial selected slide index when uncontrolled.
    #[props(default)]
    pub default_value: usize,

    /// Called whenever the selected slide changes.
    #[props(default)]
    pub on_value_change: Callback<usize>,

    /// The text direction for the root-level `ArrowLeft`/`ArrowRight` keys.
    pub dir: Option<Direction>,

    /// Additional attributes to apply to the carousel element.
    #[props(extends = GlobalAttributes)]
    pub attributes: Vec<Attribute>,

    /// The children of the carousel component.
    pub children: Element,
}

#[component]
pub fn Carousel(props: CarouselProps) -> Element {
    let base = attributes!(div {
        class: format!("{} {}", props.class, "dx-carousel"),
    });
    let merged = merge_attributes(vec![base, props.attributes]);

    rsx! {
        document::Link { rel: "stylesheet", href: asset!("/src/components/carousel/style.css") }
        carousel::Carousel {
            orientation: props.orientation,
            value: props.value,
            default_value: props.default_value,
            on_value_change: props.on_value_change,
            dir: props.dir,
            attributes: merged,
            {props.children}
        }
    }
}

#[component]
pub fn CarouselContent(props: CarouselContentProps) -> Element {
    let base = attributes!(div {
        class: "dx-carousel-content"
    });
    let merged = merge_attributes(vec![base, props.attributes]);

    rsx! {
        document::Link { rel: "stylesheet", href: asset!("/src/components/carousel/style.css") }
        carousel::CarouselContent {
            id: props.id,
            draggable: props.draggable,
            attributes: merged,
            {props.children}
        }
    }
}

#[component]
pub fn CarouselItem(props: CarouselItemProps) -> Element {
    let base = attributes!(div {
        class: "dx-carousel-item"
    });
    let merged = merge_attributes(vec![base, props.attributes]);

    rsx! {
        document::Link { rel: "stylesheet", href: asset!("/src/components/carousel/style.css") }
        carousel::CarouselItem {
            index: props.index,
            id: props.id,
            attributes: merged,
            {props.children}
        }
    }
}

#[component]
pub fn CarouselPrevious(props: CarouselPreviousProps) -> Element {
    let base = attributes!(button {
        class: "dx-carousel-previous"
    });
    let merged = merge_attributes(vec![base, props.attributes]);

    rsx! {
        document::Link { rel: "stylesheet", href: asset!("/src/components/carousel/style.css") }
        carousel::CarouselPrevious { attributes: merged, {props.children} }
    }
}

#[component]
pub fn CarouselNext(props: CarouselPreviousProps) -> Element {
    let base = attributes!(button {
        class: "dx-carousel-next"
    });
    let merged = merge_attributes(vec![base, props.attributes]);

    rsx! {
        document::Link { rel: "stylesheet", href: asset!("/src/components/carousel/style.css") }
        carousel::CarouselNext { attributes: merged, {props.children} }
    }
}

/// A row of dot indicators, one per slide, for jumping directly to any
/// slide -- composed entirely from [`use_carousel`]'s public
/// [`CarouselApi`], not a new primitive. Used by the `indicators` demo
/// variant; exported so any consumer can drop it in verbatim the way
/// `dx components add` copies the rest of this file.
#[component]
pub fn CarouselIndicators() -> Element {
    let api = use_carousel();

    rsx! {
        document::Link { rel: "stylesheet", href: asset!("/src/components/carousel/style.css") }
        div { class: "dx-carousel-indicators", role: "group", "aria-label": "Slide indicators",
            for i in 0..api.count {
                button {
                    key: "{i}",
                    r#type: "button",
                    class: "dx-carousel-indicator",
                    "data-active": i == api.selected,
                    "aria-label": "Go to slide {i + 1}",
                    "aria-current": if i == api.selected { "true" } else { "false" },
                    onclick: move |_| api.scroll_to(i),
                }
            }
        }
    }
}

Usage notes

The Carousel component is a slideshow of slides the user pages through with Previous/Next buttons, ArrowLeft/ArrowRight (or ArrowUp/ArrowDown when vertical), or by dragging/scrolling the track directly. It implements the WAI-ARIA Carousel pattern's "basic" (prev/next, no picker) style.

Component Structure

// CarouselPrevious/CarouselNext are positioned with CSS (absolutely, so
// DOM order doesn't affect where they appear), but come BEFORE
// CarouselContent in markup so they precede the slide content in the
// page's Tab order -- matching the APG reference implementation exactly.
Carousel {
    // An accessible name is required (aria-label or aria-labelledby) and
    // must not contain the word "carousel" -- the region's own
    // aria-roledescription already says that.
    aria_label: "Featured photos",

    CarouselPrevious { /* an icon or "Previous" */ }
    CarouselNext { /* an icon or "Next" */ }

    // The scroll-snap track.
    CarouselContent {
        // Each slide gets a 0-based index, contiguous from 0 -- the same
        // convention Tabs' TabTrigger/TabContent use for their own index.
        CarouselItem { index: 0usize, /* slide 1 content */ }
        CarouselItem { index: 1usize, /* slide 2 content */ }
        CarouselItem { index: 2usize, /* slide 3 content */ }
    }
}

Each CarouselItem defaults its own accessible name to "{n} of {m}" (APG's own sanctioned exception to "don't put position/size in an accessible name") unless you supply your own aria-label/aria-labelledby.

Sizing slides

How much of the track each slide occupies is a CSS decision, not a prop: CarouselItem defaults to flex: 0 0 100% (one full slide per view). Override it per item with an inline style (or flex_basis) to build a "peek"/multi-item-per-view layout -- see the multiple variant.

Orientation

orientation: CarouselOrientation::Vertical pages with ArrowUp/ArrowDown instead of ArrowLeft/ArrowRight, and scrolls on the block axis. A vertical carousel needs an explicit height on CarouselContent (e.g. style: "height: 20rem;") -- there is nothing else to derive one from, the same way ScrollArea needs an explicit height.

A custom picker (dot indicators, etc.)

use_carousel() returns a read-only CarouselApi (selected, count, can_scroll_prev, can_scroll_next) plus scroll_to(index), for building any picker UI alongside or instead of CarouselPrevious/CarouselNext. See the indicators variant's CarouselIndicators, or the indicators demo's own composition:

let api = use_carousel();
rsx! {
    for i in 0..api.count {
        button {
            "data-active": i == api.selected,
            onclick: move |_| api.scroll_to(i),
        }
    }
}

Pointer drag

Click-and-drag anywhere on the track pages the carousel with the mouse or a pen, the same way shadcn/embla-style carousels do -- touch is never affected either way, since it already scrolls the track natively. A drag has to move a few pixels before it takes over, so a plain click on a link or button placed inside a CarouselItem still works as a click; a real drag suppresses the synthetic click that would otherwise follow it. Releasing scrolls smoothly to the nearest slide through the same scrollIntoView paging path CarouselPrevious/CarouselNext already use -- not the browser's own scroll-snap re-settling on its own, which (measured) never animated -- and it still lands instantly if the OS/browser reports prefers-reduced-motion: reduce, matching every other transition in this component. That settle is picked up by the same bridge (use_carousel_scroll_tracking) that already keeps selected correct after a native trackpad/touch scroll or a CarouselPrevious/CarouselNext click, so the Previous/Next buttons' disabled state, the "N of M" slide labels, and a custom picker built on use_carousel() all stay correct after a drag too. See dev-docs/research/carousel-2026-09-19.md §6 for the full engineering rationale.

Set draggable: false on CarouselContent to opt a particular carousel out of the gesture entirely (it defaults to true).

Dragging past the first or last slide does not bounce back elastically -- a deliberate choice, not a gap: the disabled Previous/Next buttons already signal the boundary, elastic overscroll doesn't exist for a programmatic drag like this one, and it's a macOS/iOS compositor feature to begin with -- a native scroll-snap track doesn't rubber-band on Linux or Windows either, dragged or not.

Direction / RTL

Carousel accepts a dir: Option<Direction> prop (defaulting to the nearest DirectionProvider, or LTR). Under RTL, the root's ArrowLeft/ArrowRight paging swaps: ArrowLeft moves to the next slide, ArrowRight to the previous one (matching Radix's shared RovingFocusGroup convention, the same one Tabs follows). Scrolling itself needs no such swap at all -- slide order in the DOM never changes, and the browser's own scrollIntoView already resolves the correct physical position under dir="rtl". CarouselPrevious/CarouselNext also reposition correctly on their own (inset-inline-start/-end), and any chevron-style icon placed inside either one is automatically mirrored (transform: scaleX(-1), horizontal orientation only) so a caller who uses the same icon regardless of direction still gets one pointing the right way. See the rtl variant.

Pointer drag mirrors the same way the keyboard does: dragging is direct manipulation (the track tracks the pointer), so the physical direction that reveals the next slide flips under RTL -- swipe-left-for-next in LTR, swipe-right-for-next in RTL, the same split a right-to-left photo gallery or story viewer already has.

What v1 does not include yet

  • Infinite looping. CarouselPrevious/CarouselNext are genuinely disabled (native disabled, not just aria-disabled) at the first/last slide -- matching shadcn's own default carousel, which doesn't loop either.
  • Autoplay / a rotation control. The APG pattern's "basic" style has no rotation requirement at all, so a manually-paged carousel like this one is fully conformant on its own; a rotation control (plus the aria-live region and focus/hover-pause behavior that only matter once one exists) is a planned fast-follow.
  • A tablist picker variant (the APG pattern's "tabbed" style, slide picker = tabs). Planned as a fast-follow composed on Tabs' own roving-tabindex machinery.

Variants

Alternative examples for common configurations.

multiple

indicators

vertical

rtl