UIDex

Carousel

الشريط الدوّار

aria-roledescription="carousel"

also calledslider، image slider، slideshow، content rotator، hero slider، banner rotator، كاروسيل، سلايدر، شريط الشرائح، معرض منزلق، عارض دوّار، شرائح متحرّكة

A carousel shows a set of slides in one slot, one slide or a slide and a half at a time, and every slide has a position the user can reach with the arrows, with the dots, or by swiping. That position belongs to the user, and it gets announced and numbered: "3 of 6". A marquee is not this. It crawls without stopping, has no current slide, and offers no way back to what already passed, so it decorates rather than navigates. A plain horizontal scroller is not this either: it moves with a finger or a trackpad and nothing more, with no controls and no slide semantics in the accessibility tree, and for a row of products it is usually the better choice, because a clipped edge already tells people there is more. Tabs part company on one point, since a tab label describes the panel it opens while a dot describes nothing beyond its own position. One cost belongs in every review of this pattern: whatever sits after slide one is seen by a small fraction of visitors, so a price, a signup button, or an offer you need people to read does not go in there.

If you called it…

"the pictures at the top that change by themselves""the arrows that flip through the banners""the little dots under the photo""the strip you swipe sideways that stops on each card""the rotating thing on the home page"

Live specimen

Interact with the demo. Every part is real and numbered.

١ من ٥شحن مجانيعلى كل الطلبات٢ من ٥خصم ٣٠٪المجموعة الشتوية٣ من ٥استرجاع ١٤ يوماًبلا رسوم٤ من ٥تقسيط ٤ دفعاتبدون فوائد٥ من ٥نقاط مضاعفةلأعضاء البرنامج
الشريحة ١ من ٥
RTLالشريحة الأولى عند اليمين، والتالي يمرّر نحو اليسار

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a carousel on CSS scroll-snap with no library. Wrap the whole thing in role="group" with aria-roledescription="carousel" and a clear aria-label. Inside it, the scroll container is the viewport: overflow-x: auto with scroll-snap-type: inline mandatory, scroll-padding-inline-start for the gutter, tabindex="0", and a name of its own, because Chrome only makes scrollers focusable on their own when they hold nothing focusable. The track inside it is a flex row of max-content width, and every slide carries scroll-snap-align: start, scroll-snap-stop: always, role="group", aria-roledescription="slide", and an aria-label that says "3 of 6". Add previous and next buttons with accessible names and 44px touch targets, pin them with inset-inline-start and inset-inline-end, and move with el.scrollBy({ left: step * dir }) where dir is -1 when getComputedStyle(el).direction is rtl. Never derive the current slide from scrollLeft. Compare each slide getBoundingClientRect against the logical edge of the viewport instead. Add a dot row where each dot is a button named after its slide number, the current one carries aria-current="true", and jumping uses slide.scrollIntoView({ inline: "start", block: "nearest" }). Put an aria-live="polite" region in the page from first paint that reports the new position after each move. If you add autoplay, ship a visible pause control with it, stop on hover, on focus, and under prefers-reduced-motion, and set the live region to off while it rotates. Use logical properties everywhere: inset-inline, margin-inline, padding-inline, scroll-padding-inline-start, and never left, right, or margin-left. For the two cases that have no logical form at all, the gradient scrim and background-position, write an explicit [dir="rtl"] override. Keep anything that has to be read out of slides two and beyond.

How it behaves right-to-left

This section is ours alone.

The inline axis reverses by itself

mirrors

Slide one starts at the right in Arabic and advancing moves leftward, which a scroll container, a flex track, and the dot row all do by themselves, because the inline axis follows direction. Do not flip it a second time: flex-direction: row-reverse under [dir="rtl"], or a reversed array in JavaScript, puts the visual order back the way English had it and leaves Tab order disagreeing with the eye. The exception is a track driven by transform: translateX(calc(-100% * var(--i))), since transforms know nothing about direction, so sign the offset with var(--dir) and set --dir: -1 inside [dir="rtl"].

The arrows: place, glyph, and job

mirrors

The Previous button belongs at the logical start, which in Arabic is the right, so place the pair with inset-inline-start and inset-inline-end rather than left and right. The chevron drawing mirrors with them: put scaleX(-1) on a class you use for arrows alone, or swap the two icons. The job mirrors too, because Next now scrolls toward the left: write el.scrollBy({ left: step * dir }) with dir at -1 under RTL, and have the key handler read ArrowLeft as Next.

The sign of scrollLeft

mirrors

Current engines start at zero on the first slide and go negative as you advance, older WebKit started at the maximum positive value and counted down, and IE counted up from zero. Anything shaped like Math.round(scrollLeft / slideWidth) therefore returns a negative or wrong slide number in Arabic. Stay off the sign entirely: jump with slide.scrollIntoView({ inline: "start", block: "nearest" }), which is logical already, and derive the current slide by comparing each getBoundingClientRect against the logical edge of the viewport.

What CSS will not flip for you

mirrors

scroll-snap-align: start is flow relative, so it becomes the right edge in Arabic on its own, and scroll-snap-type: inline states the intent more precisely than x. scroll-padding-left, on the other hand, keeps the gutter on the left after the flip, so write scroll-padding-inline-start. The scrim over a hero image is worse, because neither gradients nor background-position accept flow-relative values at all: linear-gradient(to right, …) and background-position: left center both stay where they were, so they need an explicit override inside [dir="rtl"].

The artwork stays as drawn

never mirrors

The photograph, a logo inside it, and the play and pause control of an autoplaying carousel keep their drawing in Arabic. The usual cause of a mirrored face or a backwards logo is a blanket rule like [dir="rtl"] svg, [dir="rtl"] img { transform: scaleX(-1) }, so reserve the flip for a class you put on the two arrows and nothing else. Playback controls point along the timeline rather than along the text, which is why the triangle faces the same way in both languages.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIAaria-roledescription="carousel"Goes on an element that already has role="group" or role="region" and a name, otherwise nothing is spoken.
ARIArole="group" + aria-roledescription="slide"On each slide, with a name that states the position, such as "3 of 6".
CSSscroll-snap-type: inline mandatoryThe logical axis, so it needs no RTL override, and it gives you the paging behaviour with no library.
CSSscroll-snap-stop: alwaysStops a fast flick from skipping several slides at once, and it belongs on the slide, not on the container.
CSS::scroll-marker + ::scroll-button()Dots and arrows the browser generates from the scroll container itself. Chrome shipped them in 135 and support elsewhere is still landing, so keep a fallback.
shadcn/ui<Carousel opts={{ direction: 'rtl' }}>The wrapper is Embla, and the library has to be told the direction explicitly. Its arrow positioning classes are physical, so check them.
Ant Design<Carousel autoplay dots />Built on react-slick, so it pauses on hover through the default pauseOnHover, but no visible pause control ships with it, so add one yourself.

See also

Updated · 2026-08-19