Carousel
Displays multiple content items in one space, rotating through them.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
✨ Features
- Follows WAI-ARIA design pattern
- Full keyboard navigation
- Dynamic slide offsetting
- Customizable alignment (start, center, end)
- Pagination with bullet navigation
- Navigate via Previous/Next buttons
- Autoplay functionality
- Looping option
- Support for multiple slides per view
- Reactive slide updates
- Initial slide selection
- Customizable accessible names
- Supports scroller and conditional carousels
- Improve test coverage
- Enhance documentation
- Refine API for consistency with other components
- Add support for vertical carousels
Roadmap
CSS Scroll snapping
Qwik UI combines CSS scroll snapping and flexbox for the carousel:
- Scroll snapping: Used on mobile for smooth touch interactions and initial snap position.
- Flexbox: Provides a simple layout system for variable widths, gaps, and columns.
@layer qwik-ui {
[data-qui-carousel-scroller] {
overflow: hidden;
display: flex;
gap: var(--gap);
/* for mobile & scroll-snap-start */
scroll-snap-type: x mandatory;
}
[data-qui-carousel-slide] {
/* default, feel free to override */
--total-gap-width: calc(var(--gap) * (var(--slides-per-view) - 1));
--available-slide-width: calc(100% - var(--total-gap-width));
--slide-width: calc(var(--available-slide-width) / var(--slides-per-view));
flex-basis: var(--slide-width);
flex-shrink: 0;
}
@media (pointer: coarse) {
[data-qui-carousel-scroller][data-draggable] {
overflow-x: scroll;
}
/* make sure snap align is added after initial index animation */
[data-draggable][data-initial-touch] [data-qui-carousel-slide] {
scroll-snap-align: start;
}
[data-draggable][data-align='center'][data-initial-touch] [data-qui-carousel-slide] {
scroll-snap-align: center;
}
[data-draggable][data-align='end'][data-initial-touch] [data-qui-carousel-slide] {
scroll-snap-align: end;
}
}
}Pagination
Use <Carousel.Pagination /> and <Carousel.Bullet /> components to add pagination.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
<Carousel.Pagination class="carousel-pagination">
{colors.map((color, index) => (
<Carousel.Bullet class="carousel-pagination-bullet" key={color}>
{index + 1}
</Carousel.Bullet>
))}
</Carousel.Pagination>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
Multiple Slides
Set the slidesPerView prop for multiple slides.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" slidesPerView={3} gap={30}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
<Carousel.Pagination class="carousel-pagination">
{colors.map((color, index) => (
<Carousel.Bullet class="carousel-pagination-bullet" key={color}>
{index + 1}
</Carousel.Bullet>
))}
</Carousel.Pagination>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
Non-draggable
Opt-out of the draggable behavior by setting the draggable prop to false.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" draggable={false}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
Different widths
By default, the slides will take up the full width of the carousel.
To change this, use the flex-basis CSS property on the <Carousel.Slide /> component.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
return (
<Carousel.Root class="carousel-root" gap={30}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
<Carousel.Slide style={{ flexBasis: '200px' }} class="carousel-slide">
red
</Carousel.Slide>
<Carousel.Slide style={{ flexBasis: '400px' }} class="carousel-slide">
green
</Carousel.Slide>
<Carousel.Slide style={{ flexBasis: '300px' }} class="carousel-slide">
blue
</Carousel.Slide>
<Carousel.Slide style={{ flexBasis: '350px' }} class="carousel-slide">
yellow
</Carousel.Slide>
<Carousel.Slide style={{ flexBasis: '100px' }} class="carousel-slide">
purple
</Carousel.Slide>
</Carousel.Scroller>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
Without Scroller
Qwik UI supports carousels without a scroller, which can be useful for conditional slide carousels.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
Remove the <Carousel.Scroller /> component to remove the scroller.
Animations
Conditional Slides
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<div class="carousel-conditional">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</div>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
.carousel-conditional {
position: relative;
height: 200px;
}
.carousel-conditional .carousel-slide {
opacity: 0;
transition: opacity 0.5s;
/* NOT display block */
display: revert;
position: absolute;
inset: 0;
}
.carousel-conditional .carousel-slide[data-active] {
opacity: 1;
}CSR
Both SSR and CSR are supported. In this example, we conditionally render the carousel based on an interaction.
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
const renderCarousel = useSignal(false);
return (
<>
<button onClick$={() => (renderCarousel.value = !renderCarousel.value)}>
Render Carousel
</button>
{renderCarousel.value && (
<Carousel.Root class="carousel-root">
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
)}
</>
);
});
// internal
import styles from './carousel.css?inline';
Center
Align slides to the center of the carousel by setting the align prop to center.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30} align="center">
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide
style={{ flexBasis: '300px' }}
key={color}
class="carousel-slide"
>
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
End
Align slides to the end of the carousel by setting the align prop to end.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30} align="end">
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide
style={{ flexBasis: '300px' }}
key={color}
class="carousel-slide"
>
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
Loop
Loop the carousel by setting the loop prop to true.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30} loop>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
<Carousel.Pagination class="carousel-pagination">
{colors.map((color, index) => (
<Carousel.Bullet class="carousel-pagination-bullet" key={color}>
{index + 1}
</Carousel.Bullet>
))}
</Carousel.Pagination>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
Accessible Name
Add an accessible name to the carousel by adding the <Carousel.Title /> component.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30}>
<Carousel.Title>Favorite Colors</Carousel.Title>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
To hide the title from screen readers, use the <VisuallyHidden /> component.
Autoplay
To use autoplay, use the bind:autoplay prop.
isPlaying: false
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
import { LuPause, LuPlay } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
const isPlaying = useSignal<boolean>(false);
return (
<>
<Carousel.Root
class="carousel-root"
gap={30}
autoPlayIntervalMs={3500}
bind:autoplay={isPlaying}
>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Player>{isPlaying.value ? <LuPause /> : <LuPlay />}</Carousel.Player>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color, index) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
<div>{index === 1 && <button>I stop autoplay on focus!</button>}</div>
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
<p>isPlaying: {isPlaying.value.toString()}</p>
<button onClick$={() => (isPlaying.value = !isPlaying.value)}>
Toggle autoplay
</button>
</>
);
});
// internal
import styles from './carousel.css?inline';
What if I want to autoplay on initial render?
Use a visible task to change the signal passed to bind:autoplay to true when the component is visible.
{/* inside your component */}
useVisibleTask$(() => {
isAutoplaySig.value = true;
})
{/* the carousel */}
<Carousel.Root bind:autoplay={isAutoplaySig}>Initial
To set an initial slide position, use the startIndex prop.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
return (
<Carousel.Root class="carousel-root" gap={30} startIndex={4}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
);
});
// internal
import styles from './carousel.css?inline';
Reactive
Reactively control the selected slide index by using the bind:selectedIndex prop.
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Carousel } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink'];
const selectedIndex = useSignal<number>(0);
return (
<>
<Carousel.Root class="carousel-root" gap={30} bind:selectedIndex={selectedIndex}>
<div class="carousel-buttons">
<Carousel.Previous>Prev</Carousel.Previous>
<Carousel.Next>Next</Carousel.Next>
</div>
<Carousel.Scroller class="carousel-scroller">
{colors.map((color) => (
<Carousel.Slide key={color} class="carousel-slide">
{color}
</Carousel.Slide>
))}
</Carousel.Scroller>
</Carousel.Root>
<button>Selected Index: {selectedIndex.value}</button>
<button onClick$={() => (selectedIndex.value = 4)}>Change to index 4</button>
</>
);
});
// internal
import styles from './carousel.css?inline';
API
Carousel.Root
| Prop | Type | Description |
|---|---|---|
gap | number | The gap between slides. |
slidesPerView | number | Number of slides to show at once. |
draggable | boolean | Whether the carousel is draggable. |
align | union"start" | "center" | "end" | Alignment of slides within the viewport. |
loop | boolean | Whether the carousel should loop. |
bind:selectedIndex | Signal<number> | Bind the selected index to a signal. |
startIndex | number | Change the initial index of the carousel on render. |
bind:autoplay | Signal<boolean> | Whether the carousel should autoplay. |
autoPlayIntervalMs | number | Time in milliseconds before the next slide plays during autoplay. |