SwipeRow — native-feeling horizontal card rail

Scroll-snap rail with trackpad/mouse wheel, touch momentum, keyboard paging (←/→), and optional desktop controls. Styling is intentionally minimal and customizable.

Be sure to check out the links at the top right to real production examples from Campvue.com, both the home page and another production usage, where you can see great examples of button styling customization!

Required stylesheet
Import the package stylesheet once:import "@goodmanlabs/react-swipe-row/style.css"
(Next.js App Router: import this in app/layout.)
Styling model
Demos use Tailwind utility classes, but gapClassName and classNames are plain class hooks. Use Tailwind, CSS Modules, vanilla CSS, styled-components, etc.

Quick start

SwipeRow works in any React app. For Next.js (App Router), follow these steps:

1) Install
npm install @goodmanlabs/react-swipe-row
2) Import the stylesheet (once)
// app/layout.tsx
import "@goodmanlabs/react-swipe-row/style.css";
3) Use the component
"use client";

import SwipeRow from "@goodmanlabs/react-swipe-row";

export function Example() {
  return (
    <SwipeRow ariaLabel="Featured items">
      <div>Card 1</div>
      <div>Card 2</div>
      <div>Card 3</div>
    </SwipeRow>
  );
}

Note: SwipeRow is a client component because it relies on DOM APIs (scroll position, resize observers, media queries).

1

Default usage

You can render content either as children or via the items prop. Both produce the same UI.
Tip: click/focus the row, then use ←/→ to page.
A) Children
Often the simplest in JSX
Card 1
Default spacing
Card 2
Default spacing
Card 3
Default spacing
Card 4
Default spacing
Card 5
Default spacing
Card 6
Default spacing
Card 7
Default spacing
Card 8
Default spacing
Card 9
Default spacing
Card 10
Default spacing
Card 11
Default spacing
Card 12
Default spacing
import SwipeRow from "@goodmanlabs/react-swipe-row";

<SwipeRow ariaLabel="Default">
  {items}
</SwipeRow>
B) items prop
Useful when you already have an array
Card 1
Default spacing
Card 2
Default spacing
Card 3
Default spacing
Card 4
Default spacing
Card 5
Default spacing
Card 6
Default spacing
Card 7
Default spacing
Card 8
Default spacing
Card 9
Default spacing
Card 10
Default spacing
Card 11
Default spacing
Card 12
Default spacing
<SwipeRow ariaLabel="Default" items={items}  />
2

gapClassName

Adds spacing between items. This is just a class hook. Here we use Tailwind’s gap-4.
Card 1
With gapClassName
Card 2
With gapClassName
Card 3
With gapClassName
Card 4
With gapClassName
Card 5
With gapClassName
Card 6
With gapClassName
Card 7
With gapClassName
Card 8
With gapClassName
Card 9
With gapClassName
Card 10
With gapClassName
Card 11
With gapClassName
Card 12
With gapClassName
<SwipeRow items={items} gapClassName="gap-4" />
3

showControls

Controls can be auto (default), always, or never.
Auto uses a “desktop-ish” media query:(hover: hover) and (pointer: fine). On touch devices, arrows are hidden by default.
A) auto (default)
Shows controls on desktop-ish devices
Card 1
Controls demo
Card 2
Controls demo
Card 3
Controls demo
Card 4
Controls demo
Card 5
Controls demo
Card 6
Controls demo
Card 7
Controls demo
Card 8
Controls demo
Card 9
Controls demo
Card 10
Controls demo
Card 11
Controls demo
Card 12
Controls demo
<SwipeRow items={items} showControls="auto" gapClassName="gap-4" />  // default
B) always
Forces arrows on all devices (including touch)
Card 1
Controls demo
Card 2
Controls demo
Card 3
Controls demo
Card 4
Controls demo
Card 5
Controls demo
Card 6
Controls demo
Card 7
Controls demo
Card 8
Controls demo
Card 9
Controls demo
Card 10
Controls demo
Card 11
Controls demo
Card 12
Controls demo
<SwipeRow items={items} showControls="always" gapClassName="gap-4" />
C) never
Swipe/scroll only (no arrows)
Card 1
Controls demo
Card 2
Controls demo
Card 3
Controls demo
Card 4
Controls demo
Card 5
Controls demo
Card 6
Controls demo
Card 7
Controls demo
Card 8
Controls demo
Card 9
Controls demo
Card 10
Controls demo
Card 11
Controls demo
Card 12
Controls demo
<SwipeRow items={items} showControls="never" gapClassName="gap-4" />
4

pageFactor

Controls and keyboard paging scroll by a fraction of the visible width. Lower values make smaller steps; higher values “page” farther.
Tip: focus the row and use ←/→ to feel the difference.
A) pageFactor = 0.5
Smaller hops
Card 1
pageFactor demo
Card 2
pageFactor demo
Card 3
pageFactor demo
Card 4
pageFactor demo
Card 5
pageFactor demo
Card 6
pageFactor demo
Card 7
pageFactor demo
Card 8
pageFactor demo
Card 9
pageFactor demo
Card 10
pageFactor demo
Card 11
pageFactor demo
Card 12
pageFactor demo
<SwipeRow items={items} pageFactor={0.5} gapClassName="gap-4" />
B) pageFactor = 0.9 (default)
Good balance
Card 1
pageFactor demo
Card 2
pageFactor demo
Card 3
pageFactor demo
Card 4
pageFactor demo
Card 5
pageFactor demo
Card 6
pageFactor demo
Card 7
pageFactor demo
Card 8
pageFactor demo
Card 9
pageFactor demo
Card 10
pageFactor demo
Card 11
pageFactor demo
Card 12
pageFactor demo
<SwipeRow items={items} pageFactor={0.9} gapClassName="gap-4" />  // default
C) pageFactor = 1.0
Full-page steps
Card 1
pageFactor demo
Card 2
pageFactor demo
Card 3
pageFactor demo
Card 4
pageFactor demo
Card 5
pageFactor demo
Card 6
pageFactor demo
Card 7
pageFactor demo
Card 8
pageFactor demo
Card 9
pageFactor demo
Card 10
pageFactor demo
Card 11
pageFactor demo
Card 12
pageFactor demo
<SwipeRow items={items} pageFactor={1.0} gapClassName="gap-4" />
5

snap

Enables CSS scroll-snap (default). With snap on, items settle into alignment. With snap off, the rail scrolls freely.
A) snap = true (default)
Items snap into alignment
Card 1
snap demo
Card 2
snap demo
Card 3
snap demo
Card 4
snap demo
Card 5
snap demo
Card 6
snap demo
Card 7
snap demo
Card 8
snap demo
Card 9
snap demo
Card 10
snap demo
Card 11
snap demo
Card 12
snap demo
<SwipeRow items={items} snap gapClassName="gap-4" />  // default
B) snap = false
Free scrolling (no snapping)
Card 1
snap demo
Card 2
snap demo
Card 3
snap demo
Card 4
snap demo
Card 5
snap demo
Card 6
snap demo
Card 7
snap demo
Card 8
snap demo
Card 9
snap demo
Card 10
snap demo
Card 11
snap demo
Card 12
snap demo
<SwipeRow items={items} snap={false} gapClassName="gap-4" />
6

scrollerStyle

Optional inline style passthrough for the scroller element. Useful for one-off tweaks like scrollbarGutter, padding, etc.
(This prop styles the scroller div — not the outer wrapper.)
A) Default
No scrollerStyle
Card 1
scrollerStyle demo
Card 2
scrollerStyle demo
Card 3
scrollerStyle demo
Card 4
scrollerStyle demo
Card 5
scrollerStyle demo
Card 6
scrollerStyle demo
Card 7
scrollerStyle demo
Card 8
scrollerStyle demo
Card 9
scrollerStyle demo
Card 10
scrollerStyle demo
Card 11
scrollerStyle demo
Card 12
scrollerStyle demo
<SwipeRow items={items} gapClassName="gap-4" />
B) With scrollerStyle
Adds padding + subtle tint on the scroller
Card 1
scrollerStyle demo
Card 2
scrollerStyle demo
Card 3
scrollerStyle demo
Card 4
scrollerStyle demo
Card 5
scrollerStyle demo
Card 6
scrollerStyle demo
Card 7
scrollerStyle demo
Card 8
scrollerStyle demo
Card 9
scrollerStyle demo
Card 10
scrollerStyle demo
Card 11
scrollerStyle demo
Card 12
scrollerStyle demo
<SwipeRow
  items={items}
  gapClassName="gap-4"
  scrollerStyle={{
    paddingTop: 12,
    paddingBottom: 12,
    borderRadius: 12,
    background: "rgba(255,255,255,0.03)",
  }}
/>
7

classNames

Fine-grained class hooks for styling the wrapper, scroller, items, and controls.
These are plain class names — Tailwind is used here for convenience, but any CSS approach works.
A) Default
No classNames
Card 1
classNames demo
Card 2
classNames demo
Card 3
classNames demo
Card 4
classNames demo
Card 5
classNames demo
Card 6
classNames demo
Card 7
classNames demo
Card 8
classNames demo
Card 9
classNames demo
Card 10
classNames demo
Card 11
classNames demo
Card 12
classNames demo
<SwipeRow items={items} gapClassName="gap-4" />
B) Custom classNames
Styled scroller + item padding + custom arrow buttons
Card 1
classNames demo
Card 2
classNames demo
Card 3
classNames demo
Card 4
classNames demo
Card 5
classNames demo
Card 6
classNames demo
Card 7
classNames demo
Card 8
classNames demo
Card 9
classNames demo
Card 10
classNames demo
Card 11
classNames demo
Card 12
classNames demo
<SwipeRow
  items={items}
  gapClassName="gap-4"
  classNames={{
    root: "relative",
    scroller: "rounded-xl border border-zinc-800 bg-zinc-950/40 ring-1 ring-white/5",
    item: "px-2",
    controlButton: "backdrop-blur-md bg-zinc-900/80 border border-zinc-700 text-zinc-100 shadow-sm hover:bg-zinc-800 disabled:opacity-40 disabled:cursor-not-allowed",
    prevButton: "left-3 top-1/2 -translate-y-1/2 rounded-full w-10 h-10",
    nextButton: "right-3 top-1/2 -translate-y-1/2 rounded-full w-10 h-10",
  }}
/>
Note: On touch devices, controls are hidden by default. To ensure your custom buttons appear everywhere, combine this with showControls="always".
8

ariaLabel + id

SwipeRow renders a focusable scroll region for keyboard paging. ariaLabel names that region for screen readers. id provides a stable identifier so the control buttons can set aria-controls.
Tip: click/focus the row, then use ←/→ to page. You can also Tab to the buttons.
A) ariaLabel only (recommended minimum)
Names the scroll region
Card 1
a11y demo
Card 2
a11y demo
Card 3
a11y demo
Card 4
a11y demo
Card 5
a11y demo
Card 6
a11y demo
Card 7
a11y demo
Card 8
a11y demo
Card 9
a11y demo
Card 10
a11y demo
Card 11
a11y demo
Card 12
a11y demo
<SwipeRow ariaLabel="Featured campsites" items={items} gapClassName="gap-4" />
B) ariaLabel + id (stable aria-controls)
Useful for deterministic DOM hooks and a11y tooling
<SwipeRow
  ariaLabel="Featured campsites"
  id="demo-featured-campsites"
  items={items}
  gapClassName="gap-4"
  showControls="always"
/>
Note: id is optional. If omitted, SwipeRow uses a stable React-generated id internally. Provide your own if you want the same aria-controls value across renders/environments.
9

className (root wrapper)

Adds a class to the outer wrapper that contains the scroller + (optional) controls. Useful for layout styling, spacing, and positioning in your app.
A) Default
No root wrapper class
Card 1
className demo
Card 2
className demo
Card 3
className demo
Card 4
className demo
Card 5
className demo
Card 6
className demo
Card 7
className demo
Card 8
className demo
Card 9
className demo
Card 10
className demo
Card 11
className demo
Card 12
className demo
<SwipeRow items={items} gapClassName="gap-4" />
B) With className
Wrapper gets padding + border + rounded corners
Card 1
className demo
Card 2
className demo
Card 3
className demo
Card 4
className demo
Card 5
className demo
Card 6
className demo
Card 7
className demo
Card 8
className demo
Card 9
className demo
Card 10
className demo
Card 11
className demo
Card 12
className demo
<SwipeRow
  items={items}
  gapClassName="gap-4"
  className="rounded-2xl border border-zinc-800 bg-zinc-950/40 p-3"
/>