Large title header

Free · MITReact Native · Expo

An 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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/large-title-header.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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`.
booleantrue

LargeTitleHeader

PropTypeDefault
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

PropTypeDefault
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.