useCollapsibleHeader
Pre-alphaThe registry and the CLI are not published yet.Roadmap

useCollapsibleHeader

Turn a list's scroll position into a collapse progress for a large header.

Draft

The API shown here is conceptual and may change.

The hook behind Collapsible header. That page explains how the header follows the scroll position, the stretch, the snap and accessibility. This one covers the API.

Installation

npx axiom add use-collapsible-header
pnpm dlx axiom add use-collapsible-header
yarn dlx axiom add use-collapsible-header
bun x axiom add use-collapsible-header

Installs: react-native-reanimated.

Usage

With AppBar, pass progress to collapseProgress:

Conceptual
import Animated from "react-native-reanimated";
import { useCollapsibleHeader } from "@/hooks/use-collapsible-header";

function Inbox() {
	const header = useCollapsibleHeader({
		expandedHeight: 96,
		collapsedHeight: 44,
	});

	return (
		<View style={{ flex: 1 }}>
			<Animated.FlatList
				data={messages}
				renderItem={renderMessage}
				{...header.scrollProps}
			/>
			<AppBar
				variant="large"
				collapseProgress={header.progress}
				style={styles.overlay}
			>
				<AppBar.Title>Inbox</AppBar.Title>
			</AppBar>
		</View>
	);
}

scrollProps includes contentContainerStyle.paddingTop equal to expandedHeight. If you pass your own contentContainerStyle, merge it with header.contentInset.

A custom header

For a cover image or a profile, read the shared values and build the styles yourself:

Conceptual
function ProfileHeader({ header }: { header: CollapsibleHeader }) {
	const container = useAnimatedStyle(() => ({ height: header.height.value }));

	const coverStyle = useAnimatedStyle(() => ({
		opacity: interpolate(header.progress.value, [0, 0.7], [1, 0], "clamp"),
		transform: [
			{
				scale: interpolate(
					header.offset.value,
					[-80, 0],
					[1.3, 1],
					"clamp",
				),
			},
		],
	}));

	const title = useAnimatedStyle(() => ({
		opacity: interpolate(header.progress.value, [0.6, 1], [0, 1], "clamp"),
	}));

	return (
		<Animated.View style={[styles.header, container]}>
			<Animated.Image
				source={coverImage}
				style={[StyleSheet.absoluteFill, coverStyle]}
			/>
			<Animated.Text style={[styles.compactTitle, title]}>
				Grace Hopper
			</Animated.Text>
		</Animated.View>
	);
}

offset goes below 0 on overscroll, which is what drives the zoom on the cover.

Expanding from code

expand() scrolls the list back to the top, which expands the header. Use it from a button of your own. For the tab press and a scroll-to-top button, useScrollToTop does the same on the same list ref.

Options

OptionTypeDefaultDescription
expandedHeightnumberHeader height at the top of the list, in pt, safe area excluded.
collapsedHeightnumber44Height of the compact bar, in pt.
snapbooleantrueWhen the scroll ends between the two heights, scrolls to the nearest end.
stretchbooleantrueAdds the overscroll to height. Turned off with Reduce Motion.

Return value

KeyTypeDescription
progressSharedValue<number>From 0 (expanded) to 1 (collapsed).
heightSharedValue<number>Current header height, stretch included.
offsetSharedValue<number>Raw scroll offset. Negative on overscroll.
expand() => voidScrolls the list to the top.
contentInset{ paddingTop: number }Padding to apply to the list content.
scrollPropsobjectref, onScroll, scrollEventThrottle and contentContainerStyle, for an Animated ScrollView, FlatList, SectionList or FlashList.