Large Title Collapse Header
An iOS-style large-title header that shrinks into a centered small title as a shared element on scroll, pulls out a search bar with a rubber-band bounce, and sticks section headings one after another below the header.
npx shadcn@latest add https://webberui.com/r/large-title-collapse-header.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<LargeTitleCollapseHeader />
Installation
npx shadcn@latest add https://webberui.com/r/large-title-collapse-header.jsonUsage
The component is itself the scroll container — set a height in className and wrap each section in LargeTitleSection. As you scroll, the large title shrinks into the nav bar's centered small title and the section headings stick one after another below the nav bar:
import {
LargeTitleCollapseHeader,
LargeTitleSection,
} from "@/components/ui/large-title-collapse-header";
<LargeTitleCollapseHeader title="Settings" search className="h-[520px]">
<LargeTitleSection title="Favorites">
<ul>…</ul>
</LargeTitleSection>
<LargeTitleSection title="General">
<ul>…</ul>
</LargeTitleSection>
</LargeTitleCollapseHeader>With search enabled, touch devices can pull down at the top of the page to pull out the search bar with a rubber-band bounce, while non-touch devices use the search button on the right of the nav bar. The search field supports controlled and uncontrolled modes:
const [query, setQuery] = React.useState("");
<LargeTitleCollapseHeader
title="Settings"
search
searchPlaceholder="Search settings"
searchValue={query}
onSearchChange={setQuery}
>
{/* sections filtered by query */}
</LargeTitleCollapseHeader>Props
LargeTitleCollapseHeader
| Prop | Type | Default | Description |
|---|---|---|---|
title | React.ReactNode | — | The header's large title; on scroll it shrinks into the nav bar's centered small title as a shared element |
children | React.ReactNode | — | The content area, usually holding a number of LargeTitleSections |
leading | React.ReactNode | — | Left slot of the nav bar (a back button, say) |
trailing | React.ReactNode | — | Right slot of the nav bar (action buttons, say); with search enabled, the search button comes after it |
search | boolean | false | Enable the pull-down search bar and the nav bar search button |
searchPlaceholder | string | "搜尋" | Placeholder for the search field |
searchValue | string | — | Controlled search string; pair it with onSearchChange |
defaultSearchValue | string | "" | Default search string in uncontrolled mode |
onSearchChange | (value: string) => void | — | Search-string change callback |
navHeight | number | 44 | Nav bar height (px), which is also where section headings stick |
largeTitleHeight | number | 52 | Large title row height (px), which determines the scroll distance of the shrink transition |
className | string | — | Class for the outer scroll container (set the height here) |
contentClassName | string | — | Class for the wrapper around the content area |
LargeTitleSection
| Prop | Type | Default | Description |
|---|---|---|---|
title | React.ReactNode | — | Section heading, which sticks below the nav bar |
children | React.ReactNode | — | Section content |
id | string | — | Anchor id applied to the section heading |
className | string | — | Class attached to the outermost section |
headerClassName | string | — | Class attached to the sticky heading row (to override the background color to match the page, for example) |
How it works
- The shrink is a shared-element-style crossfade: the large title scales down, moves up, and fades out behind the nav bar as you scroll, while the nav bar's centered small title scales up and fades in at the same time — the two hand off into one continuous morph
- Every transition is driven by scroll offset from
useScroll(mapped directly byuseTransform), so it never relies on scroll events to relayout and scroll performance matches native - The nav bar's background color and separator only fade in during the collapse, keeping the header clear in the expanded state
- The search bar carries its expanded height in a
useMotionValue: a touch pull-down computes the rubber-band offset with exponential damping, and on release a threshold decides between expanding and bouncing back; when the rubber band overstretches, the search field stretches with it - Section headings stick below the nav bar with native
position: sticky; top: navHeight; as the next section reaches the sticky line it takes over from the previous one, one after another - The nav bar and section headings use
backdrop-blurtranslucent backgrounds, keeping a sense of depth as content scrolls past
Accessibility
- The large title is a real heading semantically (
<h2>), while the small title in the nav bar is purely visual and markedaria-hiddenso it is not announced twice - Section headings are
<h3>and can take anidto serve as anchors - The search button is a native
<button>marked witharia-expanded; while the search bar is collapsed the whole thing isaria-hiddenand the input hastabIndex={-1}so it cannot be focused from the keyboard, becoming focusable once expanded, withEscapecollapsing it - When the user has "reduce motion" enabled at the system level, the search bar's expand / bounce becomes an instant change (the spring bounce is disabled), while the pull-down gesture and the scroll collapse still work
- Timers, event listeners, and gesture bindings are all cleaned up on unmount
Now Playing Sheet
A persistent mini player bar at the bottom; tap it and the artwork scales up as a shared element while the whole layer springs open into a full-screen now-playing page, dismissed by a downward drag whose gesture velocity decides the outcome.
Ornament Toolbar
A visionOS-style ornament toolbar floating just outside the edge of a content panel: it shrinks to a pill while scrolling and expands to full controls when you stop, with Z-axis lift and a follow-along parallax.