October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk9 min

How to Build a Carousel in Next.js: 3 Methods

Choose between native CSS scroll snap, a custom React component, and Embla for a Next.js carousel, with implementation patterns and accessibility checks.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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. 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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
Sale
HTML and CSS: Design and Build Websites
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • 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.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Embla 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leave a Reply

Your email address will not be published. Required fields are marked *

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.