Skip to content

Components

ScrollNav

The category-pill row from every mobile storefront and photo feed: a single horizontal line of pills that scrolls sideways with no visible scrollbar, while the selected pill stays highlighted.

Filter pills that scroll

ScrollNav is a flex row with overflow-x-auto that never wraps — extra pills simply scroll off the edge, which is exactly what a thumb expects on a phone. Each ScrollNavItem is a real button with hover, focus-ring, and active styling; the active pill inverts to the foreground color.

Open fullscreen
9:41
app.tsx
import { AppShell, Content, Header, ScrollNav, ScrollNavItem } from "appshell-react";
import { useState } from "react";

const filters = ["All", "Landscape", "Portrait", "Street", "Film", "Aerial"];

export default function App() {
  const [filter, setFilter] = useState("All");

  return (
    <AppShell safeArea>
      <Header
        behavior="fixed"
        logo={<span className="font-bold">Lens</span>}
        searchContent={
          <ScrollNav className="px-4 pb-3">
            {filters.map((f) => (
              <ScrollNavItem
                key={f}
                label={f}
                active={filter === f}
                onClick={() => setFilter(f)}
              />
            ))}
          </ScrollNav>
        }
      />
      <Content className="pb-16">
        <div className="p-4">{/* grid filtered by `filter` */}</div>
      </Content>
    </AppShell>
  );
}
The scrollbar is hidden with the scrollbar-hide utility, which is not a Tailwind built-in — it comes from the @utility scrollbar-hide block in the Installation stylesheet. Skip that block and the pill row grows a scrollbar.

In the Header — or anywhere

The natural home is the Header’s searchContent slot, where the pills become the header’s search row and take part in reveal behaviors like reveal-search — filters slide away on scroll down and return on scroll up. But nothing ties it to the Header: drop a ScrollNav at the top of a list, inside a section, or in a bottom sheet just as well.

Controlled selection

ScrollNav keeps no selection state of its own. You hold the current value (here in useState), mark the matching pill with active, and update the value in each pill’s onClick — the same controlled pattern as FooterItem tabs, so single-select, multi-select, or URL-driven filters are all just state you own.

ScrollNav props

PropTypeDefaultDescription
classNamestring—Extra classes.
children*ReactNode—ScrollNavItems.

ScrollNav itself takes only className and children — use the className for edge padding so the first pill aligns with your content while the row still scrolls edge to edge.

ScrollNavItem props

PropTypeDefaultDescription
label*string—Pill text.
activebooleanfalseHighlight as the selected filter.
onClick() => void—Selection handler.
classNamestring—Extra classes.