Tech spotlight: Floating footer with React functional components
Tech spotlight: Floating footer with React functional components
copy gray iconlinkedin graytwitter grey

Tech spotlight: Floating footer with React functional components

February 8, 2022

Overview

We recently faced a common front end challenge here at Pinwheel which led to some interesting learnings for advanced use of React functional components.

Here’s the scenario. We have a form that doesn’t always fit on-screen, requiring the user to scroll through it. However, we want to keep the submit button at the bottom visible at all times and don’t want to resort to a fixed footer because let’s face it, they’re kind of ugly and cliche. Instead, we want a footer that floats in a fixed location when it would normally scroll below the bottom of the page and otherwise displays like a normal element in the natural flow of the page.

A few more requirements increase the challenge:

  • We have a header that we want to float if scrolled out of view as well
  • The footer height and space below the footer can vary with different amounts of content
  • The position of the footer in the normal page flow can be changed in real time by the user since we have expanding tool tips
  • The height of the scrolling element can change

Here is the final UI and in this blog, I’ll describe the robust solution we came up with in React to make this possible:

Solution

Here is the overall, high-level solution which I’ll then get into piece-by-piece:

  1. A function to render the footer UI is defined.
  2. We render two copies of this footer on the page - one wrapped in a div that is part of the natural page flow and a second wrapped in a div that is fixed/floating. We show one while hiding the other, depending on whether we want to float the footer.
  3. We make the determination of whether to float by calculating a shouldFloatFooter variable with React’s useMemo function. To ensure this variable is recalculated anytime the footer should switch from floating to non-floating, we add dependencies on the footer and scrolling elements themselves as well as a few other variables which could affect their size or position.

1. Footer UI rendering function

The only thing special about this function is that it takes a parameter that specifies if it is the visible copy or not. Based on the isActive parameter, we conditionally disable the button and include a testid used for indicating the element is visible during unit testing. Otherwise, the invisible copy is identical to the visible copy to ensure that it takes up the exact same space on the page:

const renderFooter = useCallback(    (isActive: Boolean) => {      return (        <div data-testid={isActive ? 'footer-container-id' : ''}>          <Button            disabled={!isActive || normalButtonDisableCondition}            dataTestId={isActive ? 'footer-button-id' : null}          >            Footer Action Text          </Button>        </div>      );    },    [normalButtonDisableCondition]  );

view rawFloatingFooterWithReact-1.tsx hosted with ❤ by GitHub

2. Render two copies of the footer

We then render the two copies of the UI as shown below - note we use visibility: hidden instead of display: none to ensure the in-flow div continues to take up the space it normally would if visible - this is the whole point of having the duplicate copy so it can react to all the intricacies of HTML rendering. We spent days trying to avoid rendering two copies of the footer UI and instead simply calculate where the footer would have shown up on the page and with what height if not floating. However, trying to emulate the outcome of HTML rendering is a messy, dirty thing and it ended up being much cleaner to simply use the hidden duplicate so we could always know for a fact where the in-flow copy would be positioned and what its height would be:

{/* Floating copy */}<div  className="footer--floating"  style={{ visibility: shouldFloatFooter ? 'visible' : 'hidden' }}>  {renderFooter(shouldFloatFooter)}</div>{/* In flow copy */}<div  ref={onFooterContainerChange}  style={{ visibility: shouldFloatFooter ? 'hidden' : 'visible' }}>  {renderFooter(!shouldFloatFooter)}</div>

view rawFloatingFooterWithReact-2a.tsx hosted with ❤ by GitHub

.footer--floating {  position: absolute;  bottom: 12px;  left: 12px;  right: 12px;  padding: 12px;  background-color: white;  border-radius: 8px;  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.15);  z-index: 99;}

view rawFloatingFooterWithReact-2b.css hosted with ❤ by GitHub

The one other part of the code to note is the ref={onFooterContainerChange} part of the in-flow div. This is the React way to get a reference to that div element so we can read its height and top positioning attributes. Here’s what the code for that function and associated state variables looks like:

const [footerContainerElement, setFooterContainerElement] = useState<HTMLElement>(null);const onFooterContainerChange = useCallback(  node => {    if (node !== null) {      setFooterContainerElement(node)    }  },  [setFooterContainerElement]);

view rawFloatingFooterWithReact-2c.tsx hosted with ❤ by GitHub

This reference to the footer container element is used in the shouldFloatFooter function.

3. shouldFloatFooter function

Here’s the function that actually makes the determination whether to float the footer or not. In itself, it’s a pretty simple calculation - the tricky part is when to call it again to recalculate. This is determined by the dependencies which I’ll get into one by one afterward. The basic condition is the following:

Is (the height of the scrolling container + the position of the scrolling container’s top border + the amount its been scrolled) less than (the position of the footer in the natural flow of the page + the footer height and the amount of padding we want below it)?

If the condition is true, that means the bottom border of the footer would show up lower on the page than we’d want to display it so we want to float it. If the condition is false, this means the bottom border of the footer is at or above the point in the page we’d want it displayed so we return false to remove the floating copy and show the natural flow copy instead.

const footerBottomPadding = 24;const floatFooter = useMemo(() => {   if (scrollingElement && footerContainerElement) {    const {      clientHeight: parentHeight,      offsetTop: parentOffsetTop,    } = scrollingElement;    const {      clientHeight: footerHeight,      offsetTop: footerOffsetTop,    } = footerContainerElement;    return (      parentHeight + parentOffsetTop + scrollTop <        footerOffsetTop + footerHeight + footerBottomPadding    );  }  return false;}, [  scrollTop,  scrollingElement,  footerContainerElement,  windowHeight,  lastMutation,]);

view rawFloatingFooterWithReact-3a.tsx hosted with ❤ by GitHub

Note that the entire function is wrapped in the condition that we’ve received a reference to the scrolling element and footer element. If we don’t have these references, we can’t make the calculation so we just return false. In the case of our code, the scrolling element is passed in from a parent element but ultimately it can be retrieved with a ref attribute on the scrolling element just like I showed above with how we get the reference to the footerContainerElement.

I’ll now review each of the dependencies on this memoized function which will retrigger its calculation if they ever change:

scrollTop

This is the most obvious condition we want to retrigger the calculation for. Any time the page is scrolled this variable holds the new offset in pixels of how far down it’s been scrolled. It is obtained as follows:

const onScroll = useCallback(  (e: SyntheticEvent) => {    const target = e.target as HTMLDivElement;    if (target.scrollTop > 0) {      setFloatHeader(true);    } else {      setFloatHeader(false);    }    setScrollTop(target.scrollTop);  },  [setScrollTop, setFloatHeader]);

view rawFloatingFooterWithReact-3b.tsx hosted with ❤ by GitHub

This onScroll function is just put in the onScroll attribute of the div where the scrolling occurs. Note that in the case of the floating header, we want it to float if the page is scrolled down at all (since its top edge should always be flush with the top of the page) so we use this function to simply set that variable to true if scrollTop is ever greater than 0 and that variable in turn is used to set a simple floating class on the header (unlike the footer it’s a fixed height and nothing can affect where it’s top position should be so it’s a much easier use case).

scrollingElement / footerContainerElement

These only change from null to the reference to their respective elements when they are first initialized.

windowHeight

If the window height changes, this means the height of the scrolling element could have changed so we need to recalculate shouldFloatFooter. This is tracked pretty simply - here’s the proper way of doing it functionally:

const [windowHeight, setWindowHeight] = React.useState(window.innerHeight);useEffect(() => {  function handleResize() {    setWindowHeight(window.innerHeight);  }  window.addEventListener('resize', handleResize);  return () => {    window.removeEventListener('resize', handleResize);  };});

view rawFloatingFooterWithReact-3c.tsx hosted with ❤ by GitHub

lastMutation

This is the most subtle of the dependencies and may not be needed depending on what content you could have above the footer. In our case, as you saw from the screenshot above, we have expandable tooltips which can animate open. There are also other places in our code where user actions can change the height of the page above the footer. Ultimately, we don’t want our footer to depend on the implementation of other parts of the page so here comes MutationObserver  to the rescue.

What this allows us to do is simply monitor a given element and all its child elements for certain types of changes. In our case, almost any change could affect the position of the footer on the page - an element added or removed or a change in the styling - so we listen to everything.

The first task is to define the MutationObserver - this would be pretty straightforward except for some slight hackiness needed to handle changes (such as expanding tip sections) which execute with a CSS animation since no mutation is triggered after an animation is finished:

const [mutationObserver, setMutationObserver] = useState<MutationObserver>(null);useEffect(() => {  let delayedMutationEffect;  function handleMutations(mutations: MutationRecord[]) {    setLastMutation(mutations[mutations.length - 1]);    delayedMutationEffect = setTimeout(() => setLastMutation(null), maxAnimationLength);  }  if (window.MutationObserver && !mutationObserver) {    setMutationObserver(new MutationObserver(handleMutations));  }  return () => {    if (mutationObserver) {      mutationObserver.disconnect();    }    if (delayedMutationEffect) {      clearTimeout(delayedMutationEffect);    }  };}, [setMutationObserver, mutationObserver, maxAnimationLength]);

view rawFloatingFooterWithReact-3d.tsx hosted with ❤ by GitHub

From the MutationObserver perspective we check if the browser supports it (almost all do) and if it isn’t defined yet and if so, we create one and save it in a state variable with a simple callback handler that just keeps track of the last mutation which has happened in another state variable.

The rub here is that when a mutation happens, we need to update the value of lastMutation after an animation could have finished so that the shouldFloatFooter function recalculates again in case the animation changed anything. We don’t actually use the value of lastMutation anywhere so we simply set it back to null.

This is the one piece of code that is less than ideal. It leads to a jerky update after the animation finishes instead of smoothly transitioning into floating if needed during the animation. It also depends on knowing the longest animation that could take place that would matter. And furthermore, it recalculates whether it should float the footer maxAnimationLength milliseconds after every mutation, not just the ones that trigger an animation. Unfortunately, based on my research, there’s currently no support for a more accurate and targeted way of listening to animation changes on the page. If anyone knows of one, would love to hear about it — tweet us @PinwheelAPI!

In the end, though, the code works pretty decently and we clean up our mutationObserver and potential timeout handler to avoid any memory leakage in the effect return statement.

Now that it’s defined, we have to set it up to listen to the elements we care about:

This is the footerElement itself and the parent scrollingElement so I just added it to the function referenced earlier where we first get the reference to the footer element:

const onFooterContainerChange = useCallback(  node => {    if (node !== null) {      setFooterContainerElement(node);      if (mutationObserver) {        mutationObserver.observe(node, {          childList: true,          characterData: true,          attributes: true,          subtree: true,        });        if (scrollingElement) {          mutationObserver.observe(scrollingElement, {            childList: true,            characterData: true,            attributes: true,            subtree: true,          });        }      }    }  },  [mutationObserver, scrollingElement]);

view rawFloatingFooterWithReact-3e.tsx hosted with ❤ by GitHub

Conclusion

That’s it! This solution handles the many complex scenarios which could change the height of the scrolling element, the height of the footer element, or the position of the footer element on the page which together determine whether it should float or not.

The code makes the determination instantaneously while also avoiding unnecessary recalculations, leading to a completely fluid and seamless transition from floating to non-floating which doesn’t hurt the performance of the page.

linkedin gray logotwitter gray logo
Insights

Tech spotlight: Floating footer with React functional components

Insights

Tech spotlight: Floating footer with React functional components

Challenge

checkmark icon
Solution

Ready to discover what Pinwheel can do for you?

Get Started ➔

Overview

We recently faced a common front end challenge here at Pinwheel which led to some interesting learnings for advanced use of React functional components.

Here’s the scenario. We have a form that doesn’t always fit on-screen, requiring the user to scroll through it. However, we want to keep the submit button at the bottom visible at all times and don’t want to resort to a fixed footer because let’s face it, they’re kind of ugly and cliche. Instead, we want a footer that floats in a fixed location when it would normally scroll below the bottom of the page and otherwise displays like a normal element in the natural flow of the page.

A few more requirements increase the challenge:

  • We have a header that we want to float if scrolled out of view as well
  • The footer height and space below the footer can vary with different amounts of content
  • The position of the footer in the normal page flow can be changed in real time by the user since we have expanding tool tips
  • The height of the scrolling element can change

Here is the final UI and in this blog, I’ll describe the robust solution we came up with in React to make this possible:

Solution

Here is the overall, high-level solution which I’ll then get into piece-by-piece:

  1. A function to render the footer UI is defined.
  2. We render two copies of this footer on the page - one wrapped in a div that is part of the natural page flow and a second wrapped in a div that is fixed/floating. We show one while hiding the other, depending on whether we want to float the footer.
  3. We make the determination of whether to float by calculating a shouldFloatFooter variable with React’s useMemo function. To ensure this variable is recalculated anytime the footer should switch from floating to non-floating, we add dependencies on the footer and scrolling elements themselves as well as a few other variables which could affect their size or position.

1. Footer UI rendering function

The only thing special about this function is that it takes a parameter that specifies if it is the visible copy or not. Based on the isActive parameter, we conditionally disable the button and include a testid used for indicating the element is visible during unit testing. Otherwise, the invisible copy is identical to the visible copy to ensure that it takes up the exact same space on the page:

const renderFooter = useCallback(    (isActive: Boolean) => {      return (        <div data-testid={isActive ? 'footer-container-id' : ''}>          <Button            disabled={!isActive || normalButtonDisableCondition}            dataTestId={isActive ? 'footer-button-id' : null}          >            Footer Action Text          </Button>        </div>      );    },    [normalButtonDisableCondition]  );

view rawFloatingFooterWithReact-1.tsx hosted with ❤ by GitHub

2. Render two copies of the footer

We then render the two copies of the UI as shown below - note we use visibility: hidden instead of display: none to ensure the in-flow div continues to take up the space it normally would if visible - this is the whole point of having the duplicate copy so it can react to all the intricacies of HTML rendering. We spent days trying to avoid rendering two copies of the footer UI and instead simply calculate where the footer would have shown up on the page and with what height if not floating. However, trying to emulate the outcome of HTML rendering is a messy, dirty thing and it ended up being much cleaner to simply use the hidden duplicate so we could always know for a fact where the in-flow copy would be positioned and what its height would be:

{/* Floating copy */}<div  className="footer--floating"  style={{ visibility: shouldFloatFooter ? 'visible' : 'hidden' }}>  {renderFooter(shouldFloatFooter)}</div>{/* In flow copy */}<div  ref={onFooterContainerChange}  style={{ visibility: shouldFloatFooter ? 'hidden' : 'visible' }}>  {renderFooter(!shouldFloatFooter)}</div>

view rawFloatingFooterWithReact-2a.tsx hosted with ❤ by GitHub

.footer--floating {  position: absolute;  bottom: 12px;  left: 12px;  right: 12px;  padding: 12px;  background-color: white;  border-radius: 8px;  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.15);  z-index: 99;}

view rawFloatingFooterWithReact-2b.css hosted with ❤ by GitHub

The one other part of the code to note is the ref={onFooterContainerChange} part of the in-flow div. This is the React way to get a reference to that div element so we can read its height and top positioning attributes. Here’s what the code for that function and associated state variables looks like:

const [footerContainerElement, setFooterContainerElement] = useState<HTMLElement>(null);const onFooterContainerChange = useCallback(  node => {    if (node !== null) {      setFooterContainerElement(node)    }  },  [setFooterContainerElement]);

view rawFloatingFooterWithReact-2c.tsx hosted with ❤ by GitHub

This reference to the footer container element is used in the shouldFloatFooter function.

3. shouldFloatFooter function

Here’s the function that actually makes the determination whether to float the footer or not. In itself, it’s a pretty simple calculation - the tricky part is when to call it again to recalculate. This is determined by the dependencies which I’ll get into one by one afterward. The basic condition is the following:

Is (the height of the scrolling container + the position of the scrolling container’s top border + the amount its been scrolled) less than (the position of the footer in the natural flow of the page + the footer height and the amount of padding we want below it)?

If the condition is true, that means the bottom border of the footer would show up lower on the page than we’d want to display it so we want to float it. If the condition is false, this means the bottom border of the footer is at or above the point in the page we’d want it displayed so we return false to remove the floating copy and show the natural flow copy instead.

const footerBottomPadding = 24;const floatFooter = useMemo(() => {   if (scrollingElement && footerContainerElement) {    const {      clientHeight: parentHeight,      offsetTop: parentOffsetTop,    } = scrollingElement;    const {      clientHeight: footerHeight,      offsetTop: footerOffsetTop,    } = footerContainerElement;    return (      parentHeight + parentOffsetTop + scrollTop <        footerOffsetTop + footerHeight + footerBottomPadding    );  }  return false;}, [  scrollTop,  scrollingElement,  footerContainerElement,  windowHeight,  lastMutation,]);

view rawFloatingFooterWithReact-3a.tsx hosted with ❤ by GitHub

Note that the entire function is wrapped in the condition that we’ve received a reference to the scrolling element and footer element. If we don’t have these references, we can’t make the calculation so we just return false. In the case of our code, the scrolling element is passed in from a parent element but ultimately it can be retrieved with a ref attribute on the scrolling element just like I showed above with how we get the reference to the footerContainerElement.

I’ll now review each of the dependencies on this memoized function which will retrigger its calculation if they ever change:

scrollTop

This is the most obvious condition we want to retrigger the calculation for. Any time the page is scrolled this variable holds the new offset in pixels of how far down it’s been scrolled. It is obtained as follows:

const onScroll = useCallback(  (e: SyntheticEvent) => {    const target = e.target as HTMLDivElement;    if (target.scrollTop > 0) {      setFloatHeader(true);    } else {      setFloatHeader(false);    }    setScrollTop(target.scrollTop);  },  [setScrollTop, setFloatHeader]);

view rawFloatingFooterWithReact-3b.tsx hosted with ❤ by GitHub

This onScroll function is just put in the onScroll attribute of the div where the scrolling occurs. Note that in the case of the floating header, we want it to float if the page is scrolled down at all (since its top edge should always be flush with the top of the page) so we use this function to simply set that variable to true if scrollTop is ever greater than 0 and that variable in turn is used to set a simple floating class on the header (unlike the footer it’s a fixed height and nothing can affect where it’s top position should be so it’s a much easier use case).

scrollingElement / footerContainerElement

These only change from null to the reference to their respective elements when they are first initialized.

windowHeight

If the window height changes, this means the height of the scrolling element could have changed so we need to recalculate shouldFloatFooter. This is tracked pretty simply - here’s the proper way of doing it functionally:

const [windowHeight, setWindowHeight] = React.useState(window.innerHeight);useEffect(() => {  function handleResize() {    setWindowHeight(window.innerHeight);  }  window.addEventListener('resize', handleResize);  return () => {    window.removeEventListener('resize', handleResize);  };});

view rawFloatingFooterWithReact-3c.tsx hosted with ❤ by GitHub

lastMutation

This is the most subtle of the dependencies and may not be needed depending on what content you could have above the footer. In our case, as you saw from the screenshot above, we have expandable tooltips which can animate open. There are also other places in our code where user actions can change the height of the page above the footer. Ultimately, we don’t want our footer to depend on the implementation of other parts of the page so here comes MutationObserver  to the rescue.

What this allows us to do is simply monitor a given element and all its child elements for certain types of changes. In our case, almost any change could affect the position of the footer on the page - an element added or removed or a change in the styling - so we listen to everything.

The first task is to define the MutationObserver - this would be pretty straightforward except for some slight hackiness needed to handle changes (such as expanding tip sections) which execute with a CSS animation since no mutation is triggered after an animation is finished:

const [mutationObserver, setMutationObserver] = useState<MutationObserver>(null);useEffect(() => {  let delayedMutationEffect;  function handleMutations(mutations: MutationRecord[]) {    setLastMutation(mutations[mutations.length - 1]);    delayedMutationEffect = setTimeout(() => setLastMutation(null), maxAnimationLength);  }  if (window.MutationObserver && !mutationObserver) {    setMutationObserver(new MutationObserver(handleMutations));  }  return () => {    if (mutationObserver) {      mutationObserver.disconnect();    }    if (delayedMutationEffect) {      clearTimeout(delayedMutationEffect);    }  };}, [setMutationObserver, mutationObserver, maxAnimationLength]);

view rawFloatingFooterWithReact-3d.tsx hosted with ❤ by GitHub

From the MutationObserver perspective we check if the browser supports it (almost all do) and if it isn’t defined yet and if so, we create one and save it in a state variable with a simple callback handler that just keeps track of the last mutation which has happened in another state variable.

The rub here is that when a mutation happens, we need to update the value of lastMutation after an animation could have finished so that the shouldFloatFooter function recalculates again in case the animation changed anything. We don’t actually use the value of lastMutation anywhere so we simply set it back to null.

This is the one piece of code that is less than ideal. It leads to a jerky update after the animation finishes instead of smoothly transitioning into floating if needed during the animation. It also depends on knowing the longest animation that could take place that would matter. And furthermore, it recalculates whether it should float the footer maxAnimationLength milliseconds after every mutation, not just the ones that trigger an animation. Unfortunately, based on my research, there’s currently no support for a more accurate and targeted way of listening to animation changes on the page. If anyone knows of one, would love to hear about it — tweet us @PinwheelAPI!

In the end, though, the code works pretty decently and we clean up our mutationObserver and potential timeout handler to avoid any memory leakage in the effect return statement.

Now that it’s defined, we have to set it up to listen to the elements we care about:

This is the footerElement itself and the parent scrollingElement so I just added it to the function referenced earlier where we first get the reference to the footer element:

const onFooterContainerChange = useCallback(  node => {    if (node !== null) {      setFooterContainerElement(node);      if (mutationObserver) {        mutationObserver.observe(node, {          childList: true,          characterData: true,          attributes: true,          subtree: true,        });        if (scrollingElement) {          mutationObserver.observe(scrollingElement, {            childList: true,            characterData: true,            attributes: true,            subtree: true,          });        }      }    }  },  [mutationObserver, scrollingElement]);

view rawFloatingFooterWithReact-3e.tsx hosted with ❤ by GitHub

Conclusion

That’s it! This solution handles the many complex scenarios which could change the height of the scrolling element, the height of the footer element, or the position of the footer element on the page which together determine whether it should float or not.

The code makes the determination instantaneously while also avoiding unnecessary recalculations, leading to a completely fluid and seamless transition from floating to non-floating which doesn’t hurt the performance of the page.

Always stay up to date

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
View our Privacy Policy   ➔

Up next

Pinwheel Pulse: 2024 Year in Review

Pinwheel Pulse: 2024 Year in Review

From groundbreaking product developments to new partnerships, 2024 has been a banner year for Pinwheel, our partners, and our customers.

Read more  ➔
Why credit unions have a neighborhood advantage

Why credit unions have a neighborhood advantage

n an era dominated by digital innovation, physical branches remain relevant—and even essential. While many national financial institutions are doubling down on digital channels and closing branches, 60% of new accounts are still being opened in person. This underscores a critical competitive differentiator for credit unions, who maintain a unique home town advantage.

Read more  ➔
The Product Pulse

The Product Pulse

Pinwheel Pulse is where we share recent highlights on our journey to building solutions that drive primacy and unlock the potential of payroll connectivity.

Read more  ➔
Introducing Bill Navigator

Introducing Bill Navigator

A new and powerful tool for banks to leverage in their battle for primacy.

Read more  ➔
PreMatch results are In

PreMatch results are In

We began beta testing our groundbreaking new solution for authorizing payroll accounts in Q1 2024. And we’re thrilled to share that the industry’s only 100% credential-less solution that automatically identifies users’ payroll accounts is living up to the hype.

Read more  ➔
How we achieve the industry’s best conversion rates

How we achieve the industry’s best conversion rates

Pinwheel Prime’s intelligent waterfall covers 100% of U.S. payroll scenarios and delivers 30% more successful conversions than any other provider in the industry.

Read more  ➔
Automated direct deposit is powering the next generation of growth for credit unions

Automated direct deposit is powering the next generation of growth for credit unions

Neo-banks like Cash App and Chime have mastered the art of digital onboarding, leveraging automated direct deposit features as a key driver of customer acquisition and long-term engagement. By investing in payroll integrations that deliver a seamless direct deposit enrollment option during onboarding, these fintech giants have grown their actively funded user base at hyper speeds.

Read more  ➔
Nassau Financial Credit Union Selects Pinwheel As Direct Deposit Switch Partner

Nassau Financial Credit Union Selects Pinwheel As Direct Deposit Switch Partner

Nassau Financial’s mission is to improve the financial well-being of each member in the communities we serve, reflecting the credit union philosophy of "People helping people."

Read more  ➔
Perpay increases direct deposit conversion rate for eligible users by 2.19x with Pinwheel PreMatch

Perpay increases direct deposit conversion rate for eligible users by 2.19x with Pinwheel PreMatch

Implementation of the Pinwheel Prematch feature took Perpay only 3 weeks, after which they saw an immediate performance uplift. For Perpay customers eligible for PreMatch, they saw a 2.19x increase to switch success rate, and an 11% overall switch conversion improvement.

Read more  ➔
Success rate for EECU’s deposit change feature improves by over 50% after switching to Pinwheel PreMatch

Success rate for EECU’s deposit change feature improves by over 50% after switching to Pinwheel PreMatch

Read more  ➔
SafeLink expands access to frictionless experiences

SafeLink expands access to frictionless experiences

In the ever-evolving landscape of banking and fintech, winning the primary banking relationship has never been more difficult. In our recent white paper, Primacy in personalized banking: a moving target, we reveal new consumer insights that emphasize the importance of offering seamless digital experiences to win and keep customers. 

Read more  ➔
Primacy in personalized banking: A moving target

Primacy in personalized banking: A moving target

Learn about changes to consumer behavior that have affected financial institutions' ability to achieve and defend primacy.

Read more  ➔
Industry leaders talk consumer bank switching behaviors

Industry leaders talk consumer bank switching behaviors

Are you curious about the latest behavioral trends in consumer banking? Do you want to know how top financial institutions are adapting to meet evolving customer needs? 

Read more  ➔
What drives primacy with today’s consumer?

What drives primacy with today’s consumer?

Discover what consumers believe is needed for bank account primacy, and what really motivates them to switch.

Read more  ➔
Why  are consumers on the move?

Why are consumers on the move?

See what's driving the acceleration of bank switching across generations.

Read more  ➔
The branch of the future

The branch of the future

With most banks focusing investments on digital and reducing branch networks, 60% of new accounts are still being opened in branch. For decades, bank branches have been a cornerstone of everyday life in America’s cities and towns. But these days, branches are getting harder and harder to come by. By the end of 2023, the number of bank branches in the U.S.

Read more  ➔
Giving credit where it’s due

Giving credit where it’s due

Are credit scores missing the point?Despite the lip service in banking pledging fairness and equity, systemic issues can allow discrimination in extending credit to surface in unexpected ways. One glaring example is the industry’s struggle to underwrite those who have non-traditional career histories or those who make their living through self employment or the gig economy. Whether they need to produce two years’ worth of tax returns or a specific credit score, many financially secure Americans still struggle to present the “right” paperwork. As a result, banks cannot deliver an optimal experience for:

Read more  ➔
Trust and Verify

Trust and Verify

Read more  ➔
Citizens & Pinwheel talk primacy

Citizens & Pinwheel talk primacy

A conversation with Chris Powell, EVP & Head of Deposits, Citizens Bank and Kurtis Lin, Co-Founder & CEO, Pinwheel. Last month, Pinwheel hosted a webinar with the Consumer Bankers Association to explore our latest research on The Power of Primacy conducted in partnership with The Digital Banking Report. In a discussion moderated by Pinwheel CMO, Crystal Gopman, Chris Powell, EVP and Head of Deposits at Citizens Bank and Pinwheel CEO, Kurtis Lin, delved deep into the challenges banks are facing today as they compete for share of wealth with the modern consumer. 

Read more  ➔
Be the Amazon of banks  

Be the Amazon of banks  

The next time you’re browsing on your phone, add this idea to your cart: Does your bank need to engage customers less like an overworked teller and more like Amazon?In a climate of exploding technology and regulatory scrutiny, the BAI Banking Outlook: 2024 Trends survey identified the customer digital experience as a top priority for this year, citing technology integration and intuitive platforms as the pathways to engagement that’s personal, frictionless – and even somewhat fun. But as you know, fraud mitigation and other protocols can introduce drag and result in user drop-off. “Somewhat unfairly, customers measure their bank’s digital delivery of services against the practices of world-class online retailers,” the survey concludes. 

Read more  ➔
Enhancing digital trust: Inside Pinwheel's commitment to security

Enhancing digital trust: Inside Pinwheel's commitment to security

Ensuring Digital Security in the rapidly evolving digital world, the importance of security cannot be overstated. As the Chief Information Security Officer at Pinwheel, I'm at the forefront of our battle against digital threats. Our mission is clear: to safeguard our clients' data with the most robust security measures available. This dedication is embodied in our two flagship products: Pinwheel Core and Pinwheel Prime.

Read more  ➔
Who’s making money moves in 2024?

Who’s making money moves in 2024?

Are your new customer acquisition goals higher than ever in 2024? Yes? Then, you’re on trend. Late last year, the BAI surveyed 102 financial services organizations to gain insights for the coming year. The findings of the study, the BAI 2024 Banking Outlook, placed customer acquisition in the top two priorities—just behind deposit growth—for bankers. Coming in third? You can probably guess. Enhanced digital banking experiences. And we’re here for it. 

Read more  ➔
New study confirms direct deposit unlocks primacy

New study confirms direct deposit unlocks primacy

The report highlights a significant disconnect between banks' customer acquisition strategies and consumer behavior. While banks continue to invest heavily in account opening incentives, such as cash bonuses and promotional offers, these efforts often fall short of their intended goal. To the dismay of banks, although they spend high acquisition costs to attract new customers, many of these accounts remain dormant.

Read more  ➔
Consumer bank switching behavior demystified

Consumer bank switching behavior demystified

My january piece on the offer wars got me thinking: what actually motivates consumers to switch banks? Are rich account opening incentives turning heads, or is our industry missing the mark? To answer this question, we partnered with Savanta research to understand the inner dialogue of a consumer contemplating a new banking relationship.

Read more  ➔
The metrics you care about the most are now available in real-time

The metrics you care about the most are now available in real-time

Pinwheel was founded to unlock powerful income data for financial institutions, so that they can better serve their users with more personalized products, driving long term relationships that are proven to deliver better financial outcomes. Introducing the Dashboard Activity Page for Real-Time Engagement Insights.

Read more  ➔
New Jack Henry partnership makes it easier for community banks to take advantage of Pinwheel

New Jack Henry partnership makes it easier for community banks to take advantage of Pinwheel

We are thrilled to announce Pinwheel's new strategic partnership with Jack Henry, a leading financial technology company, which gives their customers a fast path to implementation for the industry's top performing Direct Deposit Switching solution. This collaboration is set to revolutionize the digital direct deposit setup experience for accountholders at community and regional financial institutions.

Read more  ➔
 Pinwheel's CMO discusses bank competition for primacy in 2024

Pinwheel's CMO discusses bank competition for primacy in 2024

Banks rival top brands like Coke and P&G as the highest spending advertisers in the world. And bank marketing teams - channeling their best Cardi B energy - literally make money move with hundreds of millions of dollars at their disposal to hit annual growth goals. While the accounts keep rolling in, there’s a frantic scramble as institutions fumble in their attempts to convert active customers and meaningful engagement through aggressive, unsustainable offers. 

Read more  ➔
Hear from Pinwheel’s Chief revenue officer on growing profitably in 2024

Hear from Pinwheel’s Chief revenue officer on growing profitably in 2024

As the Chief Revenue Officer at Pinwheel, I speak to executives from the world’s top banks every day and I see first hand how rapidly the financial services industry is changing. With approximately 94% of the U.S. population holding bank accounts and a staggering 13 million new accounts opened in 2022, the competitive quest for primacy, or being the primary account for a customer, is more intense than ever. That’s why digital advertising spend is on track to close out 2023 by surpassing $30 billion.

Read more  ➔
Introducing the next generation of Automated Direct Deposit Switching

Introducing the next generation of Automated Direct Deposit Switching

Introducing our first-of-its-kind, reimagined automated direct deposit switching experience, expected to at least double end-to-end conversion.

Read more  ➔
Know Your Fraudster Q&A with Robert Reynolds

Know Your Fraudster Q&A with Robert Reynolds

Read more  ➔
Fraud Fighers Chapter 1: Know Your Fraudster

Fraud Fighers Chapter 1: Know Your Fraudster

Read more  ➔
This is how banks close the loop with branch guests: Introducing Pinwheel Smart Branch

This is how banks close the loop with branch guests: Introducing Pinwheel Smart Branch

Read more  ➔
Meet Pinwheel Smart Branch

Meet Pinwheel Smart Branch

Learn about how Smart Branch enables a new generation of integrated omnichannel campaigns.

Read more  ➔
Introducing Pinwheel Deposit Switch 2.0, a revolutionary upgrade that maximizes coverage and conversion for every US worker

Introducing Pinwheel Deposit Switch 2.0, a revolutionary upgrade that maximizes coverage and conversion for every US worker

Deposit Switch 2.0 allows every US worker to update their direct deposit settings regardless of where their direct deposit comes from.

Read more  ➔
Key factors to consider before implementing a payroll connectivity API

Key factors to consider before implementing a payroll connectivity API

Before integrating a payroll connectivity API, you should evaluate it based on coverage, conversion, implementation, security, and compliance.

Read more  ➔
Lendly improves its loan servicing with Pinwheel’s support

Lendly improves its loan servicing with Pinwheel’s support

Read more  ➔
Enhance credit line management with income data

Enhance credit line management with income data

Read more  ➔
See your customers’ earnings weeks into the future with projected earnings

See your customers’ earnings weeks into the future with projected earnings

Read more  ➔
Demo Video: Pinwheel’s direct deposit switching solution

Demo Video: Pinwheel’s direct deposit switching solution

Read more  ➔
How Pinwheel helped Perpay increase conversion by 29%

How Pinwheel helped Perpay increase conversion by 29%

Read more  ➔
How to reduce default risk with consumer-permissioned data

How to reduce default risk with consumer-permissioned data

Read more  ➔
Digital lending technologies and trends that are shaping the industry

Digital lending technologies and trends that are shaping the industry

Read more  ➔
4 technologies that improve fraud detection in banking

4 technologies that improve fraud detection in banking

Read more  ➔
Why automated income verification is a must-have feature for lenders

Why automated income verification is a must-have feature for lenders

Read more  ➔
December product release: 10% increase in conversion, enhanced security and access to pay frequency data

December product release: 10% increase in conversion, enhanced security and access to pay frequency data

Read more  ➔
A conversation with our Chief Information Security Officer

A conversation with our Chief Information Security Officer

Read more  ➔
Former CFPB Deputy Director Raj Date Joins Pinwheel as an Advisor

Former CFPB Deputy Director Raj Date Joins Pinwheel as an Advisor

Read more  ➔
Cash flow underwriting: Benefits & how to access cash flow data

Cash flow underwriting: Benefits & how to access cash flow data

Read more  ➔
Beyond the Credit Score: Propelling consumer finance into the future with income data

Beyond the Credit Score: Propelling consumer finance into the future with income data

Read more  ➔
Why banks need a payroll connectivity API that prioritizes information security

Why banks need a payroll connectivity API that prioritizes information security

Read more  ➔
How alternative credit data can benefit lenders

How alternative credit data can benefit lenders

Read more  ➔
Tech Spotlight: Implementing your first feature flag

Tech Spotlight: Implementing your first feature flag

Read more  ➔
Pinwheel Welcomes New Advisor, Ethan Yeh, to Advance Pinwheel’s Data Science Strategy

Pinwheel Welcomes New Advisor, Ethan Yeh, to Advance Pinwheel’s Data Science Strategy

Read more  ➔
Tech spotlight: Securing access control across internal services

Tech spotlight: Securing access control across internal services

Read more  ➔
Leading wealth management firm partners with Pinwheel to take its wealth-building solutions to the next level

Leading wealth management firm partners with Pinwheel to take its wealth-building solutions to the next level

Read more  ➔
The anatomy and potential of payroll data: Transforming complex data into insights

The anatomy and potential of payroll data: Transforming complex data into insights

Read more  ➔
Beyond the credit score: Propelling consumer finance into the future with income data

Beyond the credit score: Propelling consumer finance into the future with income data

Read more  ➔
Ayokunle (Ayo) Omojola joins Pinwheel’s Board of Directors

Ayokunle (Ayo) Omojola joins Pinwheel’s Board of Directors

Read more  ➔
Conquering conversion: Engineering practices developed to help customers

Conquering conversion: Engineering practices developed to help customers

Read more  ➔
Boost your direct deposit strategy with earned wage access

Boost your direct deposit strategy with earned wage access

A 12-page guide to leveraging earned wage access (EWA) to incentivize direct deposit switching.

Read more  ➔
Driving Customer Delight: From implementation and beyond

Driving Customer Delight: From implementation and beyond

Read more  ➔
Pinwheel Supports Open Finance Data Security Standard

Pinwheel Supports Open Finance Data Security Standard

Read more  ➔
How we design Pinwheel to solve real customer problems

How we design Pinwheel to solve real customer problems

Read more  ➔
What is consumer-permissioned data and what are its benefits?

What is consumer-permissioned data and what are its benefits?

Read more  ➔
How payroll data connectivity can help financial service providers in tumultuous market conditions

How payroll data connectivity can help financial service providers in tumultuous market conditions

Read more  ➔
Pinwheel now supports document uploads to supplement payroll data

Pinwheel now supports document uploads to supplement payroll data

Read more  ➔
Brian Karimi-Pashaki joins Pinwheel as Partnerships Lead

Brian Karimi-Pashaki joins Pinwheel as Partnerships Lead

Read more  ➔
Optimizing for conversion with smarter employer mappings

Optimizing for conversion with smarter employer mappings

Read more  ➔
What are super apps and how will they impact financial services?

What are super apps and how will they impact financial services?

Read more  ➔
Increase conversions and maximize share of wallet with Pinwheel's new UX update

Increase conversions and maximize share of wallet with Pinwheel's new UX update

Read more  ➔
Pinwheel announces support for taxes

Pinwheel announces support for taxes

Read more  ➔
Ryan Nier Joins Pinwheel as the Company’s first General Counsel

Ryan Nier Joins Pinwheel as the Company’s first General Counsel

Read more  ➔
The future of enabling earned wage access

The future of enabling earned wage access

Read more  ➔
The ultimate guide to automated direct deposit switching

The ultimate guide to automated direct deposit switching

Read more  ➔
Deliver earned wage access faster with Pinwheel Earnings Stream

Deliver earned wage access faster with Pinwheel Earnings Stream

Pinwheel Earnings Stream provides the necessary data and intelligence to reliably offer earned wage access (EWA) at scale.

Read more  ➔
Digital transformation in banking in 2022: What it means, trends & examples

Digital transformation in banking in 2022: What it means, trends & examples

Read more  ➔
June product release: Expanded connectivity to employers, a custom experience with Link API and more

June product release: Expanded connectivity to employers, a custom experience with Link API and more

Read more  ➔
Pinwheelie Spotlight: LaRena Iocco, Software Engineer

Pinwheelie Spotlight: LaRena Iocco, Software Engineer

Read more  ➔
How Perpay removed friction from its customer journey using Pinwheel

How Perpay removed friction from its customer journey using Pinwheel

Read more  ➔
Build fully custom experiences with Pinwheel’s Link API

Build fully custom experiences with Pinwheel’s Link API

Read more  ➔
Pinwheel expands connectivity to 1.5M employers

Pinwheel expands connectivity to 1.5M employers

Read more  ➔
Robert Reynolds joins Pinwheel as Head of Product

Robert Reynolds joins Pinwheel as Head of Product

Read more  ➔
Pinwheel obtains highest security certification in the industry

Pinwheel obtains highest security certification in the industry

Read more  ➔
Lauren Crossett becomes Pinwheel’s first Chief Revenue Officer

Lauren Crossett becomes Pinwheel’s first Chief Revenue Officer

Read more  ➔
Everything you should know about the role of APIs in banking

Everything you should know about the role of APIs in banking

Read more  ➔
Open finance: What is it and how does it impact financial services?

Open finance: What is it and how does it impact financial services?

Read more  ➔
How automated direct deposit switching benefits traditional banks

How automated direct deposit switching benefits traditional banks

Read more  ➔
Pinwheel Secure: Authentication optimized for market-leading conversion

Pinwheel Secure: Authentication optimized for market-leading conversion

Read more  ➔
Pinwheelie Spotlight: Elena Churilova, Software Engineer, Integrations

Pinwheelie Spotlight: Elena Churilova, Software Engineer, Integrations

Read more  ➔
May product release: Localization and downloadable pay stubs

May product release: Localization and downloadable pay stubs

Read more  ➔
How a payroll API can level up lenders and renters

How a payroll API can level up lenders and renters

Read more  ➔
The power of payroll APIs in consumer finance

The power of payroll APIs in consumer finance

Read more  ➔
Data Talks: Pinwheel’s Fortune 1000 coverage and top employer trends

Data Talks: Pinwheel’s Fortune 1000 coverage and top employer trends

Read more  ➔
April product release: Enabling connectivity to time and attendance data for 25M US workers

April product release: Enabling connectivity to time and attendance data for 25M US workers

Read more  ➔
Tech spotlight: Increasing engineering momentum at a systems level

Tech spotlight: Increasing engineering momentum at a systems level

Read more  ➔
How crypto exchanges can turn direct deposits into a fiat onramp

How crypto exchanges can turn direct deposits into a fiat onramp

Read more  ➔
March product release: Time and attendance coverage and Pinwheel's new online home

March product release: Time and attendance coverage and Pinwheel's new online home

Read more  ➔
Pinwheelie spotlight: Arianna Gelwicks, Tech Recruiting

Pinwheelie spotlight: Arianna Gelwicks, Tech Recruiting

Read more  ➔
What is payroll data and how it benefits proptech companies

What is payroll data and how it benefits proptech companies

Read more  ➔
Earned wage access: What is it and why does it matter?

Earned wage access: What is it and why does it matter?

Read more  ➔