For a simple row of cards people can swipe or scroll, start with a horizontally overflowing element and CSS scroll snap. Use a custom React component when you need a small, deliberately tailored interaction; choose a carousel library such as Embla when you need drag handling, snap navigation, or grouped slides. The right choice depends on the interaction—not an assumed speed or bundle-size advantage.
Where a carousel belongs in a Next.js app
In the App Router, a route is rendered by a default-exported React component in a page file under app. Keep ordinary content in that page and put browser-dependent interaction in a client component. A carousel using React state, event handlers, or a library needs the client boundary; a CSS-only scrolling row does not need carousel JavaScript.
As an Amazon Associate I earn from qualifying purchases.
For example, a route at app/gallery/page.tsx can render a component imported from app/gallery/Carousel.tsx. Add 'use client' at the top of the latter when it uses state, event handlers, or a browser-facing carousel package.
Method 1: Build a small custom React carousel
Choose this when the interaction is intentionally narrow—for example, one visible item at a time with previous and next buttons. The component below renders a selected slide, disables controls at the ends, and lets keyboard users activate the native buttons. It does not autoplay or loop.
#1 Best Overall
1. Add the client component
Create app/gallery/Carousel.tsx:
'use client';
import { useState } from 'react';
type Slide = {
title: string;
description: string;
};
const slides: Slide[] = [
{ title: 'Plan', description: 'Choose the content your visitors need to see.' },
{ title: 'Build', description: 'Keep the interaction focused on the task.' },
{ title: 'Review', description: 'Check every slide at mobile and desktop widths.' },
];
export default function Carousel() {
const [active, setActive] = useState(0);
const slide = slides[active];
return (
<section aria-label="Project steps" aria-roledescription="carousel">
<div aria-live="polite" aria-atomic="true">
<article aria-roledescription="slide" aria-label={`${active + 1} of ${slides.length}`}>
<h2>{slide.title}</h2>
<p>{slide.description}</p>
</article>
</div>
<div className="carouselControls">
<button type="button" onClick={() => setActive((n) => Math.max(0, n - 1))} disabled={active === 0}>
Previous
</button>
<p>{active + 1} / {slides.length}</p>
<button type="button" onClick={() => setActive((n) => Math.min(slides.length - 1, n + 1))} disabled={active === slides.length - 1}>
Next
</button>
</div>
</section>
);
}
The disabled end controls make the bounds visible and prevent an accidental wrap to another slide. If looping is a real requirement, change the index logic deliberately and make the behavior clear to users. Do not add auto-advance by default: movement can interrupt reading, and any automatically moving carousel needs a way to pause.
2. Render it from the route
Create app/gallery/page.tsx:
import Carousel from './Carousel';
export default function GalleryPage() {
return (
<main>
<h1>Project steps</h1>
<Carousel />
</main>
);
}
What you own with custom state
This approach gives direct control of the markup and state, but it also makes your component responsible for interaction details. Decide how the first and last slides behave, how the current item is communicated, whether focus stays predictable, and how the layout changes at different widths. If only one slide is rendered at a time, ensure the slide change is communicated to assistive technology and that controls remain operable by keyboard.
Method 2: Use native scrolling with CSS scroll snap
For a swipeable strip of cards, native horizontal scrolling is often the simplest starting point. The browser handles touch and trackpad scrolling; CSS defines where scrolling should settle. This avoids a JavaScript carousel engine and naturally permits users to scroll through the row.
1. Make the row scrollable
In a server-rendered page or component, render the cards as ordinary elements:
const cards = [
{ title: 'Plan', description: 'Choose the content visitors need.' },
{ title: 'Build', description: 'Keep each card focused.' },
{ title: 'Review', description: 'Check the layout at different widths.' },
];
export default function CardRow() {
return (
<section aria-label="Project steps" className="cardScroller">
{cards.map((card) => (
<article className="cardSlide" key={card.title}>
<h2>{card.title}</h2>
<p>{card.description}</p>
</article>
))}
</section>
);
}
2. Apply overflow and snap positions
Add these rules to a stylesheet available to the component:
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
.cardScroller {
display: grid;
grid-auto-columns: minmax(min(85vw, 22rem), 1fr);
grid-auto-flow: column;
gap: 1rem;
overflow-x: auto;
padding: 0 1rem 1rem;
scroll-snap-type: x proximity;
overscroll-behavior-inline: contain;
}
.cardSlide {
scroll-snap-align: start;
border: 1px solid #c9c9c9;
border-radius: 0.75rem;
padding: 1rem;
}
@media (min-width: 48rem) {
.cardScroller {
grid-auto-columns: minmax(18rem, 1fr);
}
}
scroll-snap-type belongs on the scrolling container and scroll-snap-align on each item. The example uses proximity snapping, which does not force every scroll to stop at a snap point. Test the result with touch, trackpad, keyboard scrolling, and content that can grow beyond a card’s height.
When to use mandatory snapping
scroll-snap-type: x mandatory can make a tightly controlled row settle more firmly on snap positions. Use it cautiously: if a child contains content that overflows its parent, mandatory snapping may prevent users from scrolling all of that content into view. Variable-height, text-heavy, or otherwise content-dense slides deserve particular care; use less forceful snapping or redesign the content so it remains reachable.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →CSS also has newer carousel-related UI features, including generated scroll buttons and markers. Those are distinct from the established overflow-and-scroll-snap pattern above. Check support in the browsers you target before making newer features necessary to the experience.
Method 3: Add a React carousel library such as Embla
Use a library when the desired behavior goes beyond a scrollable row or a few explicit state changes: for example, drag interaction, controlled snap navigation, or advancing through groups of cards. Embla’s React integration uses a viewport ref around a container and its slides; its documentation recommends placing navigation controls outside the viewport to avoid conflicts with dragging. Its React wrapper also handles cleanup when the component unmounts.
Version matters. The retrieved Embla React documentation identifies itself as v9.0.0-rc03 and points users looking for the latest stable release to its v8 documentation. Match your installed package to the documentation for that version rather than copying an API example across versions. The example below illustrates the documented React component shape; verify the package version and matching API in your project before using it.
Rank #3
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
1. Install the React package
Install the Embla React package using your package manager, then check the installed version against the documentation you intend to follow. Avoid mixing a release-candidate example with a stable package without verifying compatibility.
2. Put controls outside the draggable viewport
A component using the documented viewport-ref pattern has this general structure:
'use client';
import useEmblaCarousel from 'embla-carousel-react';
const slides = [
{ title: 'Plan', description: 'Choose the content visitors need.' },
{ title: 'Build', description: 'Keep the interaction focused.' },
{ title: 'Review', description: 'Check behavior across screen sizes.' },
];
export default function EmblaCarousel() {
const [viewportRef, emblaApi] = useEmblaCarousel();
return (
<section aria-label="Project steps" aria-roledescription="carousel">
<div className="emblaViewport" ref={viewportRef}>
<div className="emblaContainer">
{slides.map((slide, index) => (
<article className="emblaSlide" key={slide.title} aria-roledescription="slide" aria-label={`${index + 1} of ${slides.length}`}>
<h2>{slide.title}</h2>
<p>{slide.description}</p>
</article>
))}
</div>
</div>
<div className="carouselControls">
<button type="button" onClick={() => emblaApi?.scrollPrev()}>Previous
<button type="button" onClick={() => emblaApi?.scrollNext()}>Next
</div>
</section>
);
}
Embla exposes previous/next snap navigation and the selected snap; its options include drag behavior and how many slides advance per action. Use those capabilities only when they answer a concrete interaction need. The library does not automatically settle questions about accessible names, keyboard operation, focus, or how slide changes are announced.
Choose by interaction, not by a generic performance claim
| What you need | Start with | Why |
|---|---|---|
| A simple row visitors can swipe or scroll | Native overflow and CSS scroll snap | Browser scrolling and CSS snap positions provide the core behavior without a carousel engine. |
| A deliberately small, tailored interaction | Custom React component | You control state and markup, and take responsibility for navigation and accessibility details. |
| Drag interaction, controlled snap navigation, grouped slides, or a richer gallery | Embla or another evaluated library | Embla documents React integration, snap navigation, drag behavior, and slide-group options. |
Compare the behavior you need, the degree of markup and interaction control, the accessibility work your team will own, and the dependency’s version maintenance. The available documentation establishes capabilities, not comparative speed or bundle size; measure your actual app if those factors are decision criteria.
Accessibility checks before shipping
W3C WAI states: “All functionality, including navigating between carousel items, must be operable by keyboard.” Use semantic buttons for controls, give the carousel a clear accessible name, and ensure slide changes are communicated when content is hidden or replaced. Keep focus behavior understandable as items move, and do not make essential information available only through auto-advancing slides.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #4
- Test every control using only the keyboard, including visible focus and sensible focus order.
- Use descriptive labels for the carousel and meaningful structure for each slide.
- Announce changes when navigation hides or replaces slide content, without making announcements excessively noisy.
- If slides move automatically, provide a pause mechanism and avoid movement that disrupts reading or focus.
- Check that all text and controls remain reachable on narrow screens and at enlarged text sizes.
Troubleshooting common implementation problems
The row does not scroll horizontally
Confirm the container has overflow-x: auto and its children are laid out in a row with widths that can exceed the available container width. If every child shrinks to fit, there may be no overflow to scroll.
Cards snap to awkward positions
Check that scroll-snap-type is on the actual scrolling container and scroll-snap-align is on the intended cards. Review gaps, padding, and card widths; then try proximity snapping if mandatory behavior feels restrictive.
Users cannot reach all of a slide’s content
Avoid mandatory snapping when child content can overflow its parent. Let the content fit, use a less forceful snap mode, or restructure the slide so it can be scrolled fully into view.
A custom carousel stops at the wrong slide
Inspect the state update and bounds. For a non-looping carousel, clamp the index between zero and the final slide index; disable the corresponding button at each end. For looping behavior, implement and test the wrap explicitly rather than relying on accidental arithmetic.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesEmbla controls conflict with dragging or fail after a dependency update
Place the navigation controls outside the draggable viewport and check that the code matches the installed Embla version. The cited React documentation is a release candidate and directs stable users to the v8 documentation; do not assume its API example applies unchanged to another version.
Best Value
Or skip the browser setup
If you want to inspect how a deployed carousel page renders without setting up a browser capture workflow, ScreenshotNeo can return a screenshot from one GET request. For example, replace the URL with your deployed page:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-site.example/gallery -o shot.webp
See the ScreenshotNeo API documentation. ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server provides screenshot tools for AI agents, including Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000.
Sign up for ScreenshotNeo free: 1,000 screenshots a month, no card required.
Recommended Free Tools
Frequently Asked Questions
Does a scroll-snap carousel need a client component in Next.js?
No. A row implemented with ordinary rendered elements and CSS overflow and scroll snap does not need React state or browser event handlers.
Should every Next.js carousel autoplay?
No. Add automatic movement only when it serves the experience, and provide a way for users to pause it.
Can Embla handle accessibility automatically?
No. The library provides carousel interaction capabilities, but the implementer remains responsible for keyboard access, meaningful structure, focus behavior, and communicating changes.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




