Carousel
Carousel pages through a row of slides using real browser scrolling, so a mouse wheel, trackpad, touch drag, or the keyboard all move it the same way native scrolling would. It layers its own navigation buttons, pagination dots, autoplay, and edge fades on top, and works with any slide content you give it.
Dependencies
A plain eun-card.
Any element works.
Swipe, drag, or use the arrows.
Several cards, paged a few at a time.
When to use
Use a carousel for a row of slides too numerous, or too visually prominent, to show all at once, such as product cards, testimonials, a media gallery, or a hero banner rotation, where someone should be able to page through them with a click, a swipe, or a keyboard. Its slides can be any card variant, an image card, a sliding card, or a fully custom element. See the Slide content section below. If the slides are only ever photos, needing thumbnails, a magnifier, or a lightbox, or if the list should keep growing indefinitely instead of staying a fixed, paged set, see Alternatives below for a better fit.
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/carousel.js";
import "@eunomia/elements/icon.js";
<eun-carousel full-slides edge-fade show-arrows show-dots>
<div>Slide 1</div>
<div>Slide 2</div>
<div>Slide 3</div>
</eun-carousel>
Importing the file registers <eun-carousel> as a custom element.
eun-icon is used internally for the prev/next chevrons, so import it
alongside, unless you hide those buttons (see the dependency badge
above).
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/carousel.js";
import "@eunomia/elements/icon.js";
</script>
<eun-carousel full-slides edge-fade show-arrows show-dots>
<div>Slide 1</div>
<div>Slide 2</div>
</eun-carousel>
npm install @eunomia/elements
import "@eunomia/elements/carousel.js";
import "@eunomia/elements/icon.js";
function Highlights() {
return (
<eun-carousel full-slides edge-fade show-arrows show-dots>
<div>Slide 1</div>
<div>Slide 2</div>
</eun-carousel>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/carousel.js";
import "@eunomia/elements/icon.js";
export function Highlights() {
return (
<eun-carousel full-slides edge-fade show-arrows show-dots>
<div>Slide 1</div>
<div>Slide 2</div>
</eun-carousel>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/carousel.js";
import "@eunomia/elements/icon.js";
</script>
<template>
<eun-carousel full-slides edge-fade show-arrows show-dots>
<div>Slide 1</div>
<div>Slide 2</div>
</eun-carousel>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/carousel.js";
import "@eunomia/elements/icon.js";
@Component({
selector: "app-highlights",
template: `
<eun-carousel full-slides edge-fade show-arrows show-dots>
<div>Slide 1</div>
<div>Slide 2</div>
</eun-carousel>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class HighlightsComponent {}
Alternatives
| You want… | Prefers |
|---|---|
| A long, uniform list that only ever grows downward (infinite scroll) | eun-infinite-list |
| A photo set with thumbnails, a magnifier, and a lightbox | eun-image-carousel |
Guidance
- Give every carousel on a page a distinct
aria-label("Featured products") - Pair
autoplaywithloop, so it doesn't stall once it reaches the last slide - Keep slide count reasonable, since dozens of slides make the dots row impractical, and prefer paging (
scroll-step) instead - Let each slide's own content (a card's
href, a button, ...) stay independently interactive and focusable
- Using a carousel for a single hero image with no other slides. It's just an image at that point
- Cramming unrelated content into one carousel to save space. Split into sections instead
- Relying on
autoplayto convey time-sensitive information nobody can pause in time to read - Nesting a carousel's slide inside another carousel's own track
Live testing
Properties
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| responsive-breakpoint | number | 480 | Below this host width in pixels, an inline layout renders its buttons in the header row instead of overlaid on the slides, the same as the header layout would. Zero disables the automatic switch |
| nav-placement | EunomiaCarouselNavPlacement | 'end' | Where the previous and next buttons sit within the header row. Has no effect while they're overlaid on the viewport instead |
| aria-label | string | — | The accessible name for the carousel region. Recommended whenever a page has more than one carousel, or its purpose isn't already clear from a preceding heading. Defaults to the localized string for the current locale (English: "Carousel") |
| previous-label | string | Previous slide | The accessible label of the previous control. Defaults to the localized string for the current locale (English: "Previous slide") |
| next-label | string | Next slide | The accessible label of the next control. Defaults to the localized string for the current locale (English: "Next slide") |
| layout | EunomiaCarouselLayout | 'inline' | Where the previous and next buttons sit: overlaid on the slides and centered over them, or in a header row above the track, aligned next to the title slot |
| visible-slides | number | 1 | How many slides are shown at once |
| full-slides | boolean | false | Whether the visible slides fill the viewport exactly, or leave part of the next one peeking at the edge |
| scroll-step | number | 1 | How many slides a previous, next, or dot click advances by, independently of visibleSlides |
| center-snap | boolean | false | Free continuous scrolling that snaps to whichever slide ends up nearest the center, instead of discrete paging |
| edge-fade | boolean | false | Fades the viewport's edges to hint there's more to scroll, hidden automatically on whichever side is already at its bound |
| loop | boolean | false | Wraps the previous, next, and autoplay controls around at the bounds instead of stopping there, and keeps the previous and next buttons enabled at the bounds instead of disabling them |
| autoplay | boolean | false | Automatically advances by scrollStep on a set interval. Pauses on hover or focus, but renders no pause or play control of its own, so pair this with a visible control of your own calling the public pause and resume methods, since a moving carousel nobody can stop fails accessibility guidelines |
| autoplay-interval | number | 4000 | Milliseconds between each autoplay advance |
| show-arrows | boolean | false | Shows the previous and next buttons |
| show-dots | boolean | false | Shows the pagination dots |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| resolvedLocale | — | This instance's own `locale` when set, the shared default locale otherwise. Used for `Intl` formatting, so dates and numbers follow the same locale as dictionary. | |
| dictionary | — | This instance's locale-resolved string dictionary — see the class doc for the resolution order. |
Slots
| Name | Description |
|---|---|
| (default) | The carousel's slides, any element, one per slide |
| title | Optional content, typically a heading, shown at the top left above the carousel. Rendered the same way regardless of layout, so it works whether the previous and next buttons sit above alongside it, or stay overlaid on the slides |
Events
| Name | Type | Description |
|---|---|---|
| eunslidechange | SlideChangeEvent | Fired once the active slide settles on a new index |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --carousel-visible-slides | Sets how many slides are shown at once, overriding visibleSlides. Meant to be changed from your own media or container queries, so the slide count follows the available space without any script |
| --carousel-scroll-step | Sets how many slides a previous, next, or dot click advances by, overriding scrollStep. Read again whenever the carousel resizes |
| --carousel-responsive-breakpoint | Sets the width, in pixels, below which an inline layout moves its buttons into the header row, overriding responsiveBreakpoint. Zero disables the automatic switch. Read again whenever the carousel resizes |
| --carousel-gap | Sets the gap between slides |
| --carousel-track-padding | Sets the vertical padding on the track, giving slides' own effects that bleed outside their box, such as a hover shadow, room to render before it gets clipped |
| --carousel-track-padding-top | Sets the top padding on the track, falling back to --carousel-track-padding |
| --carousel-track-padding-bottom | Sets the bottom padding on the track, falling back to --carousel-track-padding |
| --carousel-track-padding-inline | Lets the track bleed past the carousel on both sides by this length, while slides stay aligned on its edges. The next slide and the hover shadows of the edge slides show past the container |
| --carousel-slide-width | Sets a fixed width for every slide, overriding visibleSlides, fullSlides and --carousel-min-slide-width |
| --carousel-min-slide-width | Sets the minimum slide width. On a narrow viewport, this wins over visibleSlides so slides never get uncomfortably small |
| --carousel-peek-size | Sets how much of the next slide peeks at the edge, when fullSlides is off |
| --carousel-fade-display | Set to none to hide the edge fades through pure CSS, regardless of edgeFade, for instance on a narrow screen where a single slide fills the whole width |
| --carousel-fade-width | Sets the width of the edge fades |
| --carousel-fade-color | Sets the color of the edge fades, which fade to transparent |
| --carousel-active-scale | Sets the scale applied to the active slide |
| --carousel-header-margin | Sets the space between the header row and the track |
| --carousel-header-gap | Sets the gap between the title slot and the header navigation buttons |
| --carousel-header-align | Sets the cross-axis alignment (align-items) between the title slot and the header navigation buttons. Defaults to baseline, which keeps the nav buttons close to the track below regardless of how tall a multi-line title ends up |
| --carousel-nav-display | Set to none to hide the previous and next buttons through pure CSS, regardless of showArrows |
| --carousel-nav-size | Sets the size of the previous and next buttons |
| --carousel-nav-icon-size | Sets the icon size of the previous and next buttons |
| --carousel-nav-color | Sets the icon color of the previous and next buttons |
| --carousel-nav-background | Sets the background color of the previous and next buttons |
| --carousel-nav-border-color | Sets the border color of the previous and next buttons |
| --carousel-nav-border-width | Sets the border width of the previous and next buttons |
| --carousel-nav-hover-background | Sets the background color of the previous and next buttons on hover |
| --carousel-nav-hover-color | Sets the icon color of the previous and next buttons on hover |
| --carousel-nav-hover-border-color | Sets the border color of the previous and next buttons on hover |
| --carousel-nav-shadow | Sets the shadow of the previous and next buttons in the inline layout, when enabled. Unset for the header layout, and always unset while a button is disabled |
| --carousel-nav-disabled-background | Sets the background and border color of a disabled previous or next button |
| --carousel-nav-disabled-color | Sets the icon color of a disabled previous or next button |
| --carousel-dots-display | Set to none to hide the pagination dots through pure CSS, regardless of showDots |
| --carousel-dots-margin | Sets the margin between the pagination dots and the slides above them |
| --carousel-dot-gap | Sets the gap between pagination dots |
| --carousel-dot-size | Sets the size of the pagination dots |
| --carousel-dot-border-radius | Sets the corner radius of the pagination dots |
| --carousel-dot-color | Sets the color of the pagination dots |
| --carousel-dot-active-color | Sets the color of the active pagination dot |
| --carousel-focus-outline-color | Sets the keyboard focus outline color of the track, the previous and next controls and the dots |
Layout
Visible slides and scroll step
visibleSlides controls how many slides are shown at once.
scrollStep controls how many a prev/next click or a dot jump advances
by. The two are independent.
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
visible-slides="3"
scroll-step="1"
>
...
</eun-carousel>
Responsive slide count
--carousel-min-slide-width (default 220px) sets a floor under how
narrow a slide is allowed to get. On a wide viewport, visibleSlides
wins as usual. On a narrow one, dividing the track's width by
visibleSlides would otherwise produce slides smaller than this
minimum. When that happens, the minimum wins instead, and the "extra"
requested slides simply become scrollable off-screen rather than being
squeezed. This is pure CSS (max()), no JS/ResizeObserver involved,
so it reacts to any resize for free.
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
visible-slides="4"
style="--carousel-min-slide-width: 220px;"
>
...
</eun-carousel>
Capped at 340px above to reproduce a narrow viewport regardless of your actual window size. Resize your browser on a real page and the same thing happens automatically, no separate "mobile" prop to set.
Container queries
Slide count, scroll step, the responsive breakpoint and the edge fades can all be driven from CSS custom properties, so the carousel follows the space it's given through your own media or container queries, without any script. Each variable wins over its matching property when set, and falls back to that property otherwise.
| Variable | Overrides |
|---|---|
--carousel-visible-slides |
visibleSlides |
--carousel-scroll-step |
scrollStep |
--carousel-responsive-breakpoint |
responsiveBreakpoint |
--carousel-fade-display |
edgeFade (none hides the fades) |
<div class="products">
<eun-carousel full-slides edge-fade show-arrows show-dots visible-slides="4">
...
</eun-carousel>
</div>
.products {
container-type: inline-size;
}
@container (width < 1024px) {
eun-carousel {
--carousel-visible-slides: 2;
--carousel-scroll-step: 2;
}
}
@container (width < 600px) {
eun-carousel {
--carousel-visible-slides: 1;
--carousel-scroll-step: 1;
--carousel-min-slide-width: 0px;
/* one slide fills the whole width, the fades would only cover it */
--carousel-fade-display: none;
}
}
Set these variables on eun-carousel itself or on one of its ancestors.
Slide sizing and the fades react instantly since they're pure CSS. The
scroll step and the breakpoint are read by the component on every
resize of the carousel, which is exactly when a container or media
query flips them.
Peeking slides
By default, the visible slides leave --carousel-peek-size of the next
slide peeking at the edge, hinting there's more to scroll. Set
full-slides for the visible slides to fill the viewport exactly.
<eun-carousel edge-fade show-arrows show-dots visible-slides="2">
...
</eun-carousel>
Track spacing
The track keeps --carousel-track-padding (--eun-space-l by default) above
and below the slides, so a hover shadow or a lifted card isn't clipped.
--carousel-track-padding-top and --carousel-track-padding-bottom set each
side on its own, and --carousel-header-margin sets the space between the
header row and the track.
To let the slides run past the container, set
--carousel-track-padding-inline. The track then bleeds out by that length on
both sides while the slides stay aligned on the container's edges, so the next
slide shows past the edge and the edge cards keep their full shadow. Leave
enough room around the carousel for the bleed, or the page scrolls
horizontally.
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
visible-slides="3"
style="
--carousel-track-padding-inline: 32px;
--carousel-header-margin: 0px;
--carousel-track-padding-top: 8px;
"
>
<h3 slot="title">Bleeding track</h3>
...
</eun-carousel>
--carousel-slide-width gives every slide the same fixed width instead,
whatever visibleSlides and fullSlides say.
Center snap
centerSnap switches from discrete paging to free continuous scrolling
(drag it with your finger or trackpad) that settles on whichever slide
ends up nearest the center. That slide also gets the
eun-carousel-active-slide class, scaled up here via
--carousel-active-scale, but usable for any custom styling. Scaling
the active slide up pushes it past its own unscaled box on every side,
which the track's overflow-y: hidden would otherwise clip flat. Bump
--carousel-track-padding to give it room, the same fix the Sliding
card example below uses for its own bleed.
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
center-snap
style="--carousel-active-scale: 1.06; --carousel-track-padding: 12px;"
>
...
</eun-carousel>
Edge fade
edgeFade fades the viewport's left/right edges to hint there's more
to scroll on that side, as in every demo on this page. Each side fades out automatically once its own scroll bound is
reached (no left fade right at the start, no right fade right at the
end), so it only ever shows where it's actually true. Scroll the demo
below and watch the left fade appear only once you've moved past the
first card.
<eun-carousel full-slides edge-fade show-arrows show-dots> ... </eun-carousel>
Leave it off for no fade at all:
<eun-carousel full-slides show-arrows show-dots> ... </eun-carousel>
Or hide them through CSS with --carousel-fade-display: none, which
wins over edgeFade and fits a media or container query, for instance
on a narrow screen where a single slide fills the whole width and the
fades would only cover its content (see "Container queries" above):
@media (width < 600px) {
eun-carousel {
--carousel-fade-display: none;
}
}
Behavior
Loop
loop wraps previous()/next()/autoplay around at the bounds instead
of stopping there. The prev/next buttons also stay enabled at both
ends instead of disabling themselves.
<eun-carousel full-slides edge-fade show-arrows show-dots loop>
...
</eun-carousel>
Autoplay
autoplay advances automatically every autoplayInterval
milliseconds, and pauses on hover or keyboard focus. It renders no
pause/play control of its own (see "Why no built-in control" in the
Accessibility tab), so pairing it with a visible control of your own,
wired to the public pauseAutoplay()/resumeAutoplay() methods, is
your responsibility, not optional. A moving carousel nobody can stop
is a WCAG 2.2.2 failure. The demo below is exactly that pattern. It
starts paused rather than moving the instant the page loads (press
play to see it run), and its button only ever reacts to an explicit
click. It never calls .focus() on itself, so a running carousel
doesn't keep pulling keyboard focus back onto it on every tick.
<button id="my-toggle" aria-label="Pause automatic slide rotation">
<!-- your own icon -->
</button>
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
id="my-carousel"
autoplay
loop
autoplay-interval="2500"
>
...
</eun-carousel>
<script type="module">
const carousel = document.getElementById("my-carousel");
const toggle = document.getElementById("my-toggle");
let paused = false;
toggle.addEventListener("click", () => {
paused = !paused;
paused ? carousel.pauseAutoplay() : carousel.resumeAutoplay();
toggle.setAttribute(
"aria-label",
paused
? "Resume automatic slide rotation"
: "Pause automatic slide rotation",
);
});
</script>
Pairing autoplay with loop is recommended. Without it, autoplay simply stops advancing once it reaches the last slide, since manual and automatic navigation share the same bounds behavior.
Controls
No controls
Leave out show-arrows and show-dots to rely purely on scrolling: wheel,
trackpad, touch, or keyboard once the track is focused.
<eun-carousel full-slides edge-fade> ... </eun-carousel>
--carousel-nav-display/--carousel-dots-display
(both default flex) hide the buttons/dots via CSS alone
when set to none, e.g. inside a @media
query, to hide arrows on narrow viewports without touching
showArrows/showDots in JS.
External controls
Nothing about navigating eun-carousel requires its own built-in
buttons or dots. previous()/next()/goTo(index) and the
eunslidechange event (see the API tab) are the same public surface
the built-in controls use internally, so a fully custom set of
controls (differently placed, differently styled, or driven by
something other than a button) is exactly as capable. This demo turns
the built-in arrows/dots off and rebuilds both externally from that
public API alone.
<eun-carousel full-slides edge-fade id="my-carousel"> ... </eun-carousel>
<button id="my-prev" aria-label="Previous slide">...</button>
<button id="my-next" aria-label="Next slide">...</button>
<script type="module">
const carousel = document.getElementById("my-carousel");
document
.getElementById("my-prev")
.addEventListener("click", () => carousel.previous());
document
.getElementById("my-next")
.addEventListener("click", () => carousel.next());
carousel.addEventListener("eunslidechange", (event) => {
console.log(`Now on slide ${event.index + 1} of ${event.count}`);
});
</script>
The Autoplay example above is the same idea applied to pauseAutoplay()/resumeAutoplay() instead. Both examples build on the exact same public API, just different pieces of it.
Nav button placement
layout picks where the prev/next buttons sit, independently of
everything else on this page:
inline(the default): overlaid on the slides themselves, vertically centered over them at the left/right edges. Since they sit on top of arbitrary, sometimes busy, image-heavy slide content, they carry a shadow by default (--carousel-nav-shadow) so they always read as floating controls instead of blending into whatever's behind them.header: moved into a header row above the track, aligned to the right.
Responsive button placement
Below responsiveBreakpoint (default 480px, measured on the
carousel itself), an inline layout automatically borrows header's
button placement instead. Overlay buttons need real space on either
side of the slides to read as controls rather than obscure them, which
a narrow/mobile-width carousel doesn't have. layout="header" is
unaffected, since it's already there. Resize your browser window
narrower than 480px (or shrink this one) and watch the demo below
switch on its own, with nothing to configure beyond the default.
Highlights
<eun-carousel full-slides edge-fade show-arrows show-dots> ... </eun-carousel>
<!-- or, with a custom threshold : -->
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
responsive-breakpoint="600"
>
...
</eun-carousel>
<!-- or, to keep inline overlaid at every width : -->
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
responsive-breakpoint="0"
>
...
</eun-carousel>
<!-- or, from CSS (wins over the attribute) : -->
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
style="--carousel-responsive-breakpoint: 600px;"
>
...
</eun-carousel>
Capped at 360px above purely to demonstrate the switch regardless of your actual window size, the same idea as "Responsive slide count" earlier. On a real page at full width, this only kicks in once the carousel itself is actually that narrow.
Placing the buttons within the header row
Once the buttons are in a header row (layout="header", or an inline
layout narrowed below responsiveBreakpoint), navPlacement picks
where within that row they sit: at the trailing edge, opposite title
(end, the default, shown above), at the leading edge, swapping places
with title (start), or centered together with title as one group
(center).
Featured
Centered
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
layout="header"
nav-placement="start"
>
<h3 slot="title">Featured</h3>
<!-- ... -->
</eun-carousel>
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
layout="header"
nav-placement="center"
>
<h3 slot="title">Centered</h3>
<!-- ... -->
</eun-carousel>
Has no effect while the buttons are still overlaid on the viewport (inline layout, above the breakpoint), since there's no header row to place them within yet.
The title slot
Independently of layout, anything slotted into title (typically a
heading) renders at the top-left, in a row above the track. It shows
up the same way whether the nav buttons are overlaid on the slides
(inline) or sharing that same row, to its right (header). The two
examples below use the same title slot with each placement in turn.
Pagination dots (if shown) always stay below the track, regardless of
either choice.
Inline layout
Featured products
<eun-carousel full-slides edge-fade show-arrows show-dots>
<h3 slot="title">Featured products</h3>
<div>Slide 1</div>
<!-- ... -->
</eun-carousel>
Header layout
Featured products
<eun-carousel full-slides edge-fade show-arrows show-dots layout="header">
<h3 slot="title">Featured products</h3>
<div>Slide 1</div>
<!-- ... -->
</eun-carousel>
Slot a real heading element (h2/h3/...) at whatever level fits the surrounding page structure. eun-carousel doesn't enforce one. With no title content and layout="inline", the row above the track takes up no space at all. It only appears once there's something to show there, whether that's a title, the header-layout buttons, or both.
Slide content
eun-carousel is entirely slot-based. It doesn't render or know
anything about slide content itself. Every card in this library works
as-is, and so does a fully custom element.
Plain card
A short summary sent every Monday.
What shipped this sprint.
What's shipping next quarter.
What happened, and what changed.
<eun-carousel full-slides edge-fade show-arrows show-dots visible-slides="2">
<eun-card>
<strong>Weekly digest</strong>
<p>A short summary sent every Monday.</p>
</eun-card>
<!-- ... -->
</eun-carousel>
Item card
<eun-carousel full-slides edge-fade show-arrows show-dots visible-slides="2">
<eun-card>
<eun-icon slot="start" name="bolt"></eun-icon>
<span slot="title">Fast setup</span>
<span slot="description">Drop it in and go.</span>
</eun-card>
<!-- ... -->
</eun-carousel>
Image card
<eun-carousel full-slides edge-fade show-arrows show-dots visible-slides="2">
<eun-image-card>
<img slot="image" src="/photo.jpg" alt="" />
<span slot="title">Studio session</span>
</eun-image-card>
<!-- ... -->
</eun-carousel>
Sliding card
eun-sliding-card paints a permanent bottom fade that bleeds
--sliding-card-fade-height (60px by default) below its own box,
which the carousel's own overflow-y: hidden track would otherwise
clip flat, since the default --carousel-track-padding is shorter.
Bump the padding to match so the fade renders in full and the pagination
dots stay clearly separated below it, not crowded right up against
the cards. visibleSlides (here 2) and a max-width on each card
(the same sizing attributes used throughout this page) keep the
cards from growing as large as their 300px default.
Subscriptions, one-time sales, and refunds.
Organic, referral, and paid acquisition.
Voluntary cancellations vs. failed payments.
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
visible-slides="2"
style="--carousel-track-padding: 64px;"
>
<eun-sliding-card style="max-width: 280px;"> ... </eun-sliding-card>
<!-- ... -->
</eun-carousel>
Hover, focus, or tap a card above to reveal its back layer, right inside the carousel. The dots below stay fully visible throughout. See eun-sliding-card's own page for its full API.
Custom slide
Any element works, with no special markers or base class required.
eun-carousel never inspects what it's given.
<eun-carousel full-slides edge-fade show-arrows show-dots>
<div class="my-own-slide">...</div>
</eun-carousel>
Server-side rendering
Before the carousel upgrades, its slides are plain elements stacked one
under the other, so a server-rendered carousel first fills the page from
top to bottom, then folds into a single row. Link carousel-ssr.css in
the page's <head> to lay the slides out in a row from the first paint:
<link
rel="stylesheet"
href="node_modules/@eunomia/elements/dist/ssr/carousel-ssr.css"
/>
A bundler that resolves package exports can reach it as
@eunomia/elements/carousel-ssr.css.
It sizes the slides from visible-slides (up to 6), --carousel-gap,
--carousel-slide-width and --carousel-min-slide-width, places the
title slot above them, and reserves the rows of the header buttons in
the header layout and of the dots, once there are more slides than fit.
Below 480px, it also reserves the header buttons of an inline layout,
for the default responsive-breakpoint only, and only keeps each row
with show-arrows or show-dots. The buttons and dots
themselves, the edge fades and center-snap appear with the upgrade.
Theming
Every color and spacing value is exposed as a CSS custom property (full
list in the API tab), so the carousel repaints with the active theme
automatically. See
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
style="--carousel-dot-active-color: #b45309; --carousel-nav-border-color: #b45309;"
>
...
</eun-carousel>
The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.
| Variable | Sets |
|---|---|
--carousel-fade-color |
Sets the color of the edge fades, which fade to transparent |
--carousel-nav-color |
Sets the icon color of the previous and next buttons |
--carousel-nav-background |
Sets the background color of the previous and next buttons |
--carousel-nav-border-color |
Sets the border color of the previous and next buttons |
--carousel-nav-border-width |
Sets the border width of the previous and next buttons |
--carousel-nav-hover-background |
Sets the background color of the previous and next buttons on hover |
--carousel-nav-hover-color |
Sets the icon color of the previous and next buttons on hover |
--carousel-nav-hover-border-color |
Sets the border color of the previous and next buttons on hover |
--carousel-nav-shadow |
Sets the shadow of the previous and next buttons in the inline layout, when enabled. Unset for the header layout, and always unset while a button is disabled |
--carousel-nav-disabled-background |
Sets the background and border color of a disabled previous or next button |
--carousel-nav-disabled-color |
Sets the icon color of a disabled previous or next button |
--carousel-dot-border-radius |
Sets the corner radius of the pagination dots |
--carousel-dot-color |
Sets the color of the pagination dots |
--carousel-dot-active-color |
Sets the color of the active pagination dot |
--carousel-focus-outline-color |
Sets the keyboard focus outline color of the track, the previous and next controls and the dots |
Keyboard interactions
When focus is on the track (Tab reaches it as a single stop, exactly
like a native scrollable region):
| Key | Action |
|---|---|
ArrowRight |
Advances by scrollStep (wraps with loop) |
ArrowLeft |
Retreats by scrollStep (wraps with loop) |
Home |
Jumps to the first slide |
End |
Jumps to the last reachable page |
Tab / Shift+Tab |
Moves to the prev/next buttons, the dots, or the next focusable element on the page, each a normal stop, not a roving composite |
Right to left, ArrowLeft advances and ArrowRight retreats, so each arrow still moves toward the side it points to.
The prev/next buttons and pagination dots are all plain, individually
focusable <button> elements. Every native button behavior
(Enter/Space activation, focus visibility) comes for free, with
nothing custom to implement.
Semantics and screen readers
Aria roles
- The track exposes
role="region"andaria-roledescription="carousel", witharia-label(by default "Carousel" in the active locale, overridable via thearia-labelattribute, see below). - Every slide gets
role="group",aria-roledescription="slide", andaria-label="N of total", applied directly on whatever element you slotted in, re-applied automatically whenever slides are added, removed, or reordered. This is theWAI-ARIA carousel pattern 's own recommended labeling, and the reason a screen reader announces "group, 2 of 5" when landing on a slide. - Pagination dots are a plain
role="group"of buttons (notrole="tablist"/"tab", since there's no correspondingtabpanel, so overloading the tabs pattern here would be a misapplied, misleading pattern), each labeled "Go to slide N of total", witharia-current="true"on whichever one matches the active page. - Prev/next buttons carry
aria-controls="track"and disable themselves at the bounds (unlessloopis set) rather than wrapping silently, so their state always reflects what activating them will do.
Labeling the carousel
When a page has more than one carousel, or its purpose isn't already clear from a preceding heading, give it an accessible name:
<eun-carousel
full-slides
edge-fade
show-arrows
show-dots
aria-label="Featured products"
>
...
</eun-carousel>
aria-label set on <eun-carousel> is forwarded to the internal
role="region" element. It isn't visible, only announced. It defaults
to the generic "Carousel", in the active locale, when unset.
Live announcements
A visually-hidden role="status" aria-live="polite" region announces
the active slide ("Slide 2 of 5", or "Showing slides 2 to 4 of 9" once
visibleSlides is greater than 1) once scrolling settles. It's debounced
so it fires once per navigation, not on every intermediate frame while a
drag or a smooth-scroll animation is still moving. eunslidechange is
dispatched at the same moment, for syncing external UI (see the API
tab).
Visual and motion
Autoplay and WCAG 2.2.2
eun-carousel itself only covers half of
that. Whenever autoplay is set, it pauses on hover and keyboard
focus, and never resumes on its own until the pointer/focus actually
leaves. It renders no pause/play control of its own, on purpose (see
the "Why no built-in control" note below), so satisfying 2.2.2 fully is
on you. Pair autoplay with a visible pause/play button of your own,
wired to the public pauseAutoplay()/resumeAutoplay() methods. The
Autoplay example (Examples tab) is exactly that pattern, copy-pasteable
as-is. Calling pauseAutoplay() persists across hover/focus. Once
paused that way, hovering and leaving again doesn't quietly resume it
behind the user's back: only resumeAutoplay() does.
Why no built-in control
Earlier versions of this component rendered an always-visible pause/play
button itself. It was pulled in favor of the public
pauseAutoplay()/resumeAutoplay() methods, since autoplay's placement,
icon, and label are highly product-specific (a hero banner's controls
look nothing like a testimonial strip's), and baking in one fixed
design meant every consumer either lived with it or fought it. Exposing
the two methods keeps the underlying pause/resume behavior (including
the hover/focus handling above) in the component, while leaving the
actual control's markup and styling entirely up to whoever's building
the page.
Reduced motion
Slide transitions and the edge fades respect
prefers-reduced-motion: reduce. With that OS-level setting on,
smooth-scroll navigation (prev/next, dots, keyboard) becomes an instant
jump instead of an animated scroll, and the active-slide/fade
transitions are disabled outright.