Introducing react-laag

07 October, 20195 min to read

I haven't posted anything in a while, because lately I have been working hard on a new side-project of mine. Today I'm excited to announce that it is officially released! Everybody, meet react-laag 👋🏻🎉.

laag... what?!

Yeah, bit of a weird name, isn't it? Well, laag means layer in Dutch, and since 'react-layer' was already taken and people are naming stuff in their native language anyways (think Zustand / Immer), I decided to go with react-laag. But enough about the name... what is it good for?

react-laag exposes a primitive to build things like tooltips, dropdown menu's and pop-overs in React. Basically any kind of layer that can be toggled.

The keyword here is primitive. It's not a library with components that can be instantly used in your app. It's more in the spirit of tools like react-beautiful-dnd and downshift, where a lot of work is being done for you. In other words, you have to code the last 25%. I know there are already a great number of libraries on NPM that cover things like tooltips and popovers, so why put energy in something incomplete? Well, it's all about control.

The reason I started working on this package, is because it's sometimes really hard to make a 3rd party component fit into your own / company's style. And that's a shame. One of React's selling point is to compose and reuse building blocks, but thousands of people trying to build the same thing over and over again doesn't fit that philosophy, right? I find that most people are building their components from scratch, because they want to control how their components look and behave.

react-laag tries to do just that: you focus on what your layer (tooltip) should look like and behave, and let react-laag take care of where and when to show it.

Features

  • Build your own tooltips / dropdown-menu's / pop-overs / etc...
  • Not opinionated regarding styling or animations
  • Highly customizable
  • Only ~5kb minified and gzipped
  • Zero dependencies
  • Built with typescript / ships with typescript definitions
  • Integrates well with other libraries
  • Automatically adjusts your layer's placement to fit the screen
  • Works with nested scroll-containers
  • Observes and reacts to changes in dimensions

Show me something!

Alright, let's start by building a simple tooltip component. This is what we are gonna build:

We'll be using styled-components for styling, and framer-motion for animations.

In this example we want to wrap text in a span-element and enhance it by showing a tooltip when the user hovers their mouse. This is what we will end up with:

<Tooltip text="Tooltip text here">wrapped text here</Tooltip>

Let's start with the tooltip component:

import * as React from "react";
import styled from "styled-components";
import { motion } from "framer-motion";

// create a styled-component out of a `motion.div`
const TooltipBox = styled(motion.div)`
  background-color: rgba(0, 0, 0, 0.8);
  border: 1px solid black;
  color: white;
  font-size: 12px;
  padding: 3px 12px;
  border-radius: 3px;
  transform-origin: center center;
  z-index: 999;
`;

Nothing special here, just a div with a dark background and some padding. For the text that is being enhanced, we want some kind of visual queue, so the user knows which text has a tooltip or not.

const TooltipText = styled.span`
  border-bottom: 1px dotted #1a73a7;
  color: #1a73a7;
  cursor: help;
`;

We also need the basis of our Tooltip component:

function Tooltip({ children, text }) {
  return <div />;
}

We don't want to immediately show the tooltip when the user hovers their mouse, because that can be a bit annoying. Instead we want to wait a bit, and only when the user is still hovering after a certain timeout, we will show the tooltip. Because this is a common behavior with tooltips, react-laag exposes a hook that implements this behavior:

import { useHover } from "react-laag";

function Tooltip() {
  const [show, hoverProps] = useHover({ delayEnter: 300, delayLeave: 200 });
}

Connecting things together

Here comes the interesting part: connecting the pieces together with the help of react-laag:

function Tooltip({ children, text }) {
  const [show, hoverProps] = useHover({ delayEnter: 300, delayLeave: 200 });

  return (
    <ToggleLayer
      // tell when to show the tooltip
      isOpen={show}
      // make the tooltip fixed positioned
      fixed
      // prefer to place the tooltip above the text, but adjust when necessary
      // we also want some space between the wrapped text and tooltip
      placement={{ anchor: "TOP_CENTER", autoAdjust: true, triggerOffset: 4 }}
      // rendering the tooltip
      renderLayer={({ isOpen, layerProps }) => {
        return isOpen ? <TooltipBox {...layerProps}>{text}</TooltipBox> : null;
      }}
    >
      {({ triggerRef }) => (
        // `triggerRef` is the only required prop to pass through.
        // It is used to calculate things like positions
        // `hoverProps` help in determining when to show the tooltip
        <TooltipText ref={triggerRef} {...hoverProps}>
          {children}
        </TooltipText>
      )}
    </ToggleLayer>
  );
}

Animations

In order to make the appearance / disappearance of the tooltip more smooth we could add some animations. I really like framer-motion for stuff like this:

import { motion, AnimatePresence } from "framer-motion";
function Tooltip({ children, text }) {
  const [show, hoverProps] = useHover({ delayEnter: 300, delayLeave: 200 });

  return (
    <ToggleLayer
      isOpen={show}
      fixed
      placement={{ anchor: "TOP_CENTER", autoAdjust: true, triggerOffset: 4 }}
      renderLayer={({ isOpen, layerProps, layerSide }) => {
        return (
          // we need `AnimatePresence` to track when the tooltip enters and leaves          <AnimatePresence>            {isOpen && (              <TooltipBox                {...layerProps}                // provide config for animated styles                initial={{                  opacity: 0,                  scale: 0.8,                  y: layerSide === "top" ? -8 : 8,                }}                animate={{ opacity: 1, scale: 1, y: 0 }}                exit={{                  opacity: 0,                  scale: 0.8,                  y: layerSide === "top" ? -8 : 8,                }}                transition={{                  type: "spring",                  damping: 30,                  stiffness: 500,                }}              >                {text}              </TooltipBox>            )}          </AnimatePresence>        );
      }}
    >
      {({ triggerRef }) => (
        <TooltipText ref={triggerRef} {...hoverProps}>
          {children}
        </TooltipText>
      )}
    </ToggleLayer>
  );
}

But there's more

We've only scratched the surface of what react-laag can do. To get an idea, here's an example of an input together with a layer that gives hints about how secure a password is.

Learn more about the project on Github, or check out the docs.

Follow me on Twitter to receive the latest updates!