Skip to main content
Urbicon UI

Scrollerexperimental

A horizontal row of equal-rank items that scrolls only when it runs out of room.

Playground

Sync

Keeps every device on the same page.

Audit

Every change, with who and when.

Reports

Numbers your board actually reads.

Access

Roles, invites and passkeys.

API

Everything the UI does, scriptable.

Align
Item width
Snap
Controls
Indicator
Emphasis
<script lang="ts">
  import { Scroller } from '@urbicon-ui/blocks';

  const label = 'Main features';
  const cards = [
    { id: 'sync', title: 'Sync', body: 'Keeps every device on the same page.' },
    { id: 'audit', title: 'Audit', body: 'Every change, with who and when.' },
    { id: 'reports', title: 'Reports', body: 'Numbers your board actually reads.' },
    { id: 'access', title: 'Access', body: 'Roles, invites and passkeys.' },
    { id: 'api', title: 'API', body: 'Everything the UI does, scriptable.' }
  ];
</script>

<Scroller
  {label}
>
  {#each cards as card (card.id)}
    <article class="border-border-subtle bg-surface-elevated rounded-contain border p-4">
      <p class="text-text-primary text-sm font-semibold">{card.title}</p>
      <p class="text-text-secondary mt-1 text-sm">{card.body}</p>
    </article>
  {/each}
</Scroller>

01 Purpose

A Scroller is a horizontal row that scrolls only when it runs out of room. Where every item fits it stays an ordinary row, by default without a scrollbar, buttons or a tab stop. Once it overflows it becomes a focusable role="group" the keyboard can scroll, with snapping and jump buttons.

Every direct child is one item. The row sets each child's width from itemBasis and stretches them to a common height, so a card component you already have drops in unchanged. It takes any CSS length (16rem by default, a clamp() for a responsive row) or auto, which leaves every item at its own content width. label is required, because an unnamed group is a nameless box to a screen reader.

The jump buttons appear on their own once the row overflows. controls="none" leaves the row to the scrollbar and the keyboard, controls="always" keeps them mounted and disables them at the ends, and indicator="dots" adds a dot per position the row can scroll to.

alignReach for it when
start (default)Comparable, equal-rank items you sweep across: feature cards, media, filter chips.
centerOne item leads at a time and the count should stay visible. Pair it with emphasis to lift the centred item and indicator="dots" to show the count.

Reach for a Tab panel instead when one item should be presented at a time, like a screenshot or a live demo. It carries the right semantics for that. A Scroller is for items you compare by sweeping across them.

02 Examples

Same row, two widths

itemBasis sets each item's width, which decides when the row overflows. The same three cards sit in a wide and a narrow container. Below, the row runs out of room and picks up snapping, a tab stop and its jump buttons. The narrow cell needs min-w-0, which the snippet shows.

Room for all three, a plain row

Sync

Keeps every device on the same page, offline included.

Audit

Every change recorded, with who changed it and when.

Reports

Numbers your board actually reads.

Same cards, out of room, now scrollable

Sync

Keeps every device on the same page, offline included.

Audit

Every change recorded, with who changed it and when.

Reports

Numbers your board actually reads.

<!-- In a container with room for every card: an ordinary row. -->
<Scroller label="Main features" itemBasis="13rem">
  {#each features as feature (feature.id)}
    <FeatureCard {...feature} />
  {/each}
</Scroller>

<!-- The same row in a narrow grid or flex cell. Without min-w-0 the cell
     refuses to shrink below its content and widens its column instead of
     letting the row scroll. -->
<div class="min-w-0 max-w-[22rem]">
  <Scroller label="Main features" itemBasis="13rem">
    {#each features as feature (feature.id)}
      <FeatureCard {...feature} />
    {/each}
  </Scroller>
</div>

Centred stage

align=center makes the middle item the subject and pads the track so the first and last can reach the centre. Keep itemBasis narrow enough that a neighbour still peeks in beside the centred item: make the items much wider and the padding takes over the row, which the component warns about in DEV. emphasis=strong lifts whichever card has arrived in the middle, and indicator=dots shows a count the partly-visible row cannot.

Sync

Keeps every device on the same page, offline included.

Audit

Every change recorded, with who changed it and when.

Reports

Numbers your board actually reads.

Access

Roles, invitations and passkeys out of the box.

API

Everything the interface does, scriptable.

<Scroller
  label="Main features"
  itemBasis="15rem"
  align="center"
  emphasis="strong"
  indicator="dots"
>
  {#each features as feature (feature.id)}
    <FeatureCard {...feature} />
  {/each}
</Scroller>

Filter bar

itemBasis=auto lets each chip keep its own width instead of taking a shared one, and a tight gap holds the bar together. No indicator: a dot per chip would be noise, and the chips already label themselves.
All Active Draft Archived Shared with me Recently changed Needs review Assigned to me Due this week Waiting on client Blocked Done
<Scroller label="Filters" itemBasis="auto" gap="sm">
  {#each filters as filter (filter)}
    <Badge variant="outlined">{filter}</Badge>
  {/each}
</Scroller>

03 Customization

Tinted rail

slotClasses reaches the inner slots by name. The viewport slot is the scroll track, so a wash and a container radius there frame the row as a tray while the cards keep their elevated surface. The other slots are root, controls, control, indicator and dot, listed with the rest of the props below.

Sync

Keeps every device on the same page, offline included.

Audit

Every change recorded, with who changed it and when.

Reports

Numbers your board actually reads.

Access

Roles, invitations and passkeys out of the box.

API

Everything the interface does, scriptable.

<Scroller
  label="Main features"
  itemBasis="14rem"
  slotClasses={{ viewport: 'bg-primary-subtle rounded-contain px-4 py-3' }}
>
  {#each features as feature (feature.id)}
    <FeatureCard {...feature} />
  {/each}
</Scroller>

This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.

04 Accessibility

Keyboard

While the row overflows, the scroll container takes a tab stop and becomes a named role="group", which is what label names. A row that fits takes no stop, so a keyboard user never pays a press for a row with nothing to scroll.

Once the container has focus, , Home, End and Page Up / Page Down scroll it. The browser handles those, with the platform's own snapping and inertia, and the component adds no key handling of its own. Items that are themselves focusable (links, buttons) stay in the tab order and scroll into view when focused, so a row of clickable cards keeps working without the container stop.

Controls and indicator

Jump buttons and dots are real buttons with labels. Dots appear only while the row overflows and has more than one position to rest at, and so do the jump buttons unless controls="always" pins them. The dot the row currently rests at carries aria-current. A jump button moves the row by one viewport where align is start, and by one item where it is center, so a long chip bar pages instead of stepping through thirty chips.

A dot stands for a position the row can scroll to, not for an item. On a centred row that comes to one dot per item. On a start-aligned row the last items share the end of the scroll range, so one dot covers them and says so (“Items 4–5 of 5”). Where that count would confuse more than it orients, leave indicator off.

Motion

Smooth scrolling and the emphasis lift both collapse under prefers-reduced-motion. The lift follows scroll position rather than a clock, and where the browser does not support that (Firefox before 156) the row behaves identically, just flat.

05 API Reference

18 props
18 props 2 required
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

3 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { Scroller } from '@urbicon-ui/blocks';
import type { ScrollerProps, ScrollerAlign } from '@urbicon-ui/blocks';