Sticky Header

Hi, I would like to implement a resizable sticky header like the one found at https://frontity.org/

Thks ! :hugs:

I found a solution, I would like to share it and I await your comments :smile:

import React, { useState, useEffect, useRef } from 'react';
import { connect, styled } from 'frontity';
import Nav from './nav';

const Header = () => {

    const [height, setHeight] = useState(0);

    const ref = useRef();

    useEffect(() => {
        setHeight(ref.current.getBoundingClientRect().height);
    }, []);

    const [scrollTop, setScrollTop] = useState(0);

    useEffect(() => {
        const onScroll = (e) => {
            setScrollTop(e.target.documentElement.scrollTop);
        };
        window.addEventListener('scroll', onScroll);

        return () => {
            window.removeEventListener('scroll', onScroll);
        }
    });

    return (
        <Container className={scrollTop > height ? 'sticky' : null}>
            <div ref={ref} className='header-top'>
                <Nav />
            </div>
            <div className='header-sticky'>
                <Nav blue/>
            </div>
        </Container>
    );
};

export default connect(Header);

const Container = styled.div`
    top: 0;
    position: relative;
    overflow: hidden;

    & img {
        padding: 1em;
    }

    &.sticky {
        position: sticky
    }

    & .header-top {
        transition: .225s ease-out;
        background: #4269e4;

        & a {
            color: #fff
        }
    }  

    &.sticky .header-top {
        opacity: .5;
        transform: translateY(-100%);
        transition: none;
    }

    & .header-sticky {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        opacity: .5;
        transform: translateY(-100%);
        background: #fff;
    }

    &.sticky .header-sticky {
        opacity: 1;
        transform: translateY(0);
        transition: .35s ease-out;
    }
`;

Hmm… I don’t think that’s a good solution. Subscribing to scroll and causing a React re-render on each event it’s not going to be great performance-wise because you are causing a lot of reflows. You can see a list of things that cause a reflow here: https://gist.github.com/paulirish/5d52fb081b3570c81e3a

I think it’d be better to create add the menu twice, one static at the top and one fixed that only appears when you scroll down. You can control when to hide or show the fixed one using an useInView hook.

We had our own useInView hook but we are now recommending this one which has more features: https://www.npmjs.com/package/react-intersection-observer

This is a rough implementation of how I would do it. Once the relative menu disappears from the screen, the other one appears.

const Menus = () => {
  const [ref, inView] = useInView();

  return (
    <>
      <Menu position="relative" ref={ref} />
      {!inView && <Menu position="fixed" />
    </>
  );
}

const Menu = styled(MenuItems)`
  position: ${props => props.position}
  ...
`;

If you want to animate the entrance of the fixed menu, you can use react-spring.

cc: @David may we have your opinion here?

1 Like

I just found this article on Twitter, it might be helpful: https://dev.to/ibrahima92/build-a-sticky-navigation-bar-with-react-3bjh

1 Like

Is this a good solution?

According to what @luisherranz thinks it’s better to use useInView

What do you think?

Yes, I think that using the Intersection Observer both improves the performance and it’s simpler to code. But I’m not an expert. @David and @orballo are much better than me on this type of performance issues!

I can’t figure out how to use React Spring

This is the code:

        <>
            <div ref={ref}>
                <TopNavbar />
            </div>

            { !inView && <Navbar /> }
        </>

Where TopNavbar and Navbar are components,
I need to animate the entrance of Navbar

This is cool: https://github.com/KyleAMathews/react-headroom

The scroll event is denounced using request animation frame, which should reduce the reflows.

I think you can use useTransition for that: https://www.react-spring.io/docs/hooks/use-transition

Look for the “mount/unmount single-component reveals” example.

I have not tested, but I guess it could be implemented like this (using useTransition as @luisherranz suggested) :

const Navbars = () => {
  const [ref, inView] = useInView();
  const transitions = useTransition(!inView, null, {
    from: { position: "fixed", top: 0, transform: "translateY(-100%)" },
    enter: { transform: "translateY(0)" },
    leave: { transform: "translateY(-100%)" }
  });

  return (
    <>
      <div ref={ref}>
        <TopNavbar />
      </div>
      {transitions.map(
        ({ item, key, props }) =>
          item && (
            <animated.div key={key} style={props}>
              <Navbar />
            </animated.div>
          )
      )}
    </>
  );
};
1 Like