A scroll-snap carousel for Astro, Svelte, and React. Scroll position is the source of truth — native CSS scroll-snap drives all swipe and scroll behavior without JS hijacking. The component works with or without the optional prev/next buttons, dot indicators, and autoplay.
Start here: swipe, trackpad-pan, or wheel-scroll the carousel. Dots sync with the visible slide via IntersectionObserver, and keyboard navigation (arrow keys, Home, End) is wired to the viewport when focused.
The viewport is the source of truth: native CSS scroll-snap drives all swipe and scroll behavior without JS hijacking. Try swiping on touch, trackpad-panning, or just using the scroll wheel — the component stays in sync via IntersectionObserver.
---
// GENERATED from packages/demos/carousel/variants.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const slide = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "200px",
width: "100%",
borderRadius: "md",
fontSize: "3xl",
fontWeight: "bold",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Scroll-snap carousel</h3>
<p>
The viewport is the source of truth: native CSS <code>scroll-snap</code>{" "}
drives all swipe and scroll behavior without JS hijacking. Try swiping on
touch, trackpad-panning, or just using the scroll wheel — the component
stays in sync via <code>IntersectionObserver</code>.
</p>
<Carousel label="Example carousel" controls indicators>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="one"
label="Slide 1"
>
<div class={`${slide} ${paletteClasses[0]}`}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="two"
label="Slide 2"
>
<div class={`${slide} ${paletteClasses[1]}`}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="three"
label="Slide 3"
>
<div class={`${slide} ${paletteClasses[2]}`}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="four"
label="Slide 4"
>
<div class={`${slide} ${paletteClasses[3]}`}>4</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="five"
label="Slide 5"
>
<div class={`${slide} ${paletteClasses[4]}`}>5</div>
</CarouselItem>
</Carousel>
</div>
</div>Use orientation="horizontal" (default) or "vertical" to flip the scroll and snap axis. Vertical carousels bound their viewport height so scrolling stays contained within the component.
Snapping along the x-axis. Arrow keys navigate left/right when the viewport is focused.
Flips the snap axis to y. Viewport gets a bounded height so scrolling is contained; arrow keys switch to up/down.
---
// GENERATED from packages/demos/carousel/orientation.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
minHeight: "200px",
minWidth: "100%",
borderRadius: "md",
fontWeight: "bold",
fontSize: "xl",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Horizontal (default)</h3>
<p>
Snapping along the x-axis. Arrow keys navigate left/right when the
viewport is focused.
</p>
<Carousel
orientation="horizontal"
label="Horizontal carousel"
controls
indicators
>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="h1"
label="H1"
orientation="horizontal"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="h2"
label="H2"
orientation="horizontal"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="h3"
label="H3"
orientation="horizontal"
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="h4"
label="H4"
orientation="horizontal"
>
<div class={tile(3)}>4</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>Vertical</h3>
<p>
Flips the snap axis to y. Viewport gets a bounded height so scrolling is
contained; arrow keys switch to up/down.
</p>
<Carousel
orientation="vertical"
label="Vertical carousel"
controls
indicators
>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="v1"
label="V1"
orientation="vertical"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="v2"
label="V2"
orientation="vertical"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="v3"
label="V3"
orientation="vertical"
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="v4"
label="V4"
orientation="vertical"
>
<div class={tile(3)}>4</div>
</CarouselItem>
</Carousel>
</div>
</div>Control how many slides share the viewport with slidesPerView. Use 1 for full-width slides, 2 or 3 for grid-like layouts, or "auto" to let each slide size itself.
Each slide fills the viewport.
slidesPerView=2Two slides share the viewport; snap keeps pairs aligned.
slidesPerView=3Three per row — the classic product-carousel layout.
slidesPerView="auto"Items size themselves — useful when cards have intrinsic widths. Mix different widths freely.
slidesPerView works the same way along the y-axis.
---
// GENERATED from packages/demos/carousel/slides-per-view.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack, flex } from "@pindoba/styled-system/patterns";
const flexCell = css({ flex: "1 1 0", minWidth: "200px" });
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "140px",
borderRadius: "md",
fontWeight: "bold",
fontSize: "xl",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
const vBase = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
minHeight: "120px",
borderRadius: "md",
fontWeight: "bold",
fontSize: "xl",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const vTile = (i: number) =>
`${vBase} ${paletteClasses[i % paletteClasses.length]}`;
const autoBase = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "140px",
width: "240px",
borderRadius: "md",
fontWeight: "bold",
fontSize: "xl",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const autoTile = (i: number) =>
`${autoBase} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>One per view (default)</h3>
<p>Each slide fills the viewport.</p>
<Carousel label="One per view" controls indicators>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="s1"
label="Slide 1"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="s2"
label="Slide 2"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="s3"
label="Slide 3"
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="s4"
label="Slide 4"
>
<div class={tile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="s5"
label="Slide 5"
>
<div class={tile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="s6"
label="Slide 6"
>
<div class={tile(0)}>6</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="s7"
label="Slide 7"
>
<div class={tile(1)}>7</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="s8"
label="Slide 8"
>
<div class={tile(2)}>8</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>
<code>slidesPerView={2}</code>
</h3>
<p>Two slides share the viewport; snap keeps pairs aligned.</p>
<Carousel label="Two per view" slidesPerView={2} controls>
<CarouselItem
size="md"
align="start"
value="s1"
label="Slide 1"
slidesPerView={2}
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s2"
label="Slide 2"
slidesPerView={2}
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s3"
label="Slide 3"
slidesPerView={2}
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s4"
label="Slide 4"
slidesPerView={2}
>
<div class={tile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s5"
label="Slide 5"
slidesPerView={2}
>
<div class={tile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s6"
label="Slide 6"
slidesPerView={2}
>
<div class={tile(0)}>6</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s7"
label="Slide 7"
slidesPerView={2}
>
<div class={tile(1)}>7</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s8"
label="Slide 8"
slidesPerView={2}
>
<div class={tile(2)}>8</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>
<code>slidesPerView={3}</code>
</h3>
<p>Three per row — the classic product-carousel layout.</p>
<Carousel label="Three per view" slidesPerView={3} controls>
<CarouselItem
size="md"
align="start"
value="s1"
label="Slide 1"
slidesPerView={3}
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s2"
label="Slide 2"
slidesPerView={3}
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s3"
label="Slide 3"
slidesPerView={3}
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s4"
label="Slide 4"
slidesPerView={3}
>
<div class={tile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s5"
label="Slide 5"
slidesPerView={3}
>
<div class={tile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s6"
label="Slide 6"
slidesPerView={3}
>
<div class={tile(0)}>6</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s7"
label="Slide 7"
slidesPerView={3}
>
<div class={tile(1)}>7</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s8"
label="Slide 8"
slidesPerView={3}
>
<div class={tile(2)}>8</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>
<code>slidesPerView="auto"</code>
</h3>
<p>
Items size themselves — useful when cards have intrinsic widths. Mix
different widths freely.
</p>
<Carousel label="Auto per view" slidesPerView="auto" controls>
<CarouselItem
size="md"
align="start"
value="s1"
label="Slide 1"
slidesPerView="auto"
>
<div class={autoTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s2"
label="Slide 2"
slidesPerView="auto"
>
<div class={autoTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s3"
label="Slide 3"
slidesPerView="auto"
>
<div class={autoTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s4"
label="Slide 4"
slidesPerView="auto"
>
<div class={autoTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s5"
label="Slide 5"
slidesPerView="auto"
>
<div class={autoTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s6"
label="Slide 6"
slidesPerView="auto"
>
<div class={autoTile(0)}>6</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s7"
label="Slide 7"
slidesPerView="auto"
>
<div class={autoTile(1)}>7</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s8"
label="Slide 8"
slidesPerView="auto"
>
<div class={autoTile(2)}>8</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>Vertical orientation</h3>
<p>
<code>slidesPerView</code> works the same way along the y-axis.
</p>
<div class={flex({ gap: "md", wrap: "wrap" })}>
<div class={flexCell}>
<Carousel
label="Two per view vertical"
orientation="vertical"
slidesPerView={2}
controls
>
<CarouselItem
size="md"
align="start"
value="s1"
label="Slide 1"
orientation="vertical"
slidesPerView={2}
>
<div class={vTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s2"
label="Slide 2"
orientation="vertical"
slidesPerView={2}
>
<div class={vTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s3"
label="Slide 3"
orientation="vertical"
slidesPerView={2}
>
<div class={vTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s4"
label="Slide 4"
orientation="vertical"
slidesPerView={2}
>
<div class={vTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s5"
label="Slide 5"
orientation="vertical"
slidesPerView={2}
>
<div class={vTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s6"
label="Slide 6"
orientation="vertical"
slidesPerView={2}
>
<div class={vTile(0)}>6</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s7"
label="Slide 7"
orientation="vertical"
slidesPerView={2}
>
<div class={vTile(1)}>7</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s8"
label="Slide 8"
orientation="vertical"
slidesPerView={2}
>
<div class={vTile(2)}>8</div>
</CarouselItem>
</Carousel>
</div>
<div class={flexCell}>
<Carousel
label="Three per view vertical"
orientation="vertical"
slidesPerView={3}
controls
>
<CarouselItem
size="md"
align="start"
value="s1"
label="Slide 1"
orientation="vertical"
slidesPerView={3}
>
<div class={vTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s2"
label="Slide 2"
orientation="vertical"
slidesPerView={3}
>
<div class={vTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s3"
label="Slide 3"
orientation="vertical"
slidesPerView={3}
>
<div class={vTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s4"
label="Slide 4"
orientation="vertical"
slidesPerView={3}
>
<div class={vTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s5"
label="Slide 5"
orientation="vertical"
slidesPerView={3}
>
<div class={vTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s6"
label="Slide 6"
orientation="vertical"
slidesPerView={3}
>
<div class={vTile(0)}>6</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s7"
label="Slide 7"
orientation="vertical"
slidesPerView={3}
>
<div class={vTile(1)}>7</div>
</CarouselItem>
<CarouselItem
size="md"
align="start"
value="s8"
label="Slide 8"
orientation="vertical"
slidesPerView={3}
>
<div class={vTile(2)}>8</div>
</CarouselItem>
</Carousel>
</div>
</div>
</div>
</div>size tunes the gap between slides and the size of indicator dots. The prop cascades to items through CSS custom properties, so children inherit the same spacing without prop drilling.
Tighter gap and smaller indicator dots.
Size cascades to items through CSS custom properties — children inherit gap and indicator size without prop drilling.
---
// GENERATED from packages/demos/carousel/size.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "140px",
borderRadius: "md",
fontWeight: "bold",
fontSize: "xl",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Small</h3>
<p>Tighter gap and smaller indicator dots.</p>
<Carousel size="sm" label="Small" controls indicators>
<CarouselItem
size="sm"
slidesPerView={1}
align="start"
value="a"
label="Slide A"
>
<div class={tile(0)}>A</div>
</CarouselItem>
<CarouselItem
size="sm"
slidesPerView={1}
align="start"
value="b"
label="Slide B"
>
<div class={tile(1)}>B</div>
</CarouselItem>
<CarouselItem
size="sm"
slidesPerView={1}
align="start"
value="c"
label="Slide C"
>
<div class={tile(2)}>C</div>
</CarouselItem>
<CarouselItem
size="sm"
slidesPerView={1}
align="start"
value="d"
label="Slide D"
>
<div class={tile(3)}>D</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>Medium (default)</h3>
<Carousel size="md" label="Medium" controls indicators>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="a"
label="Slide A"
>
<div class={tile(0)}>A</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="b"
label="Slide B"
>
<div class={tile(1)}>B</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="c"
label="Slide C"
>
<div class={tile(2)}>C</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="d"
label="Slide D"
>
<div class={tile(3)}>D</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>Large</h3>
<p>
Size cascades to items through CSS custom properties — children inherit
gap and indicator size without prop drilling.
</p>
<Carousel size="lg" label="Large" controls indicators>
<CarouselItem
size="lg"
slidesPerView={1}
align="start"
value="a"
label="Slide A"
>
<div class={tile(0)}>A</div>
</CarouselItem>
<CarouselItem
size="lg"
slidesPerView={1}
align="start"
value="b"
label="Slide B"
>
<div class={tile(1)}>B</div>
</CarouselItem>
<CarouselItem
size="lg"
slidesPerView={1}
align="start"
value="c"
label="Slide C"
>
<div class={tile(2)}>C</div>
</CarouselItem>
<CarouselItem
size="lg"
slidesPerView={1}
align="start"
value="d"
label="Slide D"
>
<div class={tile(3)}>D</div>
</CarouselItem>
</Carousel>
</div>
</div>Enable controls to render circular Prev/Next buttons that reuse the Pindoba button component. Buttons auto-disable at the ends unless loop is set.
Optional controls render circular Prev/Next buttons that reuse the Button component. Buttons auto-disable at the ends when loop is off.
Set loop to wrap past the last slide back to the first. Buttons remain enabled at both ends.
---
// GENERATED from packages/demos/carousel/controls.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "160px",
borderRadius: "md",
fontSize: "2xl",
fontWeight: "bold",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Prev / Next buttons</h3>
<p>
Optional <code>controls</code> render circular Prev/Next buttons that
reuse the <code>Button</code> component. Buttons auto-disable at the ends
when <code>loop</code> is off.
</p>
<Carousel label="Controls example" controls>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="one"
label="Slide 1"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="two"
label="Slide 2"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="three"
label="Slide 3"
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="four"
label="Slide 4"
>
<div class={tile(3)}>4</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>Looping controls</h3>
<p>
Set <code>loop</code> to wrap past the last slide back to the first.
Buttons remain enabled at both ends.
</p>
<Carousel label="Looping controls" controls loop>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="one"
label="Slide 1"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="two"
label="Slide 2"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="three"
label="Slide 3"
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="four"
label="Slide 4"
>
<div class={tile(3)}>4</div>
</CarouselItem>
</Carousel>
</div>
</div>Set indicators to render a tablist of dots. Active state tracks scroll position, and clicking a dot smoothly scrolls to that slide.
Indicators double as a jump-to-slide control and a progress marker. They sync with scroll position via IntersectionObserver, so the active dot always matches the slide most in view.
Combine both for full-featured navigation.
---
// GENERATED from packages/demos/carousel/indicators.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "160px",
borderRadius: "md",
fontSize: "2xl",
fontWeight: "bold",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Dot indicators</h3>
<p>
Indicators double as a jump-to-slide control and a progress marker. They
sync with scroll position via <code>IntersectionObserver</code>, so the
active dot always matches the slide most in view.
</p>
<Carousel label="With indicators" indicators>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="one"
label="Slide 1"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="two"
label="Slide 2"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="three"
label="Slide 3"
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="four"
label="Slide 4"
>
<div class={tile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="five"
label="Slide 5"
>
<div class={tile(4)}>5</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>Indicators + controls</h3>
<p>Combine both for full-featured navigation.</p>
<Carousel label="Full navigation" controls indicators>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="one"
label="Slide 1"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="two"
label="Slide 2"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="three"
label="Slide 3"
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="four"
label="Slide 4"
>
<div class={tile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="five"
label="Slide 5"
>
<div class={tile(4)}>5</div>
</CarouselItem>
</Carousel>
</div>
</div>Pass a number of milliseconds to autoplay to auto-advance slides. Autoplay pauses on hover, focus-within, tab-hidden, and prefers-reduced-motion: reduce — respecting the platform by default.
loopAutoplay advances every 3000 ms. It pauses on hover, focus-within, tab-hidden, and when the user has prefers-reduced-motion: reduce — respecting the platform by default.
---
// GENERATED from packages/demos/carousel/autoplay.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "200px",
borderRadius: "md",
fontSize: "2xl",
fontWeight: "bold",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>
Autoplay with <code>loop</code>
</h3>
<p>
Autoplay advances every <code>3000</code> ms. It pauses on hover,
focus-within, tab-hidden, and when the user has{" "}
<code>prefers-reduced-motion: reduce</code> — respecting the platform by
default.
</p>
<Carousel
label="Autoplay carousel"
controls
indicators
loop
autoplay={3000}
>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="one"
label="Slide 1"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="two"
label="Slide 2"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="three"
label="Slide 3"
>
<div class={tile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="four"
label="Slide 4"
>
<div class={tile(3)}>4</div>
</CarouselItem>
</Carousel>
</div>
</div>align controls where each slide snaps within the viewport: "start" (default), "center" for the classic “peek” pattern, or "end" for trailing-edge alignment.
align="start" (default)Snap anchor at the leading edge.
align="center"Each slide snaps to the center of the viewport — the common product-carousel "peek" pattern.
align="end"Snap anchor at the trailing edge.
Snap alignment works identically along the y-axis. Scroll each to see where the slides anchor.
---
// GENERATED from packages/demos/carousel/align.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack, flex } from "@pindoba/styled-system/patterns";
const flexCell = css({ flex: "1 1 0", minWidth: "140px" });
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "180px",
borderRadius: "md",
fontSize: "2xl",
fontWeight: "bold",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
const wide = css({ width: "440px" });
const wideTile = (i: number) => `${tile(i)} ${wide}`;
const vBase = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "180px",
width: "100%",
borderRadius: "md",
fontSize: "xl",
fontWeight: "bold",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const vTile = (i: number) =>
`${vBase} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>
<code>align="start"</code> (default)
</h3>
<p>Snap anchor at the leading edge.</p>
<Carousel label="Align start" slidesPerView="auto" align="start" controls>
<CarouselItem
size="md"
value="s1"
label="1"
slidesPerView="auto"
align="start"
>
<div class={wideTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
value="s2"
label="2"
slidesPerView="auto"
align="start"
>
<div class={wideTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
value="s3"
label="3"
slidesPerView="auto"
align="start"
>
<div class={wideTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
value="s4"
label="4"
slidesPerView="auto"
align="start"
>
<div class={wideTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
value="s5"
label="5"
slidesPerView="auto"
align="start"
>
<div class={wideTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
value="s6"
label="6"
slidesPerView="auto"
align="start"
>
<div class={wideTile(0)}>6</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>
<code>align="center"</code>
</h3>
<p>
Each slide snaps to the center of the viewport — the common
product-carousel "peek" pattern.
</p>
<Carousel label="Align center" slidesPerView="auto" align="center" controls>
<CarouselItem
size="md"
value="s1"
label="1"
slidesPerView="auto"
align="center"
>
<div class={wideTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
value="s2"
label="2"
slidesPerView="auto"
align="center"
>
<div class={wideTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
value="s3"
label="3"
slidesPerView="auto"
align="center"
>
<div class={wideTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
value="s4"
label="4"
slidesPerView="auto"
align="center"
>
<div class={wideTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
value="s5"
label="5"
slidesPerView="auto"
align="center"
>
<div class={wideTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
value="s6"
label="6"
slidesPerView="auto"
align="center"
>
<div class={wideTile(0)}>6</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>
<code>align="end"</code>
</h3>
<p>Snap anchor at the trailing edge.</p>
<Carousel label="Align end" slidesPerView="auto" align="end" controls>
<CarouselItem
size="md"
value="s1"
label="1"
slidesPerView="auto"
align="end"
>
<div class={wideTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
value="s2"
label="2"
slidesPerView="auto"
align="end"
>
<div class={wideTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
value="s3"
label="3"
slidesPerView="auto"
align="end"
>
<div class={wideTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
value="s4"
label="4"
slidesPerView="auto"
align="end"
>
<div class={wideTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
value="s5"
label="5"
slidesPerView="auto"
align="end"
>
<div class={wideTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
value="s6"
label="6"
slidesPerView="auto"
align="end"
>
<div class={wideTile(0)}>6</div>
</CarouselItem>
</Carousel>
</div>
<div>
<h3>Vertical orientation</h3>
<p>
Snap alignment works identically along the y-axis. Scroll each to see
where the slides anchor.
</p>
<div class={flex({ gap: "md", wrap: "wrap" })}>
<div class={flexCell}>
<Carousel
label="Align start vertical"
orientation="vertical"
slidesPerView="auto"
align="start"
controls
>
<CarouselItem
size="md"
value="s1"
label="1"
orientation="vertical"
slidesPerView="auto"
align="start"
>
<div class={vTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
value="s2"
label="2"
orientation="vertical"
slidesPerView="auto"
align="start"
>
<div class={vTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
value="s3"
label="3"
orientation="vertical"
slidesPerView="auto"
align="start"
>
<div class={vTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
value="s4"
label="4"
orientation="vertical"
slidesPerView="auto"
align="start"
>
<div class={vTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
value="s5"
label="5"
orientation="vertical"
slidesPerView="auto"
align="start"
>
<div class={vTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
value="s6"
label="6"
orientation="vertical"
slidesPerView="auto"
align="start"
>
<div class={vTile(0)}>6</div>
</CarouselItem>
</Carousel>
</div>
<div class={flexCell}>
<Carousel
label="Align center vertical"
orientation="vertical"
slidesPerView="auto"
align="center"
controls
>
<CarouselItem
size="md"
value="s1"
label="1"
orientation="vertical"
slidesPerView="auto"
align="center"
>
<div class={vTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
value="s2"
label="2"
orientation="vertical"
slidesPerView="auto"
align="center"
>
<div class={vTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
value="s3"
label="3"
orientation="vertical"
slidesPerView="auto"
align="center"
>
<div class={vTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
value="s4"
label="4"
orientation="vertical"
slidesPerView="auto"
align="center"
>
<div class={vTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
value="s5"
label="5"
orientation="vertical"
slidesPerView="auto"
align="center"
>
<div class={vTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
value="s6"
label="6"
orientation="vertical"
slidesPerView="auto"
align="center"
>
<div class={vTile(0)}>6</div>
</CarouselItem>
</Carousel>
</div>
<div class={flexCell}>
<Carousel
label="Align end vertical"
orientation="vertical"
slidesPerView="auto"
align="end"
controls
>
<CarouselItem
size="md"
value="s1"
label="1"
orientation="vertical"
slidesPerView="auto"
align="end"
>
<div class={vTile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
value="s2"
label="2"
orientation="vertical"
slidesPerView="auto"
align="end"
>
<div class={vTile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
value="s3"
label="3"
orientation="vertical"
slidesPerView="auto"
align="end"
>
<div class={vTile(2)}>3</div>
</CarouselItem>
<CarouselItem
size="md"
value="s4"
label="4"
orientation="vertical"
slidesPerView="auto"
align="end"
>
<div class={vTile(3)}>4</div>
</CarouselItem>
<CarouselItem
size="md"
value="s5"
label="5"
orientation="vertical"
slidesPerView="auto"
align="end"
>
<div class={vTile(4)}>5</div>
</CarouselItem>
<CarouselItem
size="md"
value="s6"
label="6"
orientation="vertical"
slidesPerView="auto"
align="end"
>
<div class={vTile(0)}>6</div>
</CarouselItem>
</Carousel>
</div>
</div>
</div>
</div>Two APIs are supported: a data-driven items array with named slots/snippets keyed by value, and direct <CarouselItem> child composition for when each slide needs bespoke markup. When both are provided, items wins.
Map an array of items to one CarouselItem each, keyed by the item's value. Best for data that comes from a backend, a CMS, or a static list.
Drop CarouselItem children directly into the carousel for maximum flexibility — ideal when each slide has bespoke markup or different structure.
---
// GENERATED from packages/demos/carousel/composition.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "160px",
borderRadius: "md",
fontWeight: "bold",
fontSize: "xl",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const items = [
{ value: "one", label: "Slide 1" },
{ value: "two", label: "Slide 2" },
{ value: "three", label: "Slide 3" },
];
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Items array (data-driven)</h3>
<p>
Map an array of items to one <code>CarouselItem</code> each, keyed by the
item's <code>value</code>. Best for data that comes from a backend, a CMS,
or a static list.
</p>
<Carousel label="Items API" controls indicators>
{items.map((item, i) => (
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value={item.value}
label={item.label}
>
<div class={tile(i)}>{i + 1}</div>
</CarouselItem>
))}
</Carousel>
</div>
<div>
<h3>Slot composition</h3>
<p>
Drop <code>CarouselItem</code> children directly into the carousel for
maximum flexibility — ideal when each slide has bespoke markup or
different structure.
</p>
<Carousel label="Slot API" controls>
<CarouselItem size="md" slidesPerView={1} align="start" value="a">
<div class={tile(0)}>First</div>
</CarouselItem>
<CarouselItem size="md" slidesPerView={1} align="start" value="b">
<div class={tile(1)}>Second</div>
</CarouselItem>
<CarouselItem size="md" slidesPerView={1} align="start" value="c">
<div class={tile(2)}>Third</div>
</CarouselItem>
</Carousel>
</div>
</div>The carousel root is a Panel root. It defaults to a transparent, unpadded, square-cornered layout wrapper — a plain <Carousel> paints nothing — but every Panel surface prop is available: background, emphasis, tone, padding, radius (plus the per-side variants), border, shadow, translucent and as.
Pass background + padding + radius="inner" and the carousel becomes a recessed tray whose corner is concentric with the Card (or Dialog, or any Panel) around it — inner resolves as the parent’s radius minus its padding.
The carousel root is a Panel. It paints nothing by default, but pass background, padding and radius="inner" and it becomes a recessed tray whose corner is concentric with the Card around it.
---
// GENERATED from packages/demos/carousel/inner-radius.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import Card from "@pindoba/astro-card";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const slide = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "160px",
width: "100%",
borderRadius: "md",
fontSize: "3xl",
fontWeight: "bold",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
---
<div class={stack({ gap: "md", direction: "column", width: "full" })}>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
The carousel root is a Panel. It paints nothing by default, but pass{" "}
<code>background</code>, <code>padding</code> and{" "}
<code>radius="inner"</code> and it becomes a recessed tray whose corner is
concentric with the Card around it.
</p>
<Card size="md" radius="2xl" border="default">
<Carousel
label="Concentric carousel"
controls
indicators
background="surface.ground"
padding="sm"
radius="inner"
>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="one"
label="Slide 1"
>
<div class={`${slide} ${css({ colorPalette: "primary" })}`}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="two"
label="Slide 2"
>
<div class={`${slide} ${css({ colorPalette: "success" })}`}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="three"
label="Slide 3"
>
<div class={`${slide} ${css({ colorPalette: "warning" })}`}>3</div>
</CarouselItem>
</Carousel>
</Card>
</div>The carousel is card-optional: by default it renders bare — the overlay
layout above, prev/next floating over the slides and the dots below — and its
DOM is exactly what it always was. Pass a card object to frame it in its own
Card and move the chrome into the
card:
trailing you give the card;role="tablist") become the footer’s leading content, after
any footer.leading;content.padding
defaults to "none"); an inset contentBackground works too. The live
region stays in the content.The carousel has no title of its own — give the card a heading (and
subheading, eyebrow, leading…). A string heading also names the carousel
region when you don’t pass label. In card mode the Card paints the surface:
the carousel’s own Panel props (background, border, radius, padding,
shadow, as) only style the bare root, and passThrough.card reaches any
Card slot. The card root carries data-card-kind="carousel" and the carousel’s
engine hooks, so keyboard, scroll-snap, autoplay and the Astro boot behave the
same.
---
// GENERATED from packages/demos/carousel/card.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const items = [
{ value: "coldStarts", label: "Faster cold starts" },
{ value: "purge", label: "Instant cache purge" },
{ value: "audit", label: "Audit log export" },
{ value: "darkMode", label: "Console dark mode" },
];
const updates = [
["Faster cold starts", "Functions now warm in under four seconds."],
["Instant cache purge", "Purge by tag across every edge region at once."],
["Audit log export", "Stream workspace events to your own bucket."],
["Console dark mode", "Follows the system theme by default."],
];
const short = [
["Cold starts", "Under four seconds."],
["Cache purge", "By tag, every region."],
["Audit export", "To your own bucket."],
["Dark console", "Follows the system."],
];
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "neutral" }),
];
const slide = (i: number) =>
`${paletteClasses[i % paletteClasses.length]} ${css({
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
gap: "4xs",
minHeight: "180px",
padding: "md",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
})}`;
const tile = (i: number) =>
`${paletteClasses[i % paletteClasses.length]} ${css({
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
gap: "4xs",
minHeight: "140px",
padding: "sm",
borderRadius: "md",
background: "colorPalette.surface.hill",
color: "colorPalette.text",
})}`;
const title = css({ fontWeight: "bold", fontSize: "lg" });
const note = css({ fontSize: "sm" });
---
<div
class={stack({
gap: "xl",
direction: "column",
width: "full",
maxWidth: "36rem",
})}
>
<Carousel
controls
indicators
card={{
heading: "Shipped this week",
subheading: "Four platform updates",
border: "muted",
}}
>
{items.map((item, i) => (
<CarouselItem value={item.value} label={item.label}>
<div class={slide(i)}>
<span class={title}>{updates[i]?.[0]}</span>
<span class={note}>{updates[i]?.[1]}</span>
</div>
</CarouselItem>
))}
</Carousel>
<Carousel
controls
indicators
slidesPerView={2}
card={{
heading: "Changelog",
background: "surface.valley",
contentBackground: "surface.peak",
content: { padding: "sm" },
footer: { trailing: "Updated daily" },
}}
>
{items.map((item, i) => (
<CarouselItem value={item.value} label={item.label} slidesPerView={2}>
<div class={tile(i)}>
<span class={title}>{short[i]?.[0]}</span>
<span class={note}>{short[i]?.[1]}</span>
</div>
</CarouselItem>
))}
</Carousel>
</div><Carousel {items} controls indicators card={{ heading: "Shipped this week" }}>
…
</Carousel>
card={false} is the same as leaving it unset. In Astro, rich header/footer
content goes through the forwarded card-* slots (card-trailing renders
before the prev/next buttons, card-footer-leading before the dots).
The passThrough prop targets any slot (root, viewport, track, item, controls, prevButton, nextButton, indicators, indicator, liveRegion, and card for the Card in card mode). Each slot accepts a style (Panda SystemStyleObject) and props (HTML attributes).
Use passThrough to target any slot. style accepts a Panda SystemStyleObject; props forwards arbitrary HTML attributes — useful for overriding aria-label, id, or data attributes.
---
// GENERATED from packages/demos/carousel/custom.demo.tsx — edit the source, not this file.
import Carousel from "@pindoba/astro-carousel";
import { CarouselItem } from "@pindoba/astro-carousel";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const paletteClasses = [
css({ colorPalette: "primary" }),
css({ colorPalette: "success" }),
css({ colorPalette: "warning" }),
css({ colorPalette: "danger" }),
css({ colorPalette: "neutral" }),
];
const base = css({
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "180px",
borderRadius: "md",
fontSize: "2xl",
fontWeight: "bold",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
});
const tile = (i: number) =>
`${base} ${paletteClasses[i % paletteClasses.length]}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Style + attribute overrides</h3>
<p>
Use <code>passThrough</code> to target any slot. <code>style</code>{" "}
accepts a Panda <code>SystemStyleObject</code>; <code>props</code>{" "}
forwards arbitrary HTML attributes — useful for overriding{" "}
<code>aria-label</code>, <code>id</code>, or data attributes.
</p>
<Carousel
label="Custom-styled carousel"
controls
indicators
passThrough={{
root: {
style: { maxWidth: "640px", mx: "auto" },
props: { "data-testid": "showcase-carousel" },
},
viewport: {
style: {
borderWidth: "1px",
borderColor: "border.default",
borderRadius: "lg",
},
},
indicator: {
style: { width: "12px", height: "12px" },
},
}}
>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="one"
label="Slide 1"
>
<div class={tile(0)}>1</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="two"
label="Slide 2"
>
<div class={tile(1)}>2</div>
</CarouselItem>
<CarouselItem
size="md"
slidesPerView={1}
align="start"
value="three"
label="Slide 3"
>
<div class={tile(2)}>3</div>
</CarouselItem>
</Carousel>
</div>
</div>The viewport is a labelled carousel region (role="region", aria-roledescription="carousel", tabindex="0") — pass a label so screen-reader users know what the carousel contains. Arrow keys move between slides (ArrowLeft/ArrowRight when horizontal, ArrowUp/ArrowDown when vertical), and Home/End jump to the first/last slide.
A visually-hidden polite live region announces the active slide (“Slide 2 of 6”, or the slide’s label) as it changes — never on initial load.
Set inertOffscreenSlides to mark slides scrolled out of view as aria-hidden + inert, so assistive tech and the tab order skip them. It’s off by default because inert changes focus reachability — opt in when each slide contains focusable content.
RTL is supported out of the box: render the carousel inside a dir="rtl" context and the scroll math, prev/next direction, edge state, and chevron icons all mirror automatically (the engine reads the computed direction — no prop needed).
For a vertical carousel, the viewport height defaults to 420px. Override it with the --_carousel-viewport-h CSS custom property (e.g. passThrough={{ viewport: { style: { "--_carousel-viewport-h": "320px" } } }}).