UI Lab #1

A friend of mine recently pointed me out to the Sketchin website for its smooth animations in the home page and the menu:

I really like it, so I wanted to recreate most of it on my own using React and Gsap.

Here the final result:

You can find the end result at this url: UI Lab One and the source code on GitHub.

Note: for the sake of the exercise, the code target only tablet or desktop devices

Into the code

The small application I created for this lab, scaffolded with Create React App, has a very simple structure:

function App() {
  return (
    <AppProvider>
      <div className="App">

        <Sidebar />

        <MenuPanel />

        <Hero />

        <Services />

        <Media />

      </div>

    </AppProvider>
  );
}

AppProvider component

Just before working on this lab, I read the very insightful article How to use React Context effectively by Kent C. Dodds, so I wanted to use the Context API to avoid props-drilling; the implementation of the AppProvider component comes from that article.

Sidebar component

The sidebar component contains only the toggle burger button and the bare minimum logic to handle the toggle:

const Sidebar = () => {
  const { menuIsOpen } = useAppState();
  const dispatch = useAppDispatch();

  return (
    <div className={`${styles.Sidebar} ${menuIsOpen ? styles.SidebarIsOpen : ''}`}>
      <button type="button" className={styles.SidebarBurger} onClick={() => dispatch({ type: 'toggleMenu' })}>
        <span />
        <span />
      </button>
    </div>
  );
};

The button animation is handled in pure css using transform transitions (see Sidebar.module.scss).

Clicking the button will dispatch the toggleMenu action that via context will be passed to the MenuPanel component.

MenuPanel component

This component is the full screen menu toggled by the sidebar button:

const MenuPanel = () => {
  const { menuIsOpen } = useAppState();
  const menuPanelRef = useRef();
  const menuToRevealAndStagger = useRef();
  const content1ToReveal = useRef();

  const timeline = useMemo(() => {
    return gsap.timeline({ paused: true });
  }, []);

  useEffect(() => {
    // animate menu panel
    timeline.fromTo(
      menuPanelRef.current,
      {
        x: '-100%',
        zIndex: -1
      },
      {
        x: 0,
        zIndex: 10,
        duration: 1.2,
        ease: 'expo.inOut'
      }
    );

    // animate menu links
    const elements = menuToRevealAndStagger.current.querySelectorAll('li');
    timeline.fromTo(
      elements,
      {
        autoAlpha: 0,
        x: '-50px',
        y: '0%'
      },
      {
        autoAlpha: 1,
        x: '0%',
        y: '0%',
        duration: 0.7,
        stagger: 0.05,
        onComplete() {
          clearOpacity(elements);
        }
      }
    );

    // animate content 1
    const contents = content1ToReveal.current.querySelectorAll('*');
    timeline.fromTo(
      contents,
      {
        x: '-100%'
      },
      {
        x: 0,
        duration: 0.5
      },
      '<'
    );
  }, [timeline]);

  useEffect(() => {
    if (menuPanelRef.current) {
      if (menuIsOpen) {
        timeline.play();
      } else {
        timeline.reverse();
      }
    }
  }, [menuPanelRef, menuIsOpen, timeline]);

  return (
    <section ref={menuPanelRef} className={`${styles.MenuPanel} ${menuIsOpen ? styles.MenuPanelIsOpen : ''}`}>
      <header>
        <nav>
          <ul ref={menuToRevealAndStagger}>
            <li>
              <a href="#">About</a>
            </li>
            <li>
              <a href="#">Services</a>
            </li>
            <li>
              <a href="#">Projects</a>
            </li>
            <li>
              <a href="#">Contact</a>
            </li>
          </ul>
        </nav>
        <aside ref={content1ToReveal}>
          <h6>Lorem ipsum sin ut dolorem</h6>
          <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
        </aside>
      </header>
    </section>
  );
};

The code is simple:

  • first it receive the menuIsOpen from the context hook (useAppState),

  • then we have 3 refs used to reference the 3 container elements to be animated when the menuIsOpen change value.

  • create a paused GSAP Timeline, and memoize it with useMemo.

  • the first useEffect just add to the timeline all the animations to the 3 refs

  • the last useEffect is the core, meaning that runs when menuIsOpen change value, so it just play the animation timeline forward (when opening the menu) or in reverse (to close the menu)

  • the `<ul>` and `<aside>` elements, have the CSS property `overflow: hidden;`, this is necessary to apply the reveal effect, so their children will be animated giving the idea they are in a sort of window.

Services component and the dark/light switch

Content in home page is sectioned in `<article>` elements all with the default height of 100% of the viewport height; in this way when scrolling the home content to the 2 articles, thanks to the GSAP ScrollTrigger plugin, is easy to change the background color to dark (and the foreground to white) with a smooth transition to easy the effect to the eyes.

const Services = () => {
  const servicesRef = useRef();
  const p1Ref = useRef();
  const p2Ref = useRef();
  const p3Ref = useRef();

  useEffect(() => {
    if (servicesRef.current) {
      gsap.fromTo(
        [p1Ref.current, p2Ref.current, p3Ref.current],
        {
          autoAlpha: 0,
          y: '150%'
        },
        {
          duration: 1,
          autoAlpha: 1,
          y: 0,
          stagger: 0.5,
          ease: 'circ.out',
          scrollTrigger: {
            trigger: servicesRef.current,
            start: 'top center', // when the top of the trigger hits the bottom of the viewport
            end: 'bottom center', // end when the bottom of the trigger hits the top of the viewport
            toggleClass: { targets: 'body', className: 'theme-dark' }
            // markers: true
          }
        }
      );
    }
  }, []);

  return (
    <article ref={servicesRef} className={styles.Services}>
      <div>
        <h1>Lorem Ipsum sin ut dolorem</h1>
        <footer>
          <p ref={p1Ref}>Vero omnis sed recusandae voluptas repellat in omnis. Et maiores cumque incidunt voluptatum est.</p>
          <p ref={p2Ref}>Vero omnis sed recusandae voluptas repellat in omnis. Et maiores cumque incidunt voluptatum est.</p>
          <p ref={p3Ref}>Vero omnis sed recusandae voluptas repellat in omnis. Et maiores cumque incidunt voluptatum est.</p>
        </footer>
      </div>
    </article>
  );
};

An additional effect is set to the paragraphs, that on scroll will fade-in and move from the bottom.

That's all for this lab!


Sincerely,

Marcello