Once UI Handbook
Beginner Guides
What is Once UI?
Install & config
Page skeleton
Your first page
Build And Launch
How to launch a portfolio site that actually gets you hired
How to build a documentation site with MDX and Next.js
How to ship a landing page and dashboard with authentication in Next.js
How to launch a social app with Next.js and Supabase
Vibe Coding
Introduction to vibe coding
Set up your local dev environment
Essential tools for vibe coding
Build your first Once UI app
Common Patterns
Hero section
Static panel
Form layout
Highlighted card
Responsive stacking
Decoration layers
Dialog & modal
Toast & feedback
Tables & lists
Loading states
App shell navigation
Dashboard & charts
Chat & messaging
Auth & verification
Settings & split panels
Data filters & toolbar
Command palette
Pricing & plans
Accordion & FAQ
Media & uploads
Empty & error states
SEO, Open Graph, and structured data
Documentation code blocks with live preview
Carousels, galleries, and before/after comparisons
Date pickers and scheduling
Context menus and dropdown actions
Tags, chips, and multi-value inputs
Roadmap & kanban
Form inputs & controls
Progress, status & badges
Scrolling & feeds
Profile, avatars & identity
Social proof & logo clouds
Masonry & media grids
Table pagination, search & bulk selection
Banners & announcements
Footer layouts
Onboarding & first run
Waitlist & coming soon
Design Tips
Color & surfaces
Harness overview
Row, Column & Grid
Spacing & rhythm
Icons & buttons
Reveal & motion
Theme tokens, FOUC-free init, and runtime style controls
Tooltips and hover cards
Block updates
Block updates changelog
Agent Resources
Column, not Flex
Semantics over CSS
Typography scale
Common gotchas
Block anchors
Validate generated code
Pro block registry
Match task bundles
TrademarkTrademark
Ctrl k
Search...
Sign up
Once UI Handbook
Beginner Guides
What is Once UI?
Install & config
Page skeleton
Your first page
Build And Launch
How to launch a portfolio site that actually gets you hired
How to build a documentation site with MDX and Next.js
How to ship a landing page and dashboard with authentication in Next.js
How to launch a social app with Next.js and Supabase
Vibe Coding
Introduction to vibe coding
Set up your local dev environment
Essential tools for vibe coding
Build your first Once UI app
Common Patterns
Hero section
Static panel
Form layout
Highlighted card
Responsive stacking
Decoration layers
Dialog & modal
Toast & feedback
Tables & lists
Loading states
App shell navigation
Dashboard & charts
Chat & messaging
Auth & verification
Settings & split panels
Data filters & toolbar
Command palette
Pricing & plans
Accordion & FAQ
Media & uploads
Empty & error states
SEO, Open Graph, and structured data
Documentation code blocks with live preview
Carousels, galleries, and before/after comparisons
Date pickers and scheduling
Context menus and dropdown actions
Tags, chips, and multi-value inputs
Roadmap & kanban
Form inputs & controls
Progress, status & badges
Scrolling & feeds
Profile, avatars & identity
Social proof & logo clouds
Masonry & media grids
Table pagination, search & bulk selection
Banners & announcements
Footer layouts
Onboarding & first run
Waitlist & coming soon
Design Tips
Color & surfaces
Harness overview
Row, Column & Grid
Spacing & rhythm
Icons & buttons
Reveal & motion
Theme tokens, FOUC-free init, and runtime style controls
Tooltips and hover cards
Block updates
Block updates changelog
Agent Resources
Column, not Flex
Semantics over CSS
Typography scale
Common gotchas
Block anchors
Validate generated code
Pro block registry
Match task bundles
TrademarkTrademark
Once UIDocumentationBlog
© Once UI. All rights reserved.
Built with Aveiro
Common patterns

Scrolling & feeds

Scroller for horizontal chip rails, InfiniteScroll for paginated feeds, and overflow patterns without scroll traps.
Updated 21d ago
Progress, status & badges
Profile, avatars & identity
6 min · Pattern
Scroller for horizontal chip and card rails, InfiniteScroll for paginated feeds, and overflow patterns for chat timelines and activity lists.

What this pattern covers

Long content needs deliberate scroll behavior:
  • Scroller — horizontal rails with arrow buttons and edge fades (chips, tabs, card carousels)
  • InfiniteScroll — load-more feeds with intersection observer sentinel
  • Plain overflow — overflowX="auto" or overflowY="auto" on Row/Column for simple cases
Gold references: packages/core/ai/examples/chat.tsx (feed scrolling), packages/core/ai/examples/blocks/Feed3.tsx, Roadmap1 column overflow.

When to use which

Need
Component
Why
Horizontal chip/tab railScroller `direction="row"`Arrow buttons + fade edges
Paginated activity feedInfiniteScrollAuto-fetches when sentinel visible
Kanban columns on mobileRow `overflowX="auto"`Simple sideways scroll, no chrome
Chat message historyColumn `overflowY="auto"` inside fixed-height shellVertical scroll in pane
Full page listNo wrapper — page scrolls naturallyAvoid nested scroll traps

Scroller

Scroller wraps children in a scrollable flex container with optional prev/next IconButtons and Fade overlays at the edges.
Prop
Default
Notes
`direction``"row"``"column"` for vertical rails
`fadeColor``"transparent"`Match your background token
`onItemClick`—Optional index callback for keyboard nav
`radius`—Clip children to rounded corners
Use Scroller for filter chip rows that overflow on mobile — the arrow buttons hint that more content exists off-screen. Describe chip children in prose: map your filter options to Tag or Button elements inside the Scroller.

Chip filter rail

<Column gap="8" fillWidth>
  <Text variant="label-default-s" onBackground="neutral-weak">
    Categories
  </Text>
  {/* Scroller direction="row" gap="8" fadeColor="page" */}
  {/*   map categories to Tag or Button per chip */}
  {/* /Scroller */}
</Column>
Set fadeColor to match the parent background ("page", "surface", or "overlay") so fades blend seamlessly.

InfiniteScroll

InfiniteScroll renders your items and watches a sentinel element at the bottom. When the sentinel enters the viewport, it calls .
loadMore
Prop
Type
Notes
`items``T[]`Current loaded items
`renderItem``(item, index) => ReactNode`Row renderer per item
`loadMore``() => Promise<boolean>`Return `false` when no more pages
`loading``boolean`External loading flag
`threshold``number`Pixels before sentinel triggers (default 200)
The loadMore function should append to your items array in the parent and return whether more data exists.

Activity feed pattern

Show a Spinner at the bottom while loading is true. Pair with empty states when the first page returns zero items.

Avoiding scroll traps

Nested scroll containers frustrate users. Follow these rules:
  • One primary scroll axis per pane — chat sidebar scrolls independently from message feed, but not two vertical scrollers in the same column
  • Fixed-height shell — set height or maxHeight on the scrollable Column so overflow works
  • Page-level lists — let the document scroll; do not wrap the entire page in InfiniteScroll unless you have a sticky header
For chat layouts, see Chat & messaging — the feed Column gets overflowY="auto" inside a SplitView pane.

Loading and error in feeds

State
Pattern
Initial loadSkeleton rows or centered Spinner
Loading moreSpinner below last item (InfiniteScroll built-in)
No more pagesRemove sentinel — `loadMore` returns `false`
Fetch errorInline Feedback below the list with retry Button
Empty first pageEmpty state Column with icon and CTA

Write this / not this

✅ Once UI way
❌ Common mistake
Scroller for chip rails with fadesHidden overflow with no scroll hint
InfiniteScroll `loadMore` returns booleanInfinite loop when API has no cursor
`fadeColor` matches parent backgroundWhite fades on dark surfaces
Single scroll container per panePage inside scroll inside scroll

Check yourself

  • Scroller fadeColor matches the panel background
  • InfiniteScroll loadMore stops when API is exhausted
  • Feed has empty state for zero results
  • No nested vertical scroll traps in the layout

Related

→ Chat & messaging → Tables & lists → Loading states
<Column
  background="surface"
  border="neutral-alpha-weak"
  radius="l"
  fillWidth
  gap="0"
>
  <Row paddingX="24" paddingY="16" borderBottom="neutral-alpha-weak">
    <Text variant="label-default-m">Recent activity</Text>
  </Row>

  <Column paddingX="24" paddingY="8" fillWidth>
    {/* InfiniteScroll
          items={activities}
          renderItem={(item) => (
            <Row key={item.id} paddingY="12" gap="12" fillWidth>
              <Avatar size="s" src={item.avatar} />
              <Column gap="4" fillWidth>
                <Text variant="body-default-s">{item.message}</Text>
                <Text variant="label-default-xs" onBackground="neutral-weak">
                  {item.time}
                </Text>
              </Column>
            </Row>
          )}
          loadMore={fetchNextPage}
          loading={isLoading}
        */}
  </Column>
</Column>