Skip to content

Carousel

.bp-carousel uses scroll-snap-type: x mandatory for native scroll snapping. In supporting browsers (Chrome 148+), ::scroll-marker renders dot indicators and ::scroll-button renders prev/next arrows — all in pure CSS with no JavaScript. The manual .bp-carousel__dots and .bp-carousel__controls elements act as fallback for older browsers.

Full-width slides

<style>
.demo-carousel--feature { --carousel-slide-padding: var(--bp-space-8); --carousel-slide-min-height: 10rem; }
.demo-carousel__slide--brand { --carousel-slide-bg: var(--bp-color-brand-subtle); color: var(--bp-color-brand); }
.demo-carousel__slide--subtle { --carousel-slide-bg: var(--bp-color-bg-subtle); }
.demo-carousel__inner {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-weight: 600;
}
</style>
<div class="bp-carousel demo-carousel--feature">
<div class="bp-carousel__track" role="region" aria-label="Feature highlights" aria-live="polite" aria-atomic="false">
<div class="bp-carousel__slide demo-carousel__slide--brand" tabindex="0">
<div class="demo-carousel__inner">Slide 1 — Tokens</div>
</div>
<div class="bp-carousel__slide demo-carousel__slide--subtle" tabindex="0">
<div class="demo-carousel__inner">Slide 2 — Components</div>
</div>
<div class="bp-carousel__slide demo-carousel__slide--subtle" tabindex="0">
<div class="demo-carousel__inner">Slide 3 — Themes</div>
</div>
</div>
<ul class="bp-carousel__dots" role="tablist" aria-label="Slides">
<li role="presentation"><button class="bp-carousel__dot is-active" role="tab" aria-label="Slide 1" aria-selected="true"></button></li>
<li role="presentation"><button class="bp-carousel__dot" role="tab" aria-label="Slide 2" aria-selected="false" tabindex="-1"></button></li>
<li role="presentation"><button class="bp-carousel__dot" role="tab" aria-label="Slide 3" aria-selected="false" tabindex="-1"></button></li>
</ul>
</div>
VariableDefaultDescription
--carousel-gapvar(--bp-space-4)Gap between slides
--carousel-slide-min100%Minimum slide width
--carousel-radiusvar(--bp-radius-lg)Slide border radius
--carousel-slide-bgtransparentSlide background — set on carousel or per-slide
--carousel-slide-padding0pxSlide inner padding
--carousel-slide-min-heightautoSlide minimum height
ClassDescription
bp-carousel--multiPartial next slide visible as scroll affordance
bp-carousel--peekResponsive peek — 1.2 slides at small, 1.8 at medium, 2.2 at wide containers

Multi-slide (peek)

<style>
.demo-carousel--multi {
--carousel-slide-min: 70%;
--carousel-slide-padding: var(--bp-space-6);
--carousel-slide-min-height: 8rem;
--carousel-slide-bg: var(--bp-color-bg-subtle);
}
.demo-carousel__slide--brand { --carousel-slide-bg: var(--bp-color-brand-subtle); color: var(--bp-color-brand); }
.demo-carousel__inner {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-weight: 600;
}
</style>
<div class="bp-carousel bp-carousel--multi demo-carousel--multi">
<div class="bp-carousel__track" role="region" aria-label="Team members">
<div class="bp-carousel__slide demo-carousel__slide--brand" tabindex="0">
<div class="demo-carousel__inner">Card A</div>
</div>
<div class="bp-carousel__slide" tabindex="0"><div class="demo-carousel__inner">Card B</div></div>
<div class="bp-carousel__slide" tabindex="0"><div class="demo-carousel__inner">Card C</div></div>
</div>
</div>

bp-carousel--peek automatically adjusts the visible slide count based on the carousel’s container width — no JavaScript, no media queries.

Responsive peek (resize the preview)

<style>
.demo-carousel--peek {
--carousel-slide-min-height: 10rem;
--carousel-slide-bg: var(--bp-color-bg-subtle);
--carousel-slide-padding: var(--bp-space-6);
}
.demo-carousel__slide--brand { --carousel-slide-bg: var(--bp-color-brand-subtle); color: var(--bp-color-brand); }
.demo-carousel__inner {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-weight: 600;
}
</style>
<div class="bp-carousel bp-carousel--peek demo-carousel--peek">
<div class="bp-carousel__track" role="region" aria-label="Features">
<div class="bp-carousel__slide demo-carousel__slide--brand" tabindex="0">
<div class="demo-carousel__inner">Tokens</div>
</div>
<div class="bp-carousel__slide" tabindex="0">
<div class="demo-carousel__inner">Components</div>
</div>
<div class="bp-carousel__slide" tabindex="0">
<div class="demo-carousel__inner">Theming</div>
</div>
<div class="bp-carousel__slide" tabindex="0">
<div class="demo-carousel__inner">Grid</div>
</div>
</div>
</div>

.bp-carousel__slide is just a snap container — drop any component inside. Here using .bp-card with no extra styles needed on the slide itself.

Card slides

<style>
.demo-carousel--cards { --carousel-slide-min-height: 16rem; }
.demo-carousel--cards .bp-card { height: 100%; }
</style>
<div class="bp-carousel demo-carousel--cards">
<div class="bp-carousel__track" role="region" aria-label="Team members">
<div class="bp-carousel__slide" tabindex="0" role="group" aria-label="Slide 1 of 3">
<div class="bp-card">
<div class="bp-card__image">
<img src="https://placehold.co/600x200/dbeafe/2563eb?text=Design" alt="" role="presentation" />
</div>
<div class="bp-card__header">
<h3 class="bp-card__title">Design Tokens</h3>
</div>
<div class="bp-card__body">
Semantic color, spacing, and typography tokens that adapt to light and dark mode.
</div>
<div class="bp-card__footer">
<a href="/tokens/tokens/" class="bp-btn bp-btn--sm">View tokens</a>
</div>
</div>
</div>
<div class="bp-carousel__slide" tabindex="0" role="group" aria-label="Slide 2 of 3">
<div class="bp-card">
<div class="bp-card__image">
<img src="https://placehold.co/600x200/dcfce7/16a34a?text=Components" alt="" role="presentation" />
</div>
<div class="bp-card__header">
<h3 class="bp-card__title">CSS Components</h3>
</div>
<div class="bp-card__body">
Zero-JS accordion, carousel, modal, nav and more — built with modern CSS primitives.
</div>
<div class="bp-card__footer">
<a href="/components/button/" class="bp-btn bp-btn--sm">View components</a>
</div>
</div>
</div>
<div class="bp-carousel__slide" tabindex="0" role="group" aria-label="Slide 3 of 3">
<div class="bp-card">
<div class="bp-card__image">
<img src="https://placehold.co/600x200/fef3c7/d97706?text=Themes" alt="" role="presentation" />
</div>
<div class="bp-card__header">
<h3 class="bp-card__title">Theming</h3>
</div>
<div class="bp-card__body">
Override any token at any scope. Dark mode, brand colors, and custom themes with pure CSS.
</div>
<div class="bp-card__footer">
<a href="/tokens/tokens/" class="bp-btn bp-btn--sm bp-btn--ghost">Learn more</a>
</div>
</div>
</div>
</div>
</div>
No axe violations tested 2026-05-11
  • Track uses role="region" + aria-label for screen reader context.
  • Slides should have tabindex="0" to be keyboard-reachable.
  • Dot buttons are decorative — aria-hidden="true" on the list is acceptable; or add aria-label per dot.
  • Arrow-key scrolling works natively once the track is focused.
APIAvailabilityUsed forWithout itPolyfill
scroll-snap-type Widely available Baseline 2020 Native slide snapping without JSFree-scrolling track, no snappingNone needed
IntersectionObserver Widely available Baseline 2020 Syncing dot .is-active state with visible slideDots don’t update on scrollpolyfill
::scroll-marker Newly available Baseline 2025 CSS-native dot indicators — auto-sync with scroll, no JSManual .bp-carousel__dots with JS .is-active toggleNone needed
::scroll-button Newly available Baseline 2025 CSS-native prev/next arrowsManual .bp-carousel__controls buttonsNone needed
Container Queries Widely available Baseline 2023 Multi-slide peek layout adjustmentsStatic slide widthsNone needed
  • --_gap, --_slide-min, --_radius — resolved on .bp-carousel, do not set directly.
  • --_bg, --_padding, --_height — resolved per-slide on .bp-carousel__slide, do not set directly.