WebberUI

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

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

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

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
<HorizontalScrollSection />

Installation

npx shadcn@latest add "https://webberui.com/r/horizontal-scroll-section.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.ReactNodePanels 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
classNamestringThe outer section (which determines the total scroll height)
viewportClassNamestringThe pinned viewport container
trackClassNamestringThe 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.

On this page