Large title header
Free · MITReact Native · ExpoAn iOS large title that collapses into a compact bar as the content scrolls: the titles cross-fade, the bar fades in, the title stretches when pulled past the top and a half-finished collapse settles on either state.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/large-title-header.jsonThen run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/large-title-header.
Usage
import { Stack, router } from "expo-router";
import { LargeTitleScrollView } from "@/components/goodui-native/large-title-header/large-title-header";
export default function Inbox() {
const [query, setQuery] = useState("");
return (
<>
<Stack.Screen options={{ headerShown: false }} />
<LargeTitleScrollView
title="Inbox"
subtitle="4 unread"
onBack={() => router.back()}
search={{ value: query, onChangeText: setQuery, placeholder: "Search mail" }}
actions={[{ key: "compose", accessibilityLabel: "New message", icon: (p) => <PencilIcon {...p} />, onPress: compose }]}
>
{messages.map((m) => <MessageRow key={m.id} message={m} />)}
</LargeTitleScrollView>
</>
);
}When to use
- Top-level screens with a list: inbox, workouts, accounts, settings.
- Screens that need a search field under the title and one to three trailing actions.
- Detail screens deep in a stack: the navigator's standard header is quieter.
- Screens without scrolling content: a static title is enough.
- Searching as the main task of the screen: use search-bar on its own.
Examples
Your own list (FlatList)
const scrollY = useSharedValue(0);
const distance = useSharedValue(52);
const onScroll = useAnimatedScrollHandler((e) => {
scrollY.value = e.contentOffset.y;
});
const insets = useInsets();
<View style={{ flex: 1 }}>
<Animated.FlatList
data={rows}
onScroll={onScroll}
scrollEventThrottle={16}
contentContainerStyle={{ paddingTop: insets.top + 44 }}
ListHeaderComponent={<LargeTitle title="Accounts" scrollY={scrollY} collapseDistance={distance} />}
renderItem={renderRow}
/>
<LargeTitleHeader title="Accounts" scrollY={scrollY} collapseDistance={distance} />
</View>Share the scroll with a hiding tab bar
const scrollY = useSharedValue(0);
<View style={{ flex: 1 }}>
<LargeTitleScrollView title="Workouts" scrollY={scrollY} contentContainerStyle={{ paddingBottom: 120 }}>
{rows}
</LargeTitleScrollView>
<TabBar variant="floating" absolute scrollY={scrollY} items={tabs} />
</View>API reference
LargeTitleScrollView
| Prop | Type | Default |
|---|---|---|
| testID Used to locate this view in end-to-end tests. > This disables the 'layout-only view removal' optimization for this view! See https://reactnative.dev/docs/view#testid | string | — |
| title* Screen title. Shown large at the top of the content and small in the bar once collapsed. | string | — |
| variant `transparent` shows the background only once the title collapses; `solid` always shows it. | "transparent" | "solid" | "transparent" |
| size `largeTitle` (34 pt) or `title1` (28 pt) for denser screens. | "large" | "medium" | "large" |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<LargeTitleHeaderLabels> | — |
| leading Content at the leading edge of the bar, such as a back button. Overrides `onBack`. | ReactNode | — |
| actions Up to three trailing icon buttons. | LargeTitleAction[] | — |
| onBack Shows a chevron back button calling this. | () => void | — |
| backLabel Text beside the back chevron. Default "Back". Pass "" for the chevron alone. Same as `labels.back`. | string | — |
| subtitle Supporting line under the title, such as "12 unread". | string | — |
| search A search field under the title. | LargeTitleSearch | — |
| children* Screen content under the title. | ReactNode | — |
| scrollY Share the scroll offset, for example with TabBar's hideOnScroll. One is created when omitted. | SharedValue<number> | — |
| snap Settle on fully expanded or fully collapsed when a drag ends in between (native). Default `true`. | boolean | true |
LargeTitleHeader
| Prop | Type | Default |
|---|---|---|
| title* Screen title. Shown large at the top of the content and small in the bar once collapsed. | string | — |
| scrollY* Vertical scroll offset of the content the header sits on. | SharedValue<number> | — |
| size `largeTitle` (34 pt) or `title1` (28 pt) for denser screens. | "large" | "medium" | "large" |
| leading Content at the leading edge of the bar, such as a back button. Overrides `onBack`. | ReactNode | — |
| onBack Shows a chevron back button calling this. | () => void | — |
| backLabel Text beside the back chevron. Default "Back". Pass "" for the chevron alone. Same as `labels.back`. | string | — |
| actions Up to three trailing icon buttons. | LargeTitleAction[] | [] |
| variant `transparent` shows the background only once the title collapses; `solid` always shows it. | "transparent" | "solid" | "transparent" |
| collapseDistance Height of the large title block in points, as measured by LargeTitle. Default 52. | SharedValue<number> | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<LargeTitleHeaderLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
LargeTitle
| Prop | Type | Default |
|---|---|---|
| title* Screen title. Shown large at the top of the content and small in the bar once collapsed. | string | — |
| scrollY* Vertical scroll offset of the content the header sits on. | SharedValue<number> | — |
| size `largeTitle` (34 pt) or `title1` (28 pt) for denser screens. | "large" | "medium" | "large" |
| subtitle Supporting line under the title, such as "12 unread". | string | — |
| search A search field under the title. | LargeTitleSearch | — |
| collapseDistance Receives the measured height of the title block, so the bar knows when it has collapsed. | SharedValue<number> | — |
| onTitleLayout Called with the measured height of the title block. | (height: number) => void | — |
| labels Overrides for the built-in strings (the search field's); a GooduiLocaleProvider pack sets them app-wide. | Partial<LargeTitleHeaderLabels> | — |
Motion and haptics
- Scrolling: the large title fades out between 35 % and 85 % of its height while the compact title fades in and rises 6 pt between 60 % and 100 %.
- The bar background and hairline fade in as the title collapses (the solid variant keeps its background).
- Pulling past the top (iOS bounce) stretches the large title up to 12 % from its leading edge, following the finger.
- When a drag or fling ends with the title half collapsed, the scroll view settles to fully expanded or fully collapsed (native; the web has no drag end events).
- Reduced motion: no stretch, no rise on the compact title, and the settle jumps instead of animating; the cross-fade stays.
- Haptic: tap on the back button and on trailing actions
- Haptic: none while scrolling
- All scroll-driven styles are computed on the UI thread from one shared scroll offset; scrolling never re-renders React.
Accessibility
- The large title has accessibilityRole header; the compact copy in the bar is hidden from screen readers so the title is read once.
- The back button and every trailing action are buttons of at least 44 × 44 pt; actions require an accessibilityLabel.
- The search field is labelled by its placeholder; the clear button is labelled 'Clear search'.
Notes for AI
- Hide the navigator's header for the screen (Stack.Screen options headerShown: false); the component draws its own bar and handles the top inset.
- Use LargeTitleScrollView for ScrollView content. For FlatList or SectionList, put LargeTitle in ListHeaderComponent, render LargeTitleHeader above the list, and share scrollY and collapseDistance.
- Pass at most three actions; each needs key, icon, accessibilityLabel and onPress. The icon receives { color, size }.
- Pass scrollY to share the offset with TabBar hideOnScroll.
Related
A bottom tab bar whose pill stretches toward the next tab and can be dragged to scrub between tabs; the selected icon bounces, badges pop, and it can hide while scrolling.
An iOS-style search field: focusing it shrinks the field while a Cancel button springs in beside it and scope chips cascade in below. Clear button, loading spinner and debounced queries built in.
Two to five views of the same thing with an elastic highlight that stretches toward the new segment, can be dragged, and ticks at every boundary. Includes a billing toggle.