react-native-collapsible-sticky-header 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +569 -2
  3. package/lib/commonjs/Badge.js +65 -0
  4. package/lib/commonjs/Badge.js.map +1 -0
  5. package/lib/commonjs/CollapsibleStickyHeaderScreen.js +386 -0
  6. package/lib/commonjs/CollapsibleStickyHeaderScreen.js.map +1 -0
  7. package/lib/commonjs/HeaderGreeting.js +122 -0
  8. package/lib/commonjs/HeaderGreeting.js.map +1 -0
  9. package/lib/commonjs/HeaderNavBar.js +219 -0
  10. package/lib/commonjs/HeaderNavBar.js.map +1 -0
  11. package/lib/commonjs/ImageOrNode.js +40 -0
  12. package/lib/commonjs/ImageOrNode.js.map +1 -0
  13. package/lib/commonjs/animations.js +34 -0
  14. package/lib/commonjs/animations.js.map +1 -0
  15. package/lib/commonjs/index.js +122 -0
  16. package/lib/commonjs/index.js.map +1 -0
  17. package/lib/commonjs/optionalGradient.js +24 -0
  18. package/lib/commonjs/optionalGradient.js.map +1 -0
  19. package/lib/commonjs/package.json +1 -0
  20. package/lib/commonjs/theme.js +93 -0
  21. package/lib/commonjs/theme.js.map +1 -0
  22. package/lib/commonjs/types.js +6 -0
  23. package/lib/commonjs/types.js.map +1 -0
  24. package/lib/commonjs/useStickyHeader.js +143 -0
  25. package/lib/commonjs/useStickyHeader.js.map +1 -0
  26. package/lib/commonjs/utils.js +90 -0
  27. package/lib/commonjs/utils.js.map +1 -0
  28. package/lib/module/Badge.js +61 -0
  29. package/lib/module/Badge.js.map +1 -0
  30. package/lib/module/CollapsibleStickyHeaderScreen.js +379 -0
  31. package/lib/module/CollapsibleStickyHeaderScreen.js.map +1 -0
  32. package/lib/module/HeaderGreeting.js +118 -0
  33. package/lib/module/HeaderGreeting.js.map +1 -0
  34. package/lib/module/HeaderNavBar.js +215 -0
  35. package/lib/module/HeaderNavBar.js.map +1 -0
  36. package/lib/module/ImageOrNode.js +36 -0
  37. package/lib/module/ImageOrNode.js.map +1 -0
  38. package/lib/module/animations.js +28 -0
  39. package/lib/module/animations.js.map +1 -0
  40. package/lib/module/index.js +11 -0
  41. package/lib/module/index.js.map +1 -0
  42. package/lib/module/optionalGradient.js +20 -0
  43. package/lib/module/optionalGradient.js.map +1 -0
  44. package/lib/module/package.json +1 -0
  45. package/lib/module/theme.js +88 -0
  46. package/lib/module/theme.js.map +1 -0
  47. package/lib/module/types.js +4 -0
  48. package/lib/module/types.js.map +1 -0
  49. package/lib/module/useStickyHeader.js +138 -0
  50. package/lib/module/useStickyHeader.js.map +1 -0
  51. package/lib/module/utils.js +78 -0
  52. package/lib/module/utils.js.map +1 -0
  53. package/lib/typescript/commonjs/package.json +1 -0
  54. package/lib/typescript/commonjs/src/Badge.d.ts +20 -0
  55. package/lib/typescript/commonjs/src/Badge.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/src/CollapsibleStickyHeaderScreen.d.ts +10 -0
  57. package/lib/typescript/commonjs/src/CollapsibleStickyHeaderScreen.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/src/HeaderGreeting.d.ts +33 -0
  59. package/lib/typescript/commonjs/src/HeaderGreeting.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/src/HeaderNavBar.d.ts +48 -0
  61. package/lib/typescript/commonjs/src/HeaderNavBar.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/src/ImageOrNode.d.ts +16 -0
  63. package/lib/typescript/commonjs/src/ImageOrNode.d.ts.map +1 -0
  64. package/lib/typescript/commonjs/src/animations.d.ts +9 -0
  65. package/lib/typescript/commonjs/src/animations.d.ts.map +1 -0
  66. package/lib/typescript/commonjs/src/index.d.ts +10 -0
  67. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  68. package/lib/typescript/commonjs/src/optionalGradient.d.ts +13 -0
  69. package/lib/typescript/commonjs/src/optionalGradient.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/src/theme.d.ts +81 -0
  71. package/lib/typescript/commonjs/src/theme.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/src/types.d.ts +244 -0
  73. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/src/useStickyHeader.d.ts +12 -0
  75. package/lib/typescript/commonjs/src/useStickyHeader.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/src/utils.d.ts +30 -0
  77. package/lib/typescript/commonjs/src/utils.d.ts.map +1 -0
  78. package/lib/typescript/module/package.json +1 -0
  79. package/lib/typescript/module/src/Badge.d.ts +20 -0
  80. package/lib/typescript/module/src/Badge.d.ts.map +1 -0
  81. package/lib/typescript/module/src/CollapsibleStickyHeaderScreen.d.ts +10 -0
  82. package/lib/typescript/module/src/CollapsibleStickyHeaderScreen.d.ts.map +1 -0
  83. package/lib/typescript/module/src/HeaderGreeting.d.ts +33 -0
  84. package/lib/typescript/module/src/HeaderGreeting.d.ts.map +1 -0
  85. package/lib/typescript/module/src/HeaderNavBar.d.ts +48 -0
  86. package/lib/typescript/module/src/HeaderNavBar.d.ts.map +1 -0
  87. package/lib/typescript/module/src/ImageOrNode.d.ts +16 -0
  88. package/lib/typescript/module/src/ImageOrNode.d.ts.map +1 -0
  89. package/lib/typescript/module/src/animations.d.ts +9 -0
  90. package/lib/typescript/module/src/animations.d.ts.map +1 -0
  91. package/lib/typescript/module/src/index.d.ts +10 -0
  92. package/lib/typescript/module/src/index.d.ts.map +1 -0
  93. package/lib/typescript/module/src/optionalGradient.d.ts +13 -0
  94. package/lib/typescript/module/src/optionalGradient.d.ts.map +1 -0
  95. package/lib/typescript/module/src/theme.d.ts +81 -0
  96. package/lib/typescript/module/src/theme.d.ts.map +1 -0
  97. package/lib/typescript/module/src/types.d.ts +244 -0
  98. package/lib/typescript/module/src/types.d.ts.map +1 -0
  99. package/lib/typescript/module/src/useStickyHeader.d.ts +12 -0
  100. package/lib/typescript/module/src/useStickyHeader.d.ts.map +1 -0
  101. package/lib/typescript/module/src/utils.d.ts +30 -0
  102. package/lib/typescript/module/src/utils.d.ts.map +1 -0
  103. package/package.json +142 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Santhosh James
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,570 @@
1
- # Temporary Holding Version
1
+ # react-native-collapsible-sticky-header
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/react-native-collapsible-sticky-header.svg)](https://www.npmjs.com/package/react-native-collapsible-sticky-header)
4
+ [![CI](https://github.com/santhoshjamesc/react-native-collapsible-sticky-header/actions/workflows/ci.yml/badge.svg)](https://github.com/santhoshjamesc/react-native-collapsible-sticky-header/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/react-native-collapsible-sticky-header.svg)](LICENSE)
6
+ ![platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android-blue)
7
+
8
+ A fully configurable collapsing hero header with a sticky compact bar for **React Native (iOS & Android)**. Works in Expo and bare React Native.
9
+
10
+ - Hero image, plain color, or nothing at all, with an optional fade and rounded bottom corners
11
+ - Built-in layout: logo, greeting, title, subtitle, avatar and up to **3** icon actions with badges
12
+ - Swap in your own expanded nav, collapsible block or sticky bar with render props
13
+ - Scroll handling runs on the UI thread through Reanimated. React re-renders only when the sticky state changes
14
+ - `FlatList`, `ScrollView`, `SectionList`, or any Animated scrollable (such as FlashList)
15
+ - Accessible: the hidden sticky bar is skipped by screen readers, and reduced motion is respected
16
+ - Fully typed and themeable, with no bundled icons, images or brand colors
17
+
18
+ <p align="center">
19
+ <img src="docs/demo.gif" alt="Demo: the hero header collapses and the sticky bar fades in" width="320" />
20
+ <br />
21
+ <em>The example app: hero header expanded, with the sticky bar ready to fade in on scroll.</em>
22
+ </p>
23
+
24
+ ## Contents
25
+
26
+ - [Installation](#installation)
27
+ - [Quick start](#quick-start)
28
+ - [How it works](#how-it-works)
29
+ - [Props](#props)
30
+ - [Actions (icon buttons)](#actions-icon-buttons)
31
+ - [Theming](#theming)
32
+ - [Recipes](#recipes) (including [customizing the collapsed header](#customize-the-collapsed-header))
33
+ - [Building blocks](#building-blocks)
34
+ - [Accessibility & performance](#accessibility--performance)
35
+ - [Example app](#example-app)
36
+
37
+ ## Installation
38
+
39
+ ```sh
40
+ npm install react-native-collapsible-sticky-header
41
+ ```
42
+
43
+ ### Expo
44
+
45
+ ```sh
46
+ npx expo install react-native-reanimated react-native-worklets react-native-safe-area-context expo-linear-gradient
47
+ ```
48
+
49
+ `babel-preset-expo` adds the Reanimated / Worklets Babel plugin for you. `expo-linear-gradient` is **optional** and only used for the fade at the bottom of the hero.
50
+
51
+ ### Bare React Native
52
+
53
+ ```sh
54
+ npm install react-native-reanimated react-native-worklets react-native-safe-area-context
55
+ # optional, for the gradient fade:
56
+ npx install-expo-modules@latest && npm install expo-linear-gradient
57
+ cd ios && pod install
58
+ ```
59
+
60
+ Add the Babel plugin. It **must be listed last**:
61
+
62
+ ```js
63
+ // babel.config.js
64
+ module.exports = {
65
+ presets: ['module:@react-native/babel-preset'],
66
+ plugins: [
67
+ // Reanimated 4.x:
68
+ 'react-native-worklets/plugin',
69
+ // Reanimated 3.x uses 'react-native-reanimated/plugin' instead
70
+ ],
71
+ };
72
+ ```
73
+
74
+ Then wrap your app in `SafeAreaProvider`:
75
+
76
+ ```tsx
77
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
78
+
79
+ export default function App() {
80
+ return <SafeAreaProvider>{/* ... */}</SafeAreaProvider>;
81
+ }
82
+ ```
83
+
84
+ | Peer dependency | Version |
85
+ | -------------------------------- | --------------------------------------------------- |
86
+ | `react` | ≥ 18 |
87
+ | `react-native` | ≥ 0.76 |
88
+ | `react-native-reanimated` | ≥ 3 (tested on 4.x; 4.x needs the New Architecture) |
89
+ | `react-native-safe-area-context` | ≥ 4 |
90
+ | `expo-linear-gradient` | optional |
91
+
92
+ > **No gradient library?** Pass `renderBackgroundFade` to draw the fade with any gradient component, such as `react-native-linear-gradient`. With neither installed, no fade is drawn. The package loads `expo-linear-gradient` through a `require` inside `try/catch`, which Metro treats as an optional dependency.
93
+
94
+ ## Quick start
95
+
96
+ ```tsx
97
+ import Ionicons from '@expo/vector-icons/Ionicons';
98
+ import { CollapsibleStickyHeaderScreen } from 'react-native-collapsible-sticky-header';
99
+
100
+ export function HomeScreen() {
101
+ return (
102
+ <CollapsibleStickyHeaderScreen
103
+ backgroundImage={require('./hero.jpg')}
104
+ logo={require('./logo.png')}
105
+ greeting="Welcome back"
106
+ title="Ava Thompson"
107
+ actions={[
108
+ {
109
+ key: 'cart',
110
+ accessibilityLabel: 'Cart',
111
+ badgeCount: 3,
112
+ icon: ({ color, size }) => (
113
+ <Ionicons name="bag-outline" color={color} size={size} />
114
+ ),
115
+ onPress: openCart,
116
+ },
117
+ ]}
118
+ scrollProps={{
119
+ data: products,
120
+ keyExtractor: (p) => p.id,
121
+ renderItem: ({ item }) => <ProductRow product={item} />,
122
+ }}
123
+ />
124
+ );
125
+ }
126
+ ```
127
+
128
+ ## How it works
129
+
130
+ ```
131
+ ┌──────────────────────────────┐ ← sticky compact bar (absolute, zIndex 20)
132
+ │ [logo] (🔔) (👜³) │ opacity 0→1 over [threshold-40, threshold]
133
+ └──────────────────────────────┘ touchable once y ≥ threshold-20
134
+ ┌──────────────────────────────┐ ← list header
135
+ │ hero image / color / custom │
136
+ │ [LOGO] (🔔) (👜³) │ ← expanded nav bar, measured with onLayout
137
+ │ Welcome back │ ← collapsible block: opacity 1→0,
138
+ │ Ava Thompson (👤) │ translateY 0→-12 over [0, threshold]
139
+ │░░░░░░░ fade ░░░░░░░░░░░░░░░░░│ ← background overhangs by 60px and fades out
140
+ └──────────────────────────────┘
141
+ list items…
142
+ ```
143
+
144
+ `threshold = safeAreaTop + navBarHeight + thresholdOffset`. Until the nav bar has been measured, the threshold is `0` and the sticky bar stays hidden.
145
+
146
+ ## Props
147
+
148
+ All props are optional except `scrollProps` for list types.
149
+
150
+ ### Scroll container
151
+
152
+ | Prop | Type | Default | Description |
153
+ | ----------------- | ---------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
154
+ | `as` | `'FlatList' \| 'ScrollView' \| 'SectionList' \| Component` | `'FlatList'` | Scroll container. A custom component must be Animated (`Animated.createAnimatedComponent`). |
155
+ | `scrollProps` | props of the container | | Passed through. `onScroll` is merged (it runs on the JS thread). `refreshControl` and `contentContainerStyle` work as usual. `scrollEventThrottle` defaults to `16`. |
156
+ | `listRef` | `Ref` | | Forwarded to the scrollable, for example to call `scrollToOffset`. |
157
+ | `headerPlacement` | `'ListHeaderComponent' \| 'children'` | `'ListHeaderComponent'` | Custom scrollables only. |
158
+
159
+ ### Background
160
+
161
+ | Prop | Type | Default | Description |
162
+ | ---------------------- | ------------------------------------------ | ------------------------------------------------ | ---------------------------------------------------------------------------- |
163
+ | `backgroundImage` | `ImageSourcePropType` | | Optional hero image. |
164
+ | `backgroundColor` | `string` | `theme.colors.headerBg` (`'transparent'`) | Shown with no image, or behind it. |
165
+ | `backgroundStyle` | `ViewStyle` | | Background container. |
166
+ | `backgroundImageStyle` | `ImageStyle` | | The image (`opacity`, `resizeMode`…). |
167
+ | `renderBackground` | `(ctx) => ReactNode` | | Replaces the image entirely. Receives `{ theme, insets, collapseProgress }`. |
168
+ | `backgroundOverhang` | `number` | `60` (`0` when rounded) | How far the background extends below the header. |
169
+ | `headerBottomRadius` | `number` | `theme.radius.headerBottom` (`0`) | Rounds the bottom corners and clips the header. |
170
+ | `showFade` | `boolean` | `true`, or `false` when `headerBottomRadius > 0` | Gradient fade at the bottom of the background. |
171
+ | `fadeColors` | `[string, string]` | `['transparent', '#ffffff']` | |
172
+ | `fadeHeight` | `number` | `60` | |
173
+ | `renderBackgroundFade` | `({ colors, height, style }) => ReactNode` | | Draws the fade with any gradient component. |
174
+
175
+ ### Header content
176
+
177
+ | Prop | Type | Default | Description |
178
+ | ------------------------------------------------ | ------------------------------------------------ | -------------------------------- | ------------------------------------------------------------------------------ |
179
+ | `logo` | `ImageSourcePropType \| ReactNode` | | Expanded-bar logo. |
180
+ | `stickyLogo` | `ImageSourcePropType \| ReactNode \| false` | `logo` | Compact-bar logo. `false` hides it in the sticky bar only. |
181
+ | `logoSize` | `{ expanded?, compact? }` | `100×75` / `64×48` | |
182
+ | `logoStyle` | `ImageStyle \| ViewStyle` | | |
183
+ | `logoAccessibilityLabel` | `string` | | |
184
+ | `onLogoPress` | `(variant) => void` | | Makes the logo a button, for example to scroll to the top from the sticky bar. |
185
+ | `greeting` / `title` / `subtitle` | `ReactNode` | | Strings are rendered as styled `Text`. |
186
+ | `greetingStyle` / `titleStyle` / `subtitleStyle` | `TextStyle` | | |
187
+ | `avatar` | `ImageSourcePropType \| ReactNode` | | Profile picture or any element. |
188
+ | `avatarPosition` | `'left' \| 'right'` | `'right'` | |
189
+ | `avatarSize` | `number` | `theme.avatarSize` (`44`) | |
190
+ | `avatarStyle` | `ImageStyle \| ViewStyle` | | |
191
+ | `onAvatarPress` | `() => void` | | Makes the avatar a button. |
192
+ | `avatarAccessibilityLabel` | `string` | `'Profile'` | |
193
+ | `stickyTitle` | `ReactNode` | | Text in the compact bar, next to or instead of the logo. |
194
+ | `stickyTitleStyle` | `TextStyle` | | |
195
+ | `stickyContent` | `('logo' \| 'title' \| 'avatar' \| 'actions')[]` | `['logo', 'title', 'actions']` | What the sticky (collapsed) bar shows. Leading items follow this order. |
196
+ | `stickyAvatarSize` | `number` | `theme.avatarSizeCompact` (`32`) | Avatar size in the sticky bar (when `stickyContent` includes `'avatar'`). |
197
+ | `stickyAvatarStyle` | `ImageStyle \| ViewStyle` | | |
198
+ | `actions` | `HeaderAction[]` | `[]` | Icon buttons, **max 3**. See [Actions](#actions-icon-buttons). |
199
+
200
+ ### Slots
201
+
202
+ Each render prop receives `{ progress, isSticky, variant, theme, insets, actions }`. `progress` is a `SharedValue<number>` from 0 to 1. It holds collapse progress for the expanded slots and sticky-reveal progress for the sticky slot.
203
+
204
+ | Prop | Default |
205
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
206
+ | `renderExpandedNav` | `<HeaderNavBar variant="overlay" logo actions />`. It is measured and defines the threshold. |
207
+ | `renderCollapsible` | `<HeaderGreeting greeting title subtitle avatar />` |
208
+ | `renderStickyNav` | `<HeaderNavBar variant="solid" logo={stickyLogo} title={stickyTitle} show={stickyContent} actions />` |
209
+ | `renderStickyBackground` | None. Draws a gradient, image or blur behind the sticky bar's content. The bar's color becomes transparent unless `stickyBarBackground` is set. |
210
+ | `children` | Extra content **inside** the header, on the background. |
211
+ | `ListHeaderExtra` | Extra content **after** the header, on the page background. |
212
+
213
+ ### Behavior
214
+
215
+ | Prop | Type | Default | Description |
216
+ | --------------------- | ------------------------------------------ | ------------------------------------- | --------------------------------------------------------------------------------- |
217
+ | `thresholdOffset` | `number` | `0` | Added to the measured threshold. |
218
+ | `stickyFadeDistance` | `number` | `40` | Distance over which the sticky bar fades in. |
219
+ | `stickyPointerOffset` | `number` | `20` | The sticky bar becomes touchable at `threshold - offset`. |
220
+ | `collapseTranslateY` | `number` | `-12` | |
221
+ | `collapseAnimation` | `(progress) => style` (worklet) | | Replaces the collapsible block's animation. |
222
+ | `stickyAnimation` | `(progress) => style` (worklet) | fade | Replaces the sticky bar's animation. |
223
+ | `respectSafeArea` | `boolean` | `true` | Pads the header and sticky bar by the top inset and includes it in the threshold. |
224
+ | `enteringAnimations` | `boolean \| { duration, stagger, easing }` | `650 / 70 / Easing.out(Easing.cubic)` | Mount animation (`FadeInUp`). Pass `false` to disable it. |
225
+ | `onStickyChange` | `(isSticky) => void` | | Fires once per transition. |
226
+
227
+ ### Styling
228
+
229
+ | Prop | Type | Default |
230
+ | --------------------- | -------------------------------- | ------------------------------------------------------------ |
231
+ | `theme` | `DeepPartial<StickyHeaderTheme>` | see [Theming](#theming) |
232
+ | `stickyBarStyle` | `ViewStyle` | |
233
+ | `stickyBarBackground` | `string` | `theme.colors.stickyBg` (`'#ffffff'`) |
234
+ | `stickyShadow` | `boolean \| ShadowStyle` | `true` (opacity 0.08, radius 6, offset `{0,2}`, elevation 4) |
235
+ | `containerStyle` | `ViewStyle` | |
236
+ | `headerStyle` | `ViewStyle` | |
237
+
238
+ ## Actions (icon buttons)
239
+
240
+ ```ts
241
+ type HeaderAction = {
242
+ key: string;
243
+ icon:
244
+ | ReactNode
245
+ | ((p: {
246
+ color: string;
247
+ size: number;
248
+ variant: 'overlay' | 'solid';
249
+ }) => ReactNode);
250
+ onPress: () => void;
251
+ accessibilityLabel: string; // required
252
+ accessibilityHint?: string;
253
+ badgeCount?: number;
254
+ color?: string; // per-icon override of the theme color
255
+ size?: number; // per-icon override of theme.iconSize
256
+ style?: StyleProp<ViewStyle>; // the 36×36 round hit area
257
+ hidden?: boolean;
258
+ showInSticky?: boolean; // default true
259
+ testID?: string;
260
+ };
261
+ ```
262
+
263
+ - **0 to 3 actions** are valid (`MAX_HEADER_ACTIONS = 3`). Only the first 3 visible actions render. Passing more logs a `__DEV__` warning.
264
+ - No icon library is bundled. Use `@expo/vector-icons`, `react-native-vector-icons`, SVG, or an `Image`.
265
+ - The function form receives the variant's color and size, so one icon definition works in both bars.
266
+ - A badge shows `9+` when the count is above 9.
267
+
268
+ ## Theming
269
+
270
+ Pass a partial theme. It is deep-merged over the defaults, and props take priority over the theme.
271
+
272
+ ```ts
273
+ const theme: StickyHeaderThemeOverrides = {
274
+ colors: {
275
+ stickyBg: '#ffffff', // compact bar
276
+ headerBg: 'transparent', // header background when there is no image
277
+ iconOverlay: '#ffffff', // icons on the hero
278
+ iconSolid: '#1f2937', // icons on the compact bar
279
+ actionBgOverlay: 'transparent',
280
+ actionBgSolid: 'transparent',
281
+ badge: '#ef4444',
282
+ badgeText: '#ffffff',
283
+ badgeBorder: '#ffffff',
284
+ greeting: '#ffffff',
285
+ title: '#ffffff',
286
+ subtitle: 'rgba(255,255,255,0.85)',
287
+ stickyTitle: '#111827',
288
+ fade: ['transparent', '#ffffff'],
289
+ shadow: '#000000',
290
+ },
291
+ spacing: {
292
+ horizontal: 16,
293
+ actionGap: 8,
294
+ avatarGap: 12,
295
+ leadingGap: 8, // logo / avatar / title gap in the nav bar
296
+ navBar: {
297
+ overlay: { paddingHorizontal: 16, paddingVertical: 8 },
298
+ solid: { paddingHorizontal: 16, paddingVertical: 4 },
299
+ },
300
+ },
301
+ radius: { headerBottom: 0 },
302
+ pressedOpacity: 0.6,
303
+ iconSize: 22,
304
+ actionSize: 36,
305
+ avatarSize: 44,
306
+ badgeSize: 18,
307
+ logoSize: {
308
+ expanded: { width: 100, height: 75 },
309
+ compact: { width: 64, height: 48 },
310
+ },
311
+ typography: {
312
+ greetingSize: 14,
313
+ titleSize: 24,
314
+ subtitleSize: 14,
315
+ stickyTitleSize: 17,
316
+ },
317
+ };
318
+ ```
319
+
320
+ `createTheme(...partials)` returns a fully resolved theme. Example brand theme (amber and white):
321
+
322
+ ```ts
323
+ const beanBrewTheme = {
324
+ colors: { iconSolid: '#d97706', badge: '#f59e0b', greeting: '#fde7c7' },
325
+ };
326
+ ```
327
+
328
+ ## Recipes
329
+
330
+ ### Customize the collapsed header
331
+
332
+ The collapsed header is the compact bar that appears once the expanded header scrolls away.
333
+
334
+ **Color:**
335
+
336
+ ```tsx
337
+ <CollapsibleStickyHeaderScreen
338
+ stickyBarBackground="#4f46e5" // or theme.colors.stickyBg
339
+ theme={{
340
+ colors: {
341
+ iconSolid: '#ffffff', // icons in the collapsed bar
342
+ actionBgSolid: 'rgba(255,255,255,0.16)',
343
+ stickyTitle: '#ffffff', // stickyTitle text
344
+ },
345
+ }}
346
+ stickyBarStyle={{ borderBottomWidth: 1, borderBottomColor: '#3730a3' }}
347
+ stickyShadow={false}
348
+ …
349
+ />
350
+ ```
351
+
352
+ **Content:** choose the items and their order. Actions always sit on the right.
353
+
354
+ ```tsx
355
+ <CollapsibleStickyHeaderScreen
356
+ avatar={{ uri: user.photoUrl }}
357
+ stickyTitle="Ava's tasks"
358
+ stickyContent={['avatar', 'title', 'actions']} // no logo in the collapsed bar
359
+ stickyAvatarSize={32}
360
+ actions={[
361
+ search,
362
+ { ...settings, showInSticky: false }, // expanded bar only
363
+ ]}
364
+ …
365
+ />
366
+ ```
367
+
368
+ To keep the same layout but drop the logo, use `stickyLogo={false}`. To show a smaller or different mark, pass `stickyLogo={require('./mark.png')}`.
369
+
370
+ **Gradient, image or blur background:**
371
+
372
+ ```tsx
373
+ <CollapsibleStickyHeaderScreen
374
+ renderStickyBackground={() => (
375
+ <LinearGradient colors={['#4f46e5', '#7c3aed']} style={StyleSheet.absoluteFill} />
376
+ )}
377
+ …
378
+ />
379
+ ```
380
+
381
+ **Animated color**, for example transparent → solid while the bar appears:
382
+
383
+ ```tsx
384
+ import { interpolateColor } from 'react-native-reanimated';
385
+
386
+ const colorIn = (p: number) => {
387
+ 'worklet';
388
+ return {
389
+ opacity: p,
390
+ backgroundColor: interpolateColor(p, [0, 1], ['rgba(79,70,229,0)', '#4f46e5']),
391
+ };
392
+ };
393
+
394
+ <CollapsibleStickyHeaderScreen stickyAnimation={colorIn} … />
395
+ ```
396
+
397
+ **Anything else:** `renderStickyNav={({ isSticky, theme }) => <MyBar />}` replaces the bar's content entirely. The bar container, safe-area padding, animation and accessibility handling stay in place.
398
+
399
+ ### "Fade" vs "slide" sticky bar
400
+
401
+ The default sticky bar fades in. To slide it down from the top instead:
402
+
403
+ ```tsx
404
+ import { slideStickyAnimation } from 'react-native-collapsible-sticky-header';
405
+
406
+ <CollapsibleStickyHeaderScreen stickyAnimation={slideStickyAnimation(110)} … />
407
+ ```
408
+
409
+ Or write your own. **It must be a worklet**:
410
+
411
+ ```tsx
412
+ const scaleIn = (p: number) => {
413
+ 'worklet';
414
+ return { opacity: p, transform: [{ scale: 0.95 + p * 0.05 }] };
415
+ };
416
+ ```
417
+
418
+ ### No background image
419
+
420
+ ```tsx
421
+ <CollapsibleStickyHeaderScreen backgroundColor="#0f766e" title="Orders" … />
422
+ // or no background at all (transparent): omit both
423
+ ```
424
+
425
+ ### Rounded header bottom
426
+
427
+ ```tsx
428
+ <CollapsibleStickyHeaderScreen
429
+ backgroundColor="#4f46e5"
430
+ headerBottomRadius={24} // also disables the fade and the overhang by default
431
+ headerStyle={{ paddingBottom: 20 }}
432
+ …
433
+ />
434
+ ```
435
+
436
+ ### Avatar + welcome text
437
+
438
+ ```tsx
439
+ <CollapsibleStickyHeaderScreen
440
+ greeting="Welcome back,"
441
+ title="Ava Thompson"
442
+ subtitle="You have 6 tasks today"
443
+ avatar={{ uri: user.photoUrl }} // or <Initials name="Ava" />
444
+ avatarPosition="left"
445
+ avatarSize={52}
446
+ onAvatarPress={openProfile}
447
+ …
448
+ />
449
+ ```
450
+
451
+ ### Custom icons (up to 3)
452
+
453
+ ```tsx
454
+ actions={[
455
+ { key: 'search', accessibilityLabel: 'Search', onPress: search,
456
+ icon: ({ color, size }) => <SearchIcon color={color} width={size} height={size} /> },
457
+ { key: 'inbox', accessibilityLabel: 'Inbox', badgeCount: unread, onPress: openInbox,
458
+ icon: ({ color, size, variant }) =>
459
+ <Ionicons name={variant === 'overlay' ? 'mail' : 'mail-outline'} color={color} size={size} /> },
460
+ { key: 'settings', accessibilityLabel: 'Settings', onPress: openSettings, showInSticky: false,
461
+ icon: <Image source={require('./gear.png')} style={{ width: 22, height: 22 }} /> },
462
+ ]}
463
+ ```
464
+
465
+ ### Custom collapsible content
466
+
467
+ ```tsx
468
+ import Animated, { useAnimatedStyle } from 'react-native-reanimated';
469
+
470
+ function Stats({ progress }: { progress: SharedValue<number> }) {
471
+ const style = useAnimatedStyle(() => ({ transform: [{ scale: 1 - progress.value * 0.1 }] }));
472
+ return <Animated.View style={style}>{/* … */}</Animated.View>;
473
+ }
474
+
475
+ <CollapsibleStickyHeaderScreen renderCollapsible={({ progress }) => <Stats progress={progress} />} … />
476
+ ```
477
+
478
+ ### Custom scrollable (FlashList)
479
+
480
+ ```tsx
481
+ const AnimatedFlashList = Animated.createAnimatedComponent(FlashList);
482
+
483
+ <CollapsibleStickyHeaderScreen as={AnimatedFlashList} scrollProps={{ data, renderItem, estimatedItemSize: 72 }} … />
484
+ ```
485
+
486
+ ## Building blocks
487
+
488
+ To build your own screen, use these pieces directly:
489
+
490
+ ```tsx
491
+ import {
492
+ useStickyHeader,
493
+ HeaderNavBar,
494
+ HeaderGreeting,
495
+ Badge,
496
+ } from 'react-native-collapsible-sticky-header';
497
+
498
+ function MyScreen() {
499
+ const { scrollHandler, onNavLayout, collapseStyle, stickyStyle, isSticky } =
500
+ useStickyHeader({
501
+ thresholdOffset: 0,
502
+ stickyFadeDistance: 40,
503
+ stickyPointerOffset: 20,
504
+ onStickyChange: (s) => console.log(s),
505
+ });
506
+ return (
507
+ <View style={{ flex: 1 }}>
508
+ <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
509
+ <View onLayout={onNavLayout}>
510
+ <HeaderNavBar variant="overlay" logo={logo} />
511
+ </View>
512
+ <Animated.View style={collapseStyle}>
513
+ <HeaderGreeting title="Hi" />
514
+ </Animated.View>
515
+ {/* … */}
516
+ </Animated.ScrollView>
517
+ <Animated.View
518
+ pointerEvents={isSticky ? 'auto' : 'none'}
519
+ style={[styles.bar, stickyStyle]}
520
+ >
521
+ <HeaderNavBar variant="solid" logo={logo} />
522
+ </Animated.View>
523
+ </View>
524
+ );
525
+ }
526
+ ```
527
+
528
+ `useStickyHeader` returns `{ scrollHandler, scrollY, threshold, navBarHeight, collapseProgress, stickyProgress, collapseStyle, stickyStyle, isSticky, onNavLayout, updateScrollY, topInset, reducedMotion }`. `updateScrollY(y)` is a worklet that feeds offsets from a scrollable you drive yourself.
529
+
530
+ Also exported: `Badge` (`count`, `max = 9`, `color`, `borderColor`, `textColor`, `size`, `showZero`), `createTheme`, `defaultTheme`, `MAX_HEADER_ACTIONS`, the pure worklets `computeThreshold`, `getCollapseProgress`, `getStickyProgress` and `isStickyAt`, and the animation presets `fadeStickyAnimation` and `slideStickyAnimation`.
531
+
532
+ ## Accessibility & performance
533
+
534
+ - Every action is a `Pressable` with `accessibilityRole="button"` and a **required** `accessibilityLabel`. A badge count is announced through `accessibilityValue`.
535
+ - While hidden, the sticky bar has `pointerEvents="none"`, `importantForAccessibility="no-hide-descendants"` (Android) and `accessibilityElementsHidden` (iOS).
536
+ - When reduced motion is enabled (`useReducedMotion()`), entering animations are skipped, the sticky bar appears instantly and the collapsible block only fades.
537
+ - Scroll is handled by `useAnimatedScrollHandler` on the UI thread. The component crosses to JS (`runOnJS`) only when the sticky state flips, so scrolling causes **no React re-renders**.
538
+ - Slots are memoized. If you pass inline `theme` objects or `actions` arrays, wrap them in `useMemo` to avoid needless header re-renders when the parent re-renders.
539
+ - Passing `scrollProps.onScroll` makes every scroll frame call into JS. Prefer `onStickyChange` or `useStickyHeader().scrollY`.
540
+
541
+ ## Example app
542
+
543
+ `example/` is an Expo app with three screens:
544
+
545
+ 1. **Bean Brew**: hero image, greeting, logo, bell and bag icons with a cart badge, and pull-to-refresh. Tap the compact logo to scroll to the top.
546
+ 2. **Slide**: a `ScrollView` with a slide-down sticky bar.
547
+ 3. **Rounded**: no image, a solid color, `headerBottomRadius={24}`, an avatar with welcome text, and 3 custom actions.
548
+
549
+ ```sh
550
+ npm install
551
+ cd example && npm install
552
+ npx expo start # press i for iOS, a for Android
553
+ # or a native build:
554
+ npx expo run:ios
555
+ npx expo run:android
556
+ ```
557
+
558
+ The example resolves the library from `../src` through `metro.config.js`, so edits show up immediately.
559
+
560
+ ## Contributing
561
+
562
+ Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
563
+
564
+ ## Machine-readable summary
565
+
566
+ AI assistants and tools can read [`llms.txt`](llms.txt) for a compact API summary.
567
+
568
+ ## License
569
+
570
+ MIT
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.Badge = void 0;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _theme = require("./theme.js");
10
+ var _utils = require("./utils.js");
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ const Badge = exports.Badge = /*#__PURE__*/(0, _react.memo)(function Badge({
13
+ count,
14
+ max = _utils.DEFAULT_BADGE_MAX,
15
+ color = _theme.defaultTheme.colors.badge,
16
+ textColor = _theme.defaultTheme.colors.badgeText,
17
+ borderColor = _theme.defaultTheme.colors.badgeBorder,
18
+ borderWidth = 1.5,
19
+ size = _theme.defaultTheme.badgeSize,
20
+ showZero = false,
21
+ style,
22
+ textStyle,
23
+ testID = 'sticky-header-badge'
24
+ }) {
25
+ const whole = Math.floor(count);
26
+ if (!Number.isFinite(whole) || whole < 0 || whole === 0 && !showZero) {
27
+ return null;
28
+ }
29
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
30
+ testID: testID,
31
+ pointerEvents: "none",
32
+ importantForAccessibility: "no-hide-descendants",
33
+ accessibilityElementsHidden: true,
34
+ style: [styles.badge, {
35
+ backgroundColor: color,
36
+ borderColor,
37
+ borderWidth,
38
+ minWidth: size,
39
+ height: size,
40
+ borderRadius: size / 2
41
+ }, style],
42
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
43
+ numberOfLines: 1,
44
+ allowFontScaling: false,
45
+ style: [styles.text, {
46
+ color: textColor,
47
+ fontSize: Math.round(size * 0.55)
48
+ }, textStyle],
49
+ children: (0, _utils.formatBadgeCount)(whole, max)
50
+ })
51
+ });
52
+ });
53
+ const styles = _reactNative.StyleSheet.create({
54
+ badge: {
55
+ alignItems: 'center',
56
+ justifyContent: 'center',
57
+ paddingHorizontal: 4
58
+ },
59
+ text: {
60
+ fontWeight: '700',
61
+ includeFontPadding: false,
62
+ textAlign: 'center'
63
+ }
64
+ });
65
+ //# sourceMappingURL=Badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","require","_reactNative","_theme","_utils","_jsxRuntime","Badge","exports","memo","count","max","DEFAULT_BADGE_MAX","color","defaultTheme","colors","badge","textColor","badgeText","borderColor","badgeBorder","borderWidth","size","badgeSize","showZero","style","textStyle","testID","whole","Math","floor","Number","isFinite","jsx","View","pointerEvents","importantForAccessibility","accessibilityElementsHidden","styles","backgroundColor","minWidth","height","borderRadius","children","Text","numberOfLines","allowFontScaling","text","fontSize","round","formatBadgeCount","StyleSheet","create","alignItems","justifyContent","paddingHorizontal","fontWeight","includeFontPadding","textAlign"],"sourceRoot":"../../src","sources":["Badge.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AAQA,IAAAE,MAAA,GAAAF,OAAA;AACA,IAAAG,MAAA,GAAAH,OAAA;AAA8D,IAAAI,WAAA,GAAAJ,OAAA;AAoBvD,MAAMK,KAAK,GAAAC,OAAA,CAAAD,KAAA,gBAAG,IAAAE,WAAI,EAAC,SAASF,KAAKA,CAAC;EACvCG,KAAK;EACLC,GAAG,GAAGC,wBAAiB;EACvBC,KAAK,GAAGC,mBAAY,CAACC,MAAM,CAACC,KAAK;EACjCC,SAAS,GAAGH,mBAAY,CAACC,MAAM,CAACG,SAAS;EACzCC,WAAW,GAAGL,mBAAY,CAACC,MAAM,CAACK,WAAW;EAC7CC,WAAW,GAAG,GAAG;EACjBC,IAAI,GAAGR,mBAAY,CAACS,SAAS;EAC7BC,QAAQ,GAAG,KAAK;EAChBC,KAAK;EACLC,SAAS;EACTC,MAAM,GAAG;AACC,CAAC,EAAE;EACb,MAAMC,KAAK,GAAGC,IAAI,CAACC,KAAK,CAACpB,KAAK,CAAC;EAC/B,IAAI,CAACqB,MAAM,CAACC,QAAQ,CAACJ,KAAK,CAAC,IAAIA,KAAK,GAAG,CAAC,IAAKA,KAAK,KAAK,CAAC,IAAI,CAACJ,QAAS,EAAE;IACtE,OAAO,IAAI;EACb;EACA,oBACE,IAAAlB,WAAA,CAAA2B,GAAA,EAAC9B,YAAA,CAAA+B,IAAI;IACHP,MAAM,EAAEA,MAAO;IACfQ,aAAa,EAAC,MAAM;IACpBC,yBAAyB,EAAC,qBAAqB;IAC/CC,2BAA2B;IAC3BZ,KAAK,EAAE,CACLa,MAAM,CAACtB,KAAK,EACZ;MACEuB,eAAe,EAAE1B,KAAK;MACtBM,WAAW;MACXE,WAAW;MACXmB,QAAQ,EAAElB,IAAI;MACdmB,MAAM,EAAEnB,IAAI;MACZoB,YAAY,EAAEpB,IAAI,GAAG;IACvB,CAAC,EACDG,KAAK,CACL;IAAAkB,QAAA,eAEF,IAAArC,WAAA,CAAA2B,GAAA,EAAC9B,YAAA,CAAAyC,IAAI;MACHC,aAAa,EAAE,CAAE;MACjBC,gBAAgB,EAAE,KAAM;MACxBrB,KAAK,EAAE,CACLa,MAAM,CAACS,IAAI,EACX;QAAElC,KAAK,EAAEI,SAAS;QAAE+B,QAAQ,EAAEnB,IAAI,CAACoB,KAAK,CAAC3B,IAAI,GAAG,IAAI;MAAE,CAAC,EACvDI,SAAS,CACT;MAAAiB,QAAA,EAED,IAAAO,uBAAgB,EAACtB,KAAK,EAAEjB,GAAG;IAAC,CACzB;EAAC,CACH,CAAC;AAEX,CAAC,CAAC;AAEF,MAAM2B,MAAM,GAAGa,uBAAU,CAACC,MAAM,CAAC;EAC/BpC,KAAK,EAAE;IACLqC,UAAU,EAAE,QAAQ;IACpBC,cAAc,EAAE,QAAQ;IACxBC,iBAAiB,EAAE;EACrB,CAAC;EACDR,IAAI,EAAE;IACJS,UAAU,EAAE,KAAK;IACjBC,kBAAkB,EAAE,KAAK;IACzBC,SAAS,EAAE;EACb;AACF,CAAC,CAAC","ignoreList":[]}