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.

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

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  ➔
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  ➔
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  ➔
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  ➔
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  ➔
How fintech APIs are transforming financial services

How fintech APIs are transforming financial services

Read more  ➔
Webinar: Unleash growth with income and payroll APIs

Webinar: Unleash growth with income and payroll APIs

Read more  ➔
February product release: Updated Link UX and data quality

February product release: Updated Link UX and data quality

Read more  ➔
Why a direct deposit switching API is a must-have for banks and neobanks

Why a direct deposit switching API is a must-have for banks and neobanks

Read more  ➔
Tech spotlight: Floating footer with React functional components

Tech spotlight: Floating footer with React functional components

Read more  ➔
Pinwheelie spotlight: Hale Ahangi, People Operations Lead

Pinwheelie spotlight: Hale Ahangi, People Operations Lead

Read more  ➔
Shift from a vicious to virtuous cycle: The foundation for a fairer financial system

Shift from a vicious to virtuous cycle: The foundation for a fairer financial system

Read more  ➔
January product release: Recurring access to income & employment

January product release: Recurring access to income & employment

Read more  ➔
Pinwheel’s Series B and our path towards a fairer financial future

Pinwheel’s Series B and our path towards a fairer financial future

We're excited to share that we have raised a $50M Series B funding round led by GGV Capital with participation from many others.

Read more  ➔
Pinwheelie spotlight: Devin DeCaro-Brown, Product Manager

Pinwheelie spotlight: Devin DeCaro-Brown, Product Manager

Read more  ➔
Tech spotlight: How to implement async requests in your Python code

Tech spotlight: How to implement async requests in your Python code

Read more  ➔
2021 recap and product update: An amazing year for Pinwheel

2021 recap and product update: An amazing year for Pinwheel

Read more  ➔
Charles Tsang joins Pinwheel as Head of Marketing

Charles Tsang joins Pinwheel as Head of Marketing

Read more  ➔
Pinwheelie spotlight: Octavio Roscioli, Senior Software Engineer

Pinwheelie spotlight: Octavio Roscioli, Senior Software Engineer

Read more  ➔
November product release: Beta launch of income & employment monitoring

November product release: Beta launch of income & employment monitoring

Read more  ➔
How can payroll data help with one’s financial picture?

How can payroll data help with one’s financial picture?

Read more  ➔
Pinwheelie spotlight: Caroline Lo, Software Engineer

Pinwheelie spotlight: Caroline Lo, Software Engineer

Read more  ➔
2021 company onsite: Bringing Pinwheelies together

2021 company onsite: Bringing Pinwheelies together

Read more  ➔
October product release: Beta launch of direct deposit allocation monitoring

October product release: Beta launch of direct deposit allocation monitoring

Read more  ➔
Why payroll data access is inevitable on your product roadmap

Why payroll data access is inevitable on your product roadmap

Read more  ➔
Security spotlight: SOC 2 compliance

Security spotlight: SOC 2 compliance

Read more  ➔
Jeff Hudesman joins Pinwheel as Chief Information Security Officer

Jeff Hudesman joins Pinwheel as Chief Information Security Officer

Read more  ➔
Welcoming John Whitfield, VP of Engineering

Welcoming John Whitfield, VP of Engineering

Read more  ➔
Announcing Pinwheel’s FCRA Compliance

Announcing Pinwheel’s FCRA Compliance

Read more  ➔
Pinwheel's statement on Section 1033

Pinwheel's statement on Section 1033

Read more  ➔
Pinwheel raises $20M Series A

Pinwheel raises $20M Series A

Read more  ➔
If I were a fintech founder

If I were a fintech founder

Read more  ➔
Pinwheelie spotlight: Phil Jen, Director of Product

Pinwheelie spotlight: Phil Jen, Director of Product

Read more  ➔
Celebrating women's history month with Sasha Pilch

Celebrating women's history month with Sasha Pilch

Read more  ➔
Tech spotlight: How we re-launched our API docs

Tech spotlight: How we re-launched our API docs

Read more  ➔
Why I chose Pinwheel: Payroll APIs as the next frontier

Why I chose Pinwheel: Payroll APIs as the next frontier

Read more  ➔
Lunch and learn with Nik Milanović

Lunch and learn with Nik Milanović

Read more  ➔
Pinwheelie spotlight: David Daudelin, Senior Front End Engineer

Pinwheelie spotlight: David Daudelin, Senior Front End Engineer

Read more  ➔
The missing link

The missing link

Read more  ➔
Introducing Pinwheel, the API for payroll

Introducing Pinwheel, the API for payroll

Read more  ➔