ScrollZone
A strip of anything, laid out in one direction and scrolled in it. Cards, chips, avatars or thumbnails run across the box or down it, in as many lines as you ask for, with a pair of buttons for the pointer that has no wheel and no finger.
import { ScrollZone } from 'neba';
<ScrollZone label="Continue watching" spacing={3}>
{shows.map((show) => (
<Card key={show.name} className="w-40" title={show.name} />
))}
</ScrollZone>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| orientationshared | 'horizontal' | 'vertical' | 'horizontal' | Which way the children run, and therefore which way the zone scrolls |
| lines | number | 1 | How many rows a horizontal zone uses before it starts a new column: columns, in a vertical one. 2 is the shelf that holds twice as much in the same width |
| spacing | number | 2 | The gap between children, on Tailwind's spacing scale: 2 is 0.5rem, the same ladder GridContainer's spacing is on |
| buttons | 'auto' | 'always' | 'none' | 'auto' | When the scroll buttons are drawn. auto draws neither while everything fits and, at an end, removes an overlay button but disables an inline one, whose lane is held open either way. always is both from the first paint, none is neither |
| buttonPlacement | 'inline' | 'overlay' | 'inline' | Whether the buttons sit beside the strip or over it. inline stops the scroller where the button starts, so an item is cut off at its edge rather than sliding under it, and the lane is kept even while that button has nowhere to go, which is why an auto button is disabled there rather than removed |
| mode | 'item' | 'page' | 'hold' | 'item' | What pressing a button does: item moves to the next child, page moves by everything on screen, hold scrolls for as long as it is held. A press too short to be a hold falls back to one item |
| step | number | 1 | How many children one press moves, in item mode |
| speed | number | 900 | How fast a held button scrolls, in pixels a second |
| snap | boolean | false | Snaps the nearest child to the leading edge when the scrolling stops: dragging and the wheel included, not only the buttons |
| drag | boolean | true | Lets a mouse or a pen drag the strip along, the way a finger already does. Touch is left to the browser, whose own scrolling brings momentum and a scrollbar with it |
| wheel | boolean | false | Turns a wheel rolled over the strip into travel along it, and holds it at the ends too: the pointer leaving the strip is what gives the page its wheel back. A sideways trackpad swipe and a vertical zone are left alone |
| scrollbar | boolean | false | Shows the native scrollbar |
| variantshared | 'solid' | 'outline' | 'text' | 'solid' | Weight of the scroll buttons. The zone itself draws nothing |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The scroll buttons' size, and how far in from the edge they sit |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role of the buttons and the focus ring |
| densityshared | 'default' | 'compact' | 'default' | The buttons' padding only |
| locale | string | — | Which language the buttons name themselves in: a BCP 47 tag. Unsupported tags fall back to English |
| label | string | locale's word | What the scrollable region is called: "Categories", "Recent files". The strip is always a tab stop, because a reader with no pointer has to be able to move it; without a name the locale's generic word stands in |
| previousLabel | string | — | The back button's own name |
| nextLabel | string | — | The forward button's own name |
| children | ReactNode | — | What is being laid out. Every top-level child is one item of the strip |
Every other <div> attribute passes through to the root. The shared axes (variant size color density orientation) are defined in prop conventions.
Examples
orientation and lines
orientation decides which way the strip runs and therefore which way it scrolls. lines is how many rows a horizontal zone fills before it starts a new column: two lines hold twice as much in the same width, and the strip is still one scroll.
spacing is the gap between children, on the same scale as GridContainer's: 2 is 0.5rem.
mode
What a press of a button does. item moves to the next child along and step says how many at a time; page moves by everything currently on screen; hold scrolls for as long as the button is held, at speed pixels a second. A press too short to be a hold moves one item, so a quick tap is never a dead press.
buttons
auto (the default) draws neither while everything fits. At an end it does whichever costs less: an overlaid button with nowhere to go is removed, an inline one is disabled, since its lane is held open either way. always draws both from the first paint, including while everything still fits, which is what a strip whose content arrives later wants. none draws neither and leaves the strip to dragging, the arrow keys and whatever the pointer can already swipe with.
snap brings the nearest child to the leading edge whenever the scrolling stops, however it was scrolled.
buttonPlacement
inline (the default) puts the buttons beside the strip: the scroller stops where the button starts, so an item is cut off at the button's edge rather than sliding beneath it, and the button is legible over the page rather than over whatever it landed on. overlay puts them over the ends of the strip instead, which keeps every pixel of the box for content and lets an item pass under a button.
An inline button keeps its lane even while it has nowhere to go, or the strip would resize under the pointer that had just reached the end of it. That is also what buttons="auto" follows at an end: the lane is paid for either way, so an inline button stays there and is disabled, while an overlaid one is removed.
Running down the page
A vertical zone needs a height to scroll inside, and it takes it from the component: the root is a flex column and the scroller fills it, so className="h-full" on a box with a height is all it needs.
drag
A finger already scrolls the strip, because the mechanism is an ordinary scroll container and touch scrolling is the browser's own: with momentum, rubber-banding and a scrollbar that no handler reproduces. drag adds the same gesture for a mouse or a pen, and the click that would otherwise follow a real drag is swallowed, so pulling the strip past a card never opens it.
<ScrollZone drag={false} scrollbar>
{items}
</ScrollZone>wheel
A mouse has one wheel and it points down the page, which is the one axis a horizontal strip does not run along. wheel turns a wheel rolled over the strip into travel along it.
It is off by default, because a wheel taken from the page is the page's: a reader who meant to scroll past the shelf would be held by it instead, and this strip already has buttons for the pointer that has no other way along.
What it takes it keeps, at the ends as well, so a flick that runs out of strip does not turn into a jump down the article. Moving the pointer off the strip is what gives the page its wheel back. A trackpad swiping sideways is left alone, since that already scrolls the strip, and a vertical zone ignores the prop.
Accessibility
- The strip is focusable and scrolls with the arrow keys, which is the browser's own key handling on a scroll container, so it is already right under RTL.
labelnames the region and is what a screen reader reads before its contents. Without one the strip is focusable but unnamed.- The scroll buttons are real buttons with real names, and
previousLabel/nextLabel(orlocale) decide what those names are. - In
holdmode the buttons answer Enter and Space the same way they answer a press, scrolling while the key is down. - Nothing inside the strip is hidden when it is off screen: it is genuinely reachable by scrolling, and
aria-hiddenon it would be a lie a keyboard reader would fall into.