Skip to content

Conversation

@alexrass
Copy link
Contributor

@alexrass alexrass commented Oct 14, 2025

What problem does this PR solve?

Sticky headers lacked key capabilities needed in real apps:

  • Could not offset headers to account for top bars (e.g., nav bars).
  • No way to render a background/backdrop beneath sticky headers.
  • Duplicate visuals when the original cell remained visible while its header was sticky.
  • No callback to know which header is currently sticky.

This PR adds configuration and APIs to control sticky header behavior and appearance, while keeping defaults backward compatible.

Changes

  • Public API
    • Added stickyHeaderConfig with:
      • offset: top offset where headers stick (default 0).
      • backdropComponent: renders behind sticky headers.
      • useNativeDriver: controls animation driver (default true).
      • hideRelatedCell: hides the cell for the active sticky header (default false).
    • Added onChangeStickyIndex(current, previous) to track sticky header changes.
  • Behavior/UX
    • Sticky header position now supports a top offset (for in-app headers).
    • Optional backdrop is layered above content but behind the sticky header.
    • Optionally hide the related cell to avoid duplicate visuals.
    • Improved push animation; when offset > 0, header fades as it’s pushed.
  • Internals
    • RecyclerView:
      • Passes sticky header config to StickyHeaders.
      • Tracks currentStickyIndex to drive hideRelatedCell.
      • Applies marginTop spacer to content for offset alignment.
      • Uses configured useNativeDriver for scroll animations.
      • Renders backdrop overlay when configured.
    • StickyHeaders:
      • Computes current/next sticky indices respecting offset and content layouts.
      • Adjusts push thresholds by offset; sets absolute top = offset; higher z-index.
      • Emits onChangeStickyIndex on index transitions.
    • ViewHolder / ViewHolderCollection:
      • New hidden prop to toggle opacity when hideRelatedCell is enabled.
      • Memoization updated to include hidden.
    • useSecondaryProps:
      • New renderStickyHeaderBackdrop helper.
  • Documentation and Examples
    • Docs: Added stickyHeaderConfig and onChangeStickyIndex sections with examples.
    • Fixture app: New StickyHeaderExample screen with toggles (enable sticky, offset, background).
    • Navigation updated to include the new example.
  • Tests
    • New src/tests/StickyHeaders.test.tsx covering:
      • Sticky index selection with/without offset.
      • Push animation thresholds (including firstItemOffset).
      • Callback behavior across scrolls and edge cases.
      • Rendering position with offset.
  • Changelog
    • Added entries for sticky header offset and backgrounds.

Test Plan

  • Run unit tests:
    • yarn test
    • Confirm StickyHeaders.test.tsx passes.
  • Manual verification in fixture app:
    • Launch the RN fixture.
    • Open “Sticky Headers”.
    • Toggle:
      • Enable Sticky Headers: headers stick at scrolling.
      • Sticky Header Offset: headers stick below the top (e.g., 44).
      • Sticky Header Background: backdrop renders behind headers.
      • Hide cell (set in code via hideRelatedCell: true): original cell hides when its header is sticky.
  • API validation:
    • Provide stickyHeaderConfig in an app using FlashList; verify default behavior remains unchanged when config is omitted.
    • Add onChangeStickyIndex and log current/previous indices; scroll through list and confirm correct sequence.

@alexrass alexrass changed the title [feat] Add stickyHeaderOffset and StickyHeaderBackgroundComponent [feat] Add stickyHeaderOffset and StickyHeaderBackgroundComponent for sticky headers Oct 14, 2025
@alexrass alexrass force-pushed the arass/flash-list/sticky-header-offsets branch from 2e4a922 to b52a523 Compare October 14, 2025 13:57
@alexrass alexrass marked this pull request as ready for review October 14, 2025 14:30
@alexrass alexrass requested a review from naqvitalha October 14, 2025 14:30
{renderEmpty}
{renderFooter}
</CompatScrollView>
{stickyHeaderIndices ? renderStickyHeaderBackground : null}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it something generic. Atleast the name

{renderHeader}
{!isHorizontalRTL && viewToMeasureBoundedSize}
{/* Spacer for sticky header offset */}
{stickyHeaderIndices && stickyHeaderOffset > 0 && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's handle this using contentContainerStyle padding

@alexrass alexrass force-pushed the arass/flash-list/sticky-header-offsets branch from b52a523 to 1fb193a Compare October 15, 2025 19:29
@alexrass alexrass changed the title [feat] Add stickyHeaderOffset and StickyHeaderBackgroundComponent for sticky headers [feat] Add sticky header offset and backdrop component for FlashList sticky headers Oct 15, 2025
@alexrass alexrass force-pushed the arass/flash-list/sticky-header-offsets branch from 1fb193a to 6adb670 Compare October 15, 2025 19:33
@alexrass alexrass changed the title [feat] Add sticky header offset and backdrop component for FlashList sticky headers [feat] Add stickyHeaderOffset and StickyHeaderBackdropComponent for sticky headers Oct 15, 2025
outputRange: [0, -currentStickyHeight],
extrapolate: "clamp",
}),
opacity: stickyHeaderOffset
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put an explicit check > 0

getY: (index: number) => number
): number {
// Special case: when at the top of the list, show the first sticky header if it's near the top
if (adjustedValue <= 0 && sortedIndices.length > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this a bit more?

style={{
position: "absolute",
inset: 0,
zIndex: 1,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zIndex isn't needed I think

/** Additional data to trigger re-renders */
extraData: FlashListProps<TItem>["extraData"];
/** Sticky header change handler */
onChangeStickyIndex?: (index: number) => void;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a useful prop on FlashList too. onChangeStickyIndex: (current, previous) => void. Let's add it.

@alexrass alexrass force-pushed the arass/flash-list/sticky-header-offsets branch from 6adb670 to 98e5177 Compare October 21, 2025 18:23
@alexrass alexrass changed the title [feat] Add stickyHeaderOffset and StickyHeaderBackdropComponent for sticky headers [feat] Add sticky header config (offset, backdrop, hide cell) and onChangeStickyIndex Oct 21, 2025
}
}

console.log("RESULT", result);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing log statement

{renderEmpty}
{renderFooter}
</CompatScrollView>
{stickyHeaderIndices ? renderStickyHeaderBackdrop : null}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's check length also, in case it's empty

<CompatView
style={{
height: horizontal ? undefined : 0,
height: horizontal ? undefined : 0 + stickyHeaderOffset,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this. It can throw off compute. I think we discussed doing this will padding instead. You can also try applying it as marginTop here.

@alexrass alexrass force-pushed the arass/flash-list/sticky-header-offsets branch 2 times, most recently from 3585f42 to 76355de Compare October 22, 2025 19:29
@alexrass alexrass changed the title [feat] Add sticky header config (offset, backdrop, hide cell) and onChangeStickyIndex [feat] Add stickyHeaderConfig (offset, backdrop, hideRelatedCell) and onChangeStickyIndex Oct 22, 2025
… offset spacer, background layer, and header hiding; update docs/changelog and add StickyHeader example in RN fixture
@alexrass alexrass force-pushed the arass/flash-list/sticky-header-offsets branch from 76355de to 3da7a69 Compare October 22, 2025 20:07
@alexrass alexrass changed the title [feat] Add stickyHeaderConfig (offset, backdrop, hideRelatedCell) and onChangeStickyIndex [feat] Introduce stickyHeaderConfig (offset, backdrop, native driver, hide cell) and onChangeStickyIndex Oct 22, 2025
@alexrass alexrass enabled auto-merge (squash) October 22, 2025 20:09
@alexrass alexrass merged commit 0cf8775 into main Oct 22, 2025
10 checks passed
@alexrass alexrass deleted the arass/flash-list/sticky-header-offsets branch October 22, 2025 20:33
@alexrass alexrass mentioned this pull request Oct 23, 2025
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants