CSS-Only Carousel with Scroll Snap
A carousel is a row of content you can scroll sideways. Browsers do that natively — with touch, trackpad, mouse and keyboard. Add scroll snap and it locks onto each slide. No slider library, no JavaScript, every slide in the HTML.
- JavaScript
- 0 KB
- Built with
scroll-snap-type,::scroll-button,::scroll-marker- Browsers
- All current browsers; arrows and dots in Chrome and Edge
- Updated
Yes. Put the slides in a flex row with overflow-x: auto, then add scroll-snap-type: x mandatory to the row and scroll-snap-align: start to each slide. Swiping, scrolling and keyboard all work natively. Chrome and Edge can even draw arrows and dots with ::scroll-button and ::scroll-marker.
Live demo
Six of our free tools in a carousel. Swipe on a phone, scroll sideways with a trackpad, or click into the carousel and use the ← → keys. In Chrome and Edge you also see arrows and dots — drawn entirely by CSS.
How the CSS carousel works
- A list that scrolls sideways
The slides are list items in a flex row.
overflow-x: autoturns the row into a scroll container — touch, trackpad, mouse wheel with Shift and keyboard all work out of the box. - Scroll snap locks onto slides
scroll-snap-type: x mandatoryon the list andscroll-snap-align: starton each slide make every scroll end exactly at the start of a slide. - The next slide peeks in
Slides are at most 80% wide, so part of the next one is always visible. That shows people there is more to see — without arrows.
- Keyboard-ready
tabindex="0"lets keyboard users focus the carousel and scroll it with the arrow keys. Tabbing through the links inside also scrolls each slide into view. - Arrows and dots from CSS
In Chrome and Edge,
::scroll-button(left/right)creates real, accessible buttons, and::scroll-markercreates one dot per slide. Anchor positioning places the arrows on the carousel's edges. - Only where supported
All of that sits inside
@supports selector(::scroll-marker). Firefox and Safari skip it and show the plain, fully working scroll carousel.
Copy the HTML and CSS
Exactly the code running in the demo. For an image carousel, put a <figure> with an <img> in each slide — with width, height and loading="lazy" on every image except the first.
<ul class="ui-carousel" tabindex="0" aria-label="Free tools for Chinese websites">
<li class="ui-carousel__slide">
<p class="ui-carousel__kit">Website Kit</p>
<h3><a href="/tools/chinese-filler-text/">Chinese Dummy Text</a></h3>
<p>Realistic Chinese placeholder text to test layouts, fonts and line breaks.</p>
</li>
<li class="ui-carousel__slide">
<p class="ui-carousel__kit">Website Kit</p>
<h3><a href="/tools/chinese-font-stack-generator/">Chinese Font Stack</a></h3>
<p>A font-family stack with PingFang and Microsoft YaHei — no web font download.</p>
</li>
<li class="ui-carousel__slide">
<p class="ui-carousel__kit">Website Kit</p>
<h3><a href="/tools/chinese-font-subset/">Chinese Font Subset</a></h3>
<p>Cut a 6 MB Chinese web font down to a few hundred KB.</p>
</li>
<li class="ui-carousel__slide">
<p class="ui-carousel__kit">Website Kit</p>
<h3><a href="/tools/pinyin-slug-generator/">Pinyin URL Slugs</a></h3>
<p>Clean Pinyin URLs from Chinese page titles, in batches of up to 200.</p>
</li>
<li class="ui-carousel__slide">
<p class="ui-carousel__kit">Baidu Kit</p>
<h3><a href="/tools/baidu-serp-preview/">Baidu SERP Preview</a></h3>
<p>See how titles and descriptions are cut in Baidu results, desktop and mobile.</p>
</li>
<li class="ui-carousel__slide">
<p class="ui-carousel__kit">Planning</p>
<h3><a href="/tools/qr-code-generator/">QR Codes for China</a></h3>
<p>Static QR codes that never expire — with a check whether your link opens in China.</p>
</li>
</ul>/* Carousel — no JavaScript.
A horizontal scroll container with scroll snap: swipe, scroll
or use the arrow keys. Chrome and Edge also draw arrows and dots
with ::scroll-button and ::scroll-marker. */
.ui-carousel {
--ui-bg: #ffffff;
--ui-text: #1b1f3b;
--ui-muted: #4b5070;
--ui-accent: #4f46e5;
display: flex;
gap: 1rem;
margin: 0;
padding: 0.25rem 0.25rem 1rem;
list-style: none;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-padding-inline: 0.25rem;
scroll-behavior: smooth;
scrollbar-width: thin;
font: 400 0.9375rem/1.55 system-ui, -apple-system, "Segoe UI", sans-serif;
}
.ui-carousel:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: 4px;
border-radius: 12px;
}
/* Slides: the next one always peeks in, so people see there is more */
.ui-carousel__slide {
flex: 0 0 min(17rem, 80%);
scroll-snap-align: start;
padding: 1.25rem;
border-radius: 12px;
background: var(--ui-bg);
color: var(--ui-muted);
}
.ui-carousel__slide h3 {
margin: 0 0 0.4rem;
font-size: 1.0625rem;
line-height: 1.3;
}
.ui-carousel__slide h3 a {
color: var(--ui-text);
text-decoration: none;
}
.ui-carousel__slide h3 a:hover {
color: var(--ui-accent);
text-decoration: underline;
}
.ui-carousel__slide a:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: 2px;
border-radius: 3px;
}
.ui-carousel__slide p {
margin: 0;
}
.ui-carousel__slide .ui-carousel__kit {
margin-bottom: 0.5rem;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ui-accent);
}
/* Bonus in Chrome and Edge: arrows and dots, generated by CSS */
@supports selector(::scroll-marker) {
.ui-carousel {
anchor-name: --ui-carousel;
scroll-marker-group: after;
}
.ui-carousel::scroll-button(*) {
position: absolute;
position-anchor: --ui-carousel;
width: 2.5rem;
height: 2.5rem;
border: 0;
border-radius: 50%;
background: var(--ui-bg);
color: var(--ui-text);
box-shadow: 0 4px 14px rgb(15 18 40 / 0.25);
font-size: 1.4rem;
cursor: pointer;
}
.ui-carousel::scroll-button(*):disabled {
opacity: 0.35;
cursor: default;
}
.ui-carousel::scroll-button(left) {
content: "‹" / "Previous";
position-area: left center;
translate: 50% 0;
}
.ui-carousel::scroll-button(right) {
content: "›" / "Next";
position-area: right center;
translate: -50% 0;
}
.ui-carousel::scroll-marker-group {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 0.5rem;
}
.ui-carousel__slide::scroll-marker {
content: "";
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background: rgb(255 255 255 / 0.35);
}
.ui-carousel__slide::scroll-marker:target-current {
background: #fff;
}
}All class names start with ui-. The dots are styled for a dark background — change the two ::scroll-marker colours for a light one.
Accessibility and browser support
| Feature | Support | Details |
|---|---|---|
| Swipe, scroll, snap | All | Scroll snap works in every current browser. |
| Keyboard | All | Focus the carousel and use the arrow keys, or Tab through the links. |
| Screen readers | All | A labelled list: “Free tools for Chinese websites, list, 6 items”. |
| Arrows and dots | Chrome, Edge | ::scroll-button and ::scroll-marker, Chrome/Edge 135+. Not yet in Firefox or Safari. |
| Autoplay | Avoid | Slides that move by themselves need a pause button (WCAG 2.2.2) — CSS animation can't offer one. Most visitors ignore auto-rotating slides anyway. |
#slide-1?
Older CSS carousels use links to slide ids as dots. They work, but each click adds an entry to the browser history and can scroll the whole page up or down to line the slide up. If you need arrows in Firefox and Safari today, a few lines of JavaScript that call scrollBy() are the cleaner fallback — the carousel keeps working without them.
Carousels and SEO
Every slide is in the HTML. Search engines read slide six as easily as slide one, and follow every link. Carousels that fetch their slides with JavaScript, or build them from JSON, can leave Baidu and other crawlers that don't run scripts with an empty box.
But visitors rarely reach slide six. Put the content a page should rank for — and the offer you most want clicked — on the page itself or on the first slide, not deep inside a carousel.
Fast on phones. No slider library means no extra download and no layout jump while a script sets the carousel up. With loading="lazy" on images after the first, off-screen slides don't slow the page down.
Questions about CSS carousels
Can you make a carousel with only HTML and CSS?
Yes. A horizontally scrolling flex row with scroll snap is a complete carousel: it swipes, snaps to each slide and works with the keyboard.
How do I add arrows to a CSS-only carousel?
In Chrome and Edge, with ::scroll-button(left) and ::scroll-button(right), as in the demo. For Firefox and Safari, add a small script that scrolls the container — or rely on swipe and keyboard, which work everywhere.
Is scroll snap supported in all browsers?
Yes, scroll snap has worked in all major browsers for years. Only the generated arrows and dots are limited to Chrome and Edge so far.
Do search engines see all carousel slides?
Yes, if all slides are in the HTML, as here. Slides loaded by JavaScript may be missed — especially by Baidu.
Can a CSS carousel play automatically?
With keyframe animations, technically yes — but visitors can't pause it, which fails accessibility guidelines. We recommend not auto-playing carousels at all.
This FAQ is built without JavaScript — see the accordion pattern.
Do your product sliders work for Baidu — and for buyers?
We build and review Chinese landing pages and product pages: what crawlers can see, what loads fast in China, and what visitors actually click. Then we run Baidu Ads and Baidu SEO for Western companies.
CSS-First UI Patterns are written and maintained by Jademond Digital, a Chinese digital marketing agency founded in Guangzhou in 2012. We build and audit websites for Baidu, Google and AI search — which is why we care what works without JavaScript. About us →