Scroll Effects

Horizontal Scroll Section

A pinned section that turns vertical scrolling into horizontal movement, with built-in spring feel, a progress bar, and edge fading.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/horizontal-scroll-section.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

16
90
26
Active edited usageDefault props
<HorizontalScrollSection/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/horizontal-scroll-section.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/horizontal-scroll-section.

Usage

The outer section automatically stretches out enough vertical scroll height for its content; as the user scrolls down, the inner track is pinned at the center of the screen and shifts to the left. Each direct child is one horizontal panel.

import { HorizontalScrollSection } from "@/components/ui/horizontal-scroll-section";

<HorizontalScrollSection gap={24} showProgress>
  {items.map((item) => (
    <div key={item.id} className="h-80 w-96 rounded-xl border p-6">
      {item.content}
    </div>
  ))}
</HorizontalScrollSection>;

If the section sits inside a nested overflow-y-auto container, pass that container's ref to container and the component will use it as the reference for pinning and scrolling:

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[80vh] overflow-y-auto">
  <HorizontalScrollSection container={scrollerRef}>
    {/* Panels */}
  </HorizontalScrollSection>
</div>;

Props

PropTypeDefaultDescription
childrenReact.ReactNode—Panels laid out horizontally in order; each direct child is one panel
containerReact.RefObject<HTMLElement | null>—Ref of the nested scroll container; defaults to using the window as the reference
gapnumber16Horizontal spacing between panels (px)
smoothbooleantrueSmooth the displacement with a spring, giving it damped momentum; turn it off for 1:1 tracking with the scroll
stiffnessnumber90Spring stiffness (applies when smooth is true)
dampingnumber26Spring damping (applies when smooth is true)
showProgressbooleanfalseShow a horizontal progress bar at the bottom of the pinned area
fadeEdgesbooleanfalseFade out the left and right edges with a mask, hinting that there is more content
ariaLabelstring"Horizontal scroll section"Accessible label applied to the scroll section
classNamestring—The outer section (which determines the total scroll height)
viewportClassNamestring—The pinned viewport container
trackClassNamestring—The horizontal track

How it works

  • Automatic measurement: a ResizeObserver measures the track's scrollWidth and the viewport width, derives the vertical scroll distance from "track width − viewport width", and stretches the outer section's height accordingly. It recalculates automatically whenever the content, the window, or the container changes size.
  • Pinned displacement: the inner viewport is pinned with sticky top-0; useScroll reads the section's progress and useTransform maps it into a horizontal displacement x. With smooth on, that value additionally passes through useSpring for a soft, momentum-like settle.
  • Correct on the first frame: measurement runs synchronously in a layout effect, so the very first frame already carries the correct height and the layout never jumps. All timers, ResizeObserver instances, and event listeners are fully cleaned up on unmount.
  • Tuning: stiffness / damping control the feel and gap controls panel spacing; panel width and height are yours to set on the children (the example uses w-56 h-40).

Accessibility

  • When the user has "reduce motion" enabled at the system level, it switches to native overflow-x-auto horizontal scrolling: the content stays fully accessible and no long stretch of vertical scroll is consumed.
  • The scroll section carries role="region" and a customizable aria-label.
  • When keyboard Tab moves focus to a panel that is currently outside the viewport, the component converts that into the matching vertical scroll position and smoothly brings the panel into view, so focus never disappears off screen.