Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Full documentation lives here:
- Scroll-driven animated headers
- Shared header state across tabs, pagers, and multiple scrollables
- Navigation-rendered headers in Expo Router or React Navigation
- Custom scrollables via `createHeaderMotionScrollable()`
- Custom scrollables via `createHeaderMotionScrollable()` and `ScrollablePresets`
- Optional header panning

## What it is not
Expand Down
51 changes: 35 additions & 16 deletions docs/docs/api/create-header-motion-scrollable.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Factory function for creating reusable Header Motion-aware wrappers around custo

This is the recommended way to integrate third-party scrollables like FlashList or LegendList.

For common integrations, the library also exports `ScrollablePresets` so you do not need to remember the recommended option combinations yourself.

## Signature

```tsx
Expand All @@ -20,38 +22,55 @@ function createHeaderMotionScrollable(

## Options

| Option | Type | Default | Description |
| ---------------------- | --------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `displayName` | `string` | Auto-generated | Component name shown in React DevTools. |
| `isComponentAnimated` | `boolean` | `false` | Set to `true` when the component is already animated (skips `Animated.createAnimatedComponent`). |
| `contentContainerMode` | `'children' \| 'renderScrollComponent'` | `'renderScrollComponent'` | How Header Motion injects content offsetting. Use `'children'` for ScrollView-like, `'renderScrollComponent'` for FlatList-like. |
| Option | Type | Default | Description |
| ---------------------- | --------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `displayName` | `string` | Auto-generated | Component name shown in React DevTools. |
| `isComponentAnimated` | `boolean` | `false` | Set to `true` when the component is already animated (skips `Animated.createAnimatedComponent`). |
| `contentContainerMode` | `'children' \| 'renderScrollComponent'` | `'renderScrollComponent'` | How Header Motion injects content offsetting. Use `'children'` for ScrollView-like, `'renderScrollComponent'` for FlatList-like. |
| `managedRefTarget` | `'outer' \| 'inner'` | `'outer'` | Which ref Header Motion uses for imperative sync. Only applies in `renderScrollComponent` mode. Use `'inner'` when the wrapped list's outer ref is not the actual scroll view. |

## Examples

### FlashList
### FlashList preset

```tsx
import { FlashList } from '@shopify/flash-list';
import { createHeaderMotionScrollable } from 'react-native-header-motion';
import {
createHeaderMotionScrollable,
ScrollablePresets,
} from 'react-native-header-motion';

const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
displayName: 'HeaderMotionFlashList',
});
const HeaderMotionFlashList = createHeaderMotionScrollable(
FlashList,
ScrollablePresets.FlashList
);
```

### LegendList
### LegendList preset

```tsx
import { AnimatedLegendList } from '@legendapp/list/reanimated';
import { createHeaderMotionScrollable } from 'react-native-header-motion';
import {
createHeaderMotionScrollable,
ScrollablePresets,
} from 'react-native-header-motion';

const HeaderMotionLegendList = createHeaderMotionScrollable(
AnimatedLegendList,
{
displayName: 'HeaderMotionLegendList',
isComponentAnimated: true,
}
ScrollablePresets.AnimatedLegendList
);
```

### Manual options

```tsx
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
displayName: 'HeaderMotionFlashList',
contentContainerMode: 'renderScrollComponent',
managedRefTarget: 'inner',
});
```

Use `managedRefTarget: 'inner'` when the wrapped component exposes a controller ref on the outside but owns the actual native scroll view internally. FlashList and LegendList both need this. React Native `FlatList` does not, so the default `'outer'` behavior remains correct there.

The returned component accepts all the original component's props plus Header Motion-specific props (`scrollId`, `headerOffsetStrategy`, `ensureScrollableContentMinHeight`, `animatedRef`).
48 changes: 39 additions & 9 deletions docs/docs/guides/custom-scrollables.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ title: Custom scrollables

Header Motion ships with `HeaderMotion.ScrollView` and `HeaderMotion.FlatList`, but you can integrate **any** scrollable component via the `createHeaderMotionScrollable()` factory. The resulting component works just like the built-in ones — it accepts all the original props plus Header Motion-specific ones like `scrollId` and `headerOffsetStrategy`.

For popular third-party lists, the library also exports `ScrollablePresets` with the recommended option combinations.

## FlashList

```tsx
import { FlashList } from '@shopify/flash-list';
import { createHeaderMotionScrollable } from 'react-native-header-motion';
import {
createHeaderMotionScrollable,
ScrollablePresets,
} from 'react-native-header-motion';

const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
displayName: 'HeaderMotionFlashList',
});
const HeaderMotionFlashList = createHeaderMotionScrollable(
FlashList,
ScrollablePresets.FlashList
);
```

Use it anywhere you'd use a regular `FlashList`:
Expand All @@ -35,24 +41,48 @@ LegendList exports a pre-animated variant, `AnimatedLegendList`, which is what y

```tsx
import { AnimatedLegendList } from '@legendapp/list/reanimated';
import { createHeaderMotionScrollable } from 'react-native-header-motion';
import {
createHeaderMotionScrollable,
ScrollablePresets,
} from 'react-native-header-motion';

const HeaderMotionLegendList = createHeaderMotionScrollable(
AnimatedLegendList,
{
displayName: 'HeaderMotionLegendList',
isComponentAnimated: true,
}
ScrollablePresets.AnimatedLegendList
);
```

## Presets vs manual options

`ScrollablePresets` is the easiest path when you are integrating supported third-party lists:

```tsx
import { ScrollablePresets } from 'react-native-header-motion';

ScrollablePresets.FlashList;
ScrollablePresets.AnimatedLegendList;
```

If you prefer to configure things manually, these are the equivalent options:

```tsx
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
displayName: 'HeaderMotionFlashList',
contentContainerMode: 'renderScrollComponent',
managedRefTarget: 'inner',
});
```

Use `managedRefTarget: 'inner'` when a third-party list exposes a controller ref on the outside but keeps the real native scroll view one layer deeper. FlashList and LegendList both fall into that category. React Native `FlatList` does not, so it should keep the default `'outer'`.

## Factory options

| Option | Type | Default | Description |
| ---------------------- | --------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `displayName` | `string` | Auto-derived from the component name | Sets the React `displayName` for dev tools. |
| `isComponentAnimated` | `boolean` | `false` | When `true`, the factory skips `Animated.createAnimatedComponent()`. Set this when the component is already animated. |
| `contentContainerMode` | `'children' \| 'renderScrollComponent'` | `'renderScrollComponent'` | `'children'` wraps children in an inner `Animated.View` (ScrollView-like). `'renderScrollComponent'` injects a custom scroll component that wraps the content (FlatList-like). |
| `managedRefTarget` | `'outer' \| 'inner'` | `'outer'` | Controls which ref Header Motion uses for imperative sync. Only applies in `renderScrollComponent` mode. Use `'inner'` for list abstractions like FlashList and LegendList. |

## What's next?

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/guides/default-scrollables.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ You can also pass standard scroll event handlers like `onScroll`, `onScrollBegin

## Custom scrollables

For third-party scrollable components like [FlashList](https://shopify.github.io/flash-list/) or [LegendList](https://github.com/LegendApp/legend-list), use the `createHeaderMotionScrollable()` factory. For more information, check the [Custom scrollables](./custom-scrollables) guide.
For third-party scrollable components like [FlashList](https://shopify.github.io/flash-list/) or [LegendList](https://github.com/LegendApp/legend-list), use the `createHeaderMotionScrollable()` factory. For those two common integrations, you can also start from the exported `ScrollablePresets`. For more information, check the [Custom scrollables](./custom-scrollables) guide.

For even more control, you can manage the scroll integration yourself using `HeaderMotion.ScrollManager` or the `useScrollManager()` hook. See [Using ScrollManager](./using-scroll-manager) for details.

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/other/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Header panning is built on Gesture Handler's pan gesture. Even if you don't use

## Can I use this with FlashList / LegendList / other custom scrollables?

Yes. Use `createHeaderMotionScrollable()` to wrap any scrollable component. See the [Custom scrollables](../guides/custom-scrollables) guide.
Yes. Use `createHeaderMotionScrollable()` to wrap any scrollable component. For FlashList and LegendList, you can also use the built-in `ScrollablePresets` export so the recommended factory options are already filled in. See the [Custom scrollables](../guides/custom-scrollables) guide.

## Does pull to refresh work?

Expand Down
8 changes: 5 additions & 3 deletions example/src/app/flashlist-pager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@/components';
import HeaderMotion, {
createHeaderMotionScrollable,
ScrollablePresets,
useActiveScrollId,
useMotionProgress,
} from 'react-native-header-motion';
Expand All @@ -23,9 +24,10 @@ import Animated, {
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { FlashList } from '@shopify/flash-list';

const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
displayName: 'HeaderMotionFlashList',
});
const HeaderMotionFlashList = createHeaderMotionScrollable(
FlashList,
ScrollablePresets.FlashList
);

const indexToKey = new Map([
[0, 'A'],
Expand Down
8 changes: 5 additions & 3 deletions example/src/app/flashlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ContentCard, ShowcaseCollapsibleHeader } from '@/components';
import { FlashList } from '@shopify/flash-list';
import HeaderMotion, {
createHeaderMotionScrollable,
ScrollablePresets,
} from 'react-native-header-motion';
import { Stack } from 'expo-router';

Expand All @@ -10,9 +11,10 @@ type ListRow = {
label: string;
};

const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
displayName: 'HeaderMotionFlashList',
});
const HeaderMotionFlashList = createHeaderMotionScrollable(
FlashList,
ScrollablePresets.FlashList
);

export default function Screen() {
return (
Expand Down
15 changes: 15 additions & 0 deletions example/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ const SECTIONS: ShowcaseSection[] = [
href: '/scroll-to-button',
icon: '🎯',
},
{
title: 'Scroll To Button (external ref) | FlatList',
href: '/scroll-to-button',
icon: '📋🎯',
},
{
title: 'Scroll To Button (external ref) | FlashList',
href: '/scroll-to-button',
icon: '⚡️🎯',
},
{
title: 'Scroll To Button (external ref) | LegendList',
href: '/scroll-to-button',
icon: '🧾🎯',
},
],
},
{
Expand Down
6 changes: 2 additions & 4 deletions example/src/app/legendlist-pager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@/components';
import HeaderMotion, {
createHeaderMotionScrollable,
ScrollablePresets,
useActiveScrollId,
useMotionProgress,
} from 'react-native-header-motion';
Expand All @@ -25,10 +26,7 @@ import { AnimatedLegendList } from '@legendapp/list/reanimated';

const HeaderMotionLegendList = createHeaderMotionScrollable(
AnimatedLegendList,
{
displayName: 'HeaderMotionLegendList',
isComponentAnimated: true,
}
ScrollablePresets.AnimatedLegendList
);

const indexToKey = new Map([
Expand Down
6 changes: 2 additions & 4 deletions example/src/app/legendlist.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ContentCard, ShowcaseCollapsibleHeader } from '@/components';
import HeaderMotion, {
createHeaderMotionScrollable,
ScrollablePresets,
} from 'react-native-header-motion';
import { Stack } from 'expo-router';

Expand All @@ -13,10 +14,7 @@ type ListRow = {

const HeaderMotionLegendList = createHeaderMotionScrollable(
AnimatedLegendList,
{
displayName: 'HeaderMotionLegendList',
isComponentAnimated: true,
}
ScrollablePresets.AnimatedLegendList
);

export default function Screen() {
Expand Down
Loading
Loading