Skip to main content

Carousel Rail

Carousel Rail is a widget that scrolls through a horizontal row of items, such as featured projects or related articles.

Import

import { CarouselRail } from '@vortexlabs/vortex';
import type { CarouselRailProps } from '@vortexlabs/vortex';

Allied components

Other links

"use client";

import { CarouselRail, Stack, Text } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselRailExample = () => {
  return (
    <Stack width="100%" maxWidth="1024px">
      <CarouselRail ariaLabel="Featured projects">
        <Stack gap={6}>
          <Stack direction="row" align="center" justify="space-between" gap={4}>
            <Stack gap={1}>
              <Text as="h2" size="body-xl-desktop" weight="medium">
                Featured projects
              </Text>
              <Text size="body-sm-desktop" color="subtle">
                Recently updated work from your team.
              </Text>
            </Stack>
            <CarouselRail.Controls
              previousAriaLabel="Previous projects"
              nextAriaLabel="Next projects"
              size="lg"
            />
          </Stack>
          <CarouselRail.Viewport>
            {slides.map((value) => (
              <CarouselRail.Item key={value} value={value} width="300px">
                <Stack
                  height="260px"
                  backgroundColor="surface"
                  borderColor="default"
                  borderRadius="md"
                />
              </CarouselRail.Item>
            ))}
          </CarouselRail.Viewport>
        </Stack>
      </CarouselRail>
    </Stack>
  );
};

Features

  • Supports responsive item widths, alignment, and navigation groups.
  • Offers swipe navigation and external or overlay arrows.
  • Supports controlled selection, autoplay, and lazy mounting.
  • Supports RTL layouts and reduced-motion preferences.

Anatomy

<CarouselRail ariaLabel="Featured projects">
<CarouselRail.Controls previousAriaLabel="Previous projects" nextAriaLabel="Next projects" />
<CarouselRail.Viewport>
  <CarouselRail.Item value="atlas" width="300px">Atlas</CarouselRail.Item>
  <CarouselRail.Item value="nova" width="300px">Nova</CarouselRail.Item>
</CarouselRail.Viewport>
</CarouselRail>
Component Part
Description
Holds the active item and names the rail.
Scrolls to the previous or next group of items.
Scrolls the row of items.
Shows one item, identified by its value.

Responsive Items

itemsPerView sets how many items fit, and itemsPerScroll how many each step moves (a full view by default). Both are responsive. For mixed widths, set width on each item instead.

"use client";

import { CarouselRail, Placeholder, Stack } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselRailResponsiveExample = () => {
  return (
    <CarouselRail
      ariaLabel="Responsive items"
      itemsPerView={{ xs: 1, sm: 2, md: 3 }}
      itemsPerScroll={{ xs: 1, md: 3 }}
    >
      <Stack gap={3}>
        <Stack direction="row" justify="end">
          <CarouselRail.Controls previousAriaLabel="Previous slides" nextAriaLabel="Next slides" />
        </Stack>
        <CarouselRail.Viewport gap={{ xs: 2, md: 4 }} borderRadius={{ xs: "sm", md: "lg" }}>
          {slides.map((value) => (
            <CarouselRail.Item key={value} value={value}>
              <Placeholder height="180px" minWidth={0} borderRadius={{ xs: "sm", md: "lg" }}>
                Slide {value}
              </Placeholder>
            </CarouselRail.Item>
          ))}
        </CarouselRail.Viewport>
      </Stack>
    </CarouselRail>
  );
};

Alignment

align snaps the selected item to the start (default), center, or end.

"use client";

import { CarouselRail, Placeholder, Stack } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselRailAlignExample = () => {
  return (
    <CarouselRail ariaLabel="Centered cards" align="center" defaultValue="3" itemsPerScroll={1}>
      <Stack gap={3}>
        <Stack direction="row" justify="end">
          <CarouselRail.Controls previousAriaLabel="Previous card" nextAriaLabel="Next card" />
        </Stack>
        <CarouselRail.Viewport>
          {slides.map((value) => (
            <CarouselRail.Item key={value} value={value} width="240px">
              <Placeholder height="180px" minWidth={0}>
                Card {value}
              </Placeholder>
            </CarouselRail.Item>
          ))}
        </CarouselRail.Viewport>
      </Stack>
    </CarouselRail>
  );
};

Controls

Place Controls anywhere in your layout, or set position="overlay" to float them over the viewport. arrows="hover" shows them on hover or focus.

"use client";

import { CarouselRail, Placeholder } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselRailOverlayExample = () => {
  return (
    <CarouselRail ariaLabel="Overlay controls" itemsPerView={3}>
      <CarouselRail.Viewport>
        {slides.map((value) => (
          <CarouselRail.Item key={value} value={value}>
            <Placeholder height="180px" minWidth={0}>
              Slide {value}
            </Placeholder>
          </CarouselRail.Item>
        ))}
        <CarouselRail.Controls
          previousAriaLabel="Previous slides"
          nextAriaLabel="Next slides"
          position="overlay"
          arrows="hover"
          rounded
        />
      </CarouselRail.Viewport>
    </CarouselRail>
  );
};

Lazy Mounting

lazyMount mounts items as users reach them and keeps them mounted, so state like these notes survives. It needs itemsPerView or an item width. Give placeholder the content's height.

"use client";

import { CarouselRail, Placeholder, Stack, TextField } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselRailLazyExample = () => {
  return (
    <CarouselRail ariaLabel="Lazy mounting" itemsPerView={1} itemsPerScroll={1} lazyMount>
      <Stack gap={3}>
        <Stack direction="row" justify="end">
          <CarouselRail.Controls previousAriaLabel="Previous slides" nextAriaLabel="Next slides" />
        </Stack>
        <CarouselRail.Viewport>
          {slides.map((value) => (
            <CarouselRail.Item
              key={value}
              value={value}
              placeholder={
                <Placeholder height="180px" minWidth={0}>
                  Not mounted yet
                </Placeholder>
              }
            >
              <Placeholder height="180px" minWidth={0}>
                <TextField.Root>
                  <TextField.Label>Notes for slide {value}</TextField.Label>
                  <TextField.Input
                    name={`notes-${value}`}
                    placeholder="Type a note, then navigate away and back"
                  />
                </TextField.Root>
              </Placeholder>
            </CarouselRail.Item>
          ))}
        </CarouselRail.Viewport>
      </Stack>
    </CarouselRail>
  );
};

Controlled Selection

Control selection with value and onChange; defaultValue only sets the start. With a numeric itemsPerScroll, onChange reports the group's first item.

Selected slide: 1
"use client";

import { Button, CarouselRail, Placeholder, Stack, Text } from "@vortexlabs/vortex";
import { useState } from "react";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselRailControlledExample = () => {
  const [value, setValue] = useState("1");

  return (
    <Stack gap={3}>
      <Stack direction="row" gap={2} wrap>
        {slides.map((slide) => (
          <Button
            key={slide}
            size="sm"
            variant={value === slide ? "primary" : "secondary"}
            onClick={() => setValue(slide)}
            attributes={{ "aria-pressed": value === slide }}
          >
            Slide {slide}
          </Button>
        ))}
      </Stack>
      <CarouselRail
        ariaLabel="Controlled carousel"
        itemsPerView={1}
        itemsPerScroll={1}
        value={value}
        onChange={({ value }) => setValue(value)}
      >
        <Stack gap={3}>
          <Stack direction="row" justify="end">
            <CarouselRail.Controls
              previousAriaLabel="Previous slides"
              nextAriaLabel="Next slides"
            />
          </Stack>
          <CarouselRail.Viewport>
            {slides.map((value) => (
              <CarouselRail.Item key={value} value={value}>
                <Placeholder height="180px" minWidth={0}>
                  Slide {value}
                </Placeholder>
              </CarouselRail.Item>
            ))}
          </CarouselRail.Viewport>
        </Stack>
      </CarouselRail>
      <Text>Selected slide: {value}</Text>
    </Stack>
  );
};

For one item at a time, use CarouselGallery. autoPlay works as in Carousel Gallery.

Accessibility

Key
Description
TabShiftTab
Move forward or backward through focusable elements.
EnterSpace
Activate a focused arrow or playback button.
Announces the visible items after each move, but not during autoplay.
Name the rail with ariaLabel, and the arrows with previousAriaLabel and nextAriaLabel.
Set playLabel, pauseLabel, and renderStatusText in non-English apps; their defaults are English.