Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-laag

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-laag

- 📦 Only 7kb minified & gzipped / tree-shakable / no dependencies - 🛠 We do the positioning, you do the rest. You maintain full control over the look and feel. - 🚀 Optimized for performance / no scroll lag whatsoever - 🏗 Comes with sensible defaults o

  • 2.0.1
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
13K
decreased by-79.05%
Maintainers
1
Weekly downloads
 
Created
Source

react-laag

Hooks for positioning tooltips & popovers



react-laag provides a couple of tools to position UI elements such as tooltips and popovers with ease. It lets you focus on how your UI should look, feel and behave, by taking care of the heavy lifting such as complex calculations you would otherwise have to do yourself.

Try it out for yourself here, or see some examples in our storybook.

Click here for the v1 documentation, or read the release-notes for migrating to v2.

Features

  • 📦 Only 7kb minified & gzipped / tree-shakable / no dependencies
  • 🛠 We do the positioning, you do the rest. You maintain full control over the look and feel.
  • 🚀 Optimized for performance / no scroll lag whatsoever
  • 🏗 Comes with sensible defaults out of the box, but you can tweak things to your liking

Who is this library for?

If you are working on your own UI library / design-system, or just struggling with some complex auto-complete-select component, react-laag might be a match for you. The flexibility react-laag provides has a small price however: you still have to do some work regarding styling and animations yourself. This pattern is also referred to as headless UI.
So, if you're looking for a full-fledged component out-of-the-box, I recommend to check out the wide range of excellent components already out there.


Table of contents


Getting started

Installation

# NPM
npm install react-laag

# Yarn
yarn add react-laag

This library is build with TypeScript, so type-definitions are shipped out-of-the-box.

Quick start

We're only scratching the surface here, but here's a quick example to get some sense what this library feels like and how to get going.

import * React from "react";
import { useLayer, useHover, Arrow } from "react-laag";

function Tooltip({ children, content }) {
  const [isOver, hoverProps] = useHover();

  const {
    triggerProps,
    layerProps,
    arrowProps,
    renderLayer
  } = useLayer({
    isOpen: isOver
  });

  return (
    <>
      <span {...triggerProps} {...hoverProps}>
        {children}
      </span>
      {isOver &&
        renderLayer(
          <div className="tooltip" {...layerProps}>
            {content}
            <Arrow {...arrowProps} />
          </div>
        )}
    </>
  );
}

In order to use this <Tooltip /> component:

const someContent = (
  <div>
    When you hover <Tooltip content="I'm a tooltip!">this</Tooltip> word, you
    should see a tooltip
  </div>
);

API docs

useLayer()

The most important hook for positioning and rendering the layer.

import { useLayer } from "react-laag";
(options: UseLayerOptions): UseLayerProps;
UseLayerOptions
nametyperequireddefaultdescription
isOpenbooleansignals whether the layer is open or closed
overflowContainerbooleantrueshould the layer be contained within the closest scroll-container (false), or is the layer allowed to overflow its closest scroll-container (true)?
placementstring"top-center"preferred placement of the layer. One of: "top-center" / "top-start" / "top-end" / "left-start" / "left-center" / "left-end" / "right-start" / "right-center" / "right-end" / "bottom-start" / "bottom-center" / "bottom-end" / "center"
possiblePlacementsstring[]allin case of auto: true, describes which placements are possible. Default are all placements.
preferX"left" | "right""right"in case of auto: true, when both left and right sides are available, which one is preferred? Note: this option only has effect when placement is "top-*" or "bottom-*".
preferY"top" | "bottom""bottom"in case of auto: true, when both top and bottom sides are available, which one is preferred? Note: this option only has effect when placement is "left-*" or "right-*"
autobooleanfalseshould we switch automatically to a placement that is more visible on the screen?
snapbooleanfalsein case of auto: true, should we stick to the possible placements (true), or should we gradually move between two placements (false)
triggerOffsetnumber0distance in pixels between layer and trigger
containerOffsetnumber10distance in pixels between layer and scroll-containers
arrowOffsetnumber0minimal distance between arrow and edges of layer and trigger
layerDimensions(layerSide: LayerSide): { width: number, height: number }lets you anticipate on the dimensions of the layer. Useful when the dimensions of the layer differ per side, preventing an infinite loop of re-positioning
onDisappear(type: "partial" | "full"): voidgets called when the layer or trigger partially or fully disappears from the screen when the layer is open. If overflowContainer is set to true, it looks at the trigger element. If overflowContainer is set to false, it looks at the layer element.
onOutsideClick(): voidgets called when user clicks somewhere except the trigger or layer when the layer is open
onParentClose(): voidUseful when working with nested layers. It is used by the parent layer to signal child layers that their layers should close also.
containerHTMLElement | (): HTMLElement | stringSpecify in which container (html-element) the layers should mount into when overflowContainer is set to true or when there's no scroll-container found. By default, in such cases the layers are mounted into a generated div#layers which gets attached to the body of the document. This prop accepts various values. When an string is passed, it is interpreted as the id of an element.
triggerobjectThis prop let's you specify information about the trigger you don't know beforehand. This is typically for situations like context-clicks (right-mouse-clicks) and text-selection. By using this prop the returning triggerProps of this hook will have no effect.
getBounds: () => ClientRectA callback function that returns the bounds of the trigger.
getParent?: () => HTMLElementA callback function that returns the parent element. This is optional but may be needed in cases where you'll want to prevent overflow of the layer. In other words, if you use the default option overflowContainer: true, this callback will have no effect. The returning element is used to position the layer relatively and to register event-listeners.
environmentWindowwindowuseful when working with i-frames for instance, when things like event-listeners should be attached to another context (environment).
ResizeObserverResizeObserverClasspass a polyfill when the browser does not support ResizeObserver out of the box
UseLayerProps
nametyperequireddescription
triggerPropsobject✔ (unless the trigger-option is used)Spread these props on the trigger-element
ref: () => voidObtains a reference to the trigger-element
layerPropsobjectSpread these props on the layer-element
ref: () => voidObtains a reference to the layer-element
style: CSSPropertiesstyle-object containing positional styles
arrowPropsobjectSpread these props on the arrow-component
ref: () => voidObtains a reference to the arrow-element
style: CSSPropertiesstyle-object containing positional styles
layerSide: LayerSidelet the arrow-component know in which direction it should point
renderLayer(children: ReactNode) => ReactPortalRender the layer inside this function. Essentially, this is a wrapper around createPortal()
layerSide"top" | "bottom" | "right" | "left"The side the layer is currently on relative to the trigger
triggerBoundsClientRect | nullBounds of the trigger when isOpen: true. Useful when sizing the layer relatively to the trigger.

useHover()

Utility hook for managing hover behavior.

import { useHover } from "react-laag";
(options?: UseHoverOptions): [boolean, UseHoverProps, () => void];

Example usage

const [
  isOver, // should we show the layer?
  hoverProps, // spread these props to the trigger-element
  close // optional callback to set `isOver` to `false`
] = useHover({
  delayEnter: 300, // wait 300ms before showing
  delayLeave: 300, // wait 300ms before leaving
  hideOnScroll: true // hide layer immediately when user starts scrolling
});
UseHoverOptions
nametyperequireddefaultdescription
delayEnternumber0delay in ms
delayLeavenumber0delay in ms
hideOnScrollbooleantruesets hovering to false when user starts scrolling
UseHoverProps
nametype
onMouseEnter() => void
onMouseLeave() => void
onTouchStart() => void
onTouchMove() => void
onTouchEnd() => void

<Arrow />

import { Arrow } from "react-laag";

<Arrow
  angle={45}
  size={8}
  roundness={0}
  borderWidth={0}
  borderColor="#000"
  backgroundColor="#FFF"
  layerSide="top"
/>;

<Arrow /> is basically just an regular svg-element, so it will accept all default svg-props as well

ArrowProps
nametyperequireddefaultdescription
anglenumber45Angle of the triangle in degrees. A smaller angle means a more 'pointy' arrow.
sizenumber8distance in pixels between point of triangle and layer.
roundnessnumber0Roundness of the point of the arrow. Range between 0 and 1.
borderWidthnumber0Width of the border in pixels
borderColorstring"black"Color of the border
backgroundColorstring"white"Color of the arrow
layerSidestring"top"Determines where to arrow should point to

useMousePositionAsTrigger()

Utility hook that lets you use the mouse-position as source of the trigger. This is useful in scenario's like context-menu's.

import { useMousePositionAsTrigger } from "react-laag";
(options?: UseMousePositionAsTriggerOptions): UseMousePositionAsTriggerProps;

type UseMousePositionAsTriggerProps = {
  hasMousePosition: boolean;
  resetMousePosition: () => void;
  handleMouseEvent: (evt: MouseEvent) => void;
  trigger: {
    getBounds: () => ClientRect;
    getParent?: () => HTMLElement;
  };
  parentRef: MutableRefObject;
};

Example usage

function ContextMenu() {
  const {
    hasMousePosition,
    handleMouseEvent,
    resetMousePosition,
    trigger
  } = useMousePositionAsTrigger();

  const { layerProps, renderLayer } = useLayer({
    isOpen: hasMousePosition,
    onOutsideClick: resetMousePosition,
    trigger
  });

  return (
    <>
      <div onContextMenu={handleMouseEvent}>Right-click to show the layer</div>
      {hasMousePosition && renderLayer(<div>Layer</div>)}
    </>
  );
}

See the context-menu example or text-selection example for more info.

UseMousePositionAsTriggerOptions
nametyperequireddefaultdescription
enablednumbertrueShould the mouse-position currently be actively tracked?
preventDefaultbooleantrueShould handleMouseEvent preventDefault()?

mergeRefs()

Utility function that lets you assign multiple references to a 'ref' prop.

import * as React from "react";
import { mergeRefs } from "react-laag";

const ref1 = React.useRef();
const ref2 = element => console.log(element);

<div ref={mergeRefs(ref1, ref2)} />;

Concepts

Relative positioning

react-laag allows you to use to methods or modes for positioning with help of the overflowContainer option in useLayer(). When using overflowContainer: true, which is the default behavior, the layer is mounted somewhere high in the document in its own container. In such a case, the position of the layer will be fixed, meaning that it will be positioned relative to the window.
On the other hand, you can decide you don't want to overflow the container by setting overflowContainer to false. In this scenario the layer will be mounted right under the scroll-container.
So, what do we mean by the term 'scroll-container' anyways? react-laag considers a scroll-container an element which has set the overflow, overflow-x or overflow-y style to one of "auto" or "scroll". react-laag tries to find these scrollable-containers by traversing up the dom-tree, starting with the trigger-element. This way, the layer will be positioned relatively to the closest scroll-container. There's one catch though: it expects you to set the position: relative style on this scroll-container. If you accidentally forgot to set this style, react-laag will output a friendly warning in the console.

Placement priority

This usually is something you don't have to think about, but in some cases it may come in handy.
When setting the auto option to true in useLayer(), react-laag will create an priority-order under the hood. The preferred placement will always be on top of the list, meaning this placement will be tried first. To determine the placements after that, react-laag looks at the following things:

  • the preferred placement for determining the preferred direction / axis. When using "top-start" for instance, we can assume that although this exact placement may not fit, somewhere on top is still preferred. This direction / axis will have more priority over preferX / preferY.
  • preferX / preferY for determining priority on the opposite axis regarding the preferred placement.
  • The next placement in line must always be as close to the previous placement as possible.
  • placements which are not defined in possiblePlacements (all by default) are skipped

Let look at an example given placement "top-start" with a preferX of "right":

top-start -> top-center -> top-end -> right-end -> left-end -> right-center -> left-center -> right-start -> left-start -> bottom-start -> bottom-center -> bottom-end

During rendering react-laag will given the list containing priorities...

  • try to find the first placement in line that fits the current screen / layout
  • if none fits, it will find the placement with the most visible surface

Nesting

Nesting multiple layer often occurs in large menu's where items are grouped. If you're looking for an example of how to accomplish this, be sure to check out the example about nesting.

There are however some important things to consider. How do we for instance signal to the rest of the nested layers, that a layer higher up in the hierarchy has just closed? Fortunately, there's a special option for that in the userLayer() options: onParentClose. react-laag uses context under the hood to monitor which layers are related to each other. This has a couple of implications:

  • When a layer closes, it will signal child-layers below to close as well through onParentClose.
  • onOutsideClick only has effect on root-layer. react-laag has a kind of event-bubbling system under the hood to make sure that the root-layer doesn't close when some child-layer down below was clicked. When there was a solid click outside somewhere in the document, the root-layer will signal the rest of the layers to close as well.

Animations

react-laag doesn't do any animations for you. Why? Because we want to focus this library purely on positioning and there are a lot of libraries out there who do a far better job than react-laag could ever do.
Since renderLayer is just an abstraction over React's createPortal you can in theory use any form of animation you'd like. Personally, I'm a big fan of framer-motion, so I will show you a quick example to get started:

import { useLayer } from "react-laag";
import { motion, AnimatePresence } from "framer-motion";

function AnimationExample() {
  const [isOpen, setOpen] = React.useState(false);

  const { renderLayer, triggerProps, layerProps } = useLayer({ isOpen });

  return (
    <>
      <button {...triggerProps} onClick={() => setOpen(!isOpen)}>
        Click me!
      </button>
      {renderLayer(
        <AnimatePresence>
          {isOpen && (
            <motion.div
              {...layerProps}
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            >
              Layer
            </motion.div>
          )}
        </AnimatePresence>
      )}
    </>
  );
}

z-index / container

By design react-laag doesn't handle any z-indexes for you. There are too many different use-cases and scenario's possible for this library to manage. You are free to implement your own z-index strategy. However, there is a cheap fix that will probably fix 95% of your problems.
By default, react-laag renders your layers in a container right under the document's body:

<body>
  <!-- React's entry -->
  <div id="root"></div>

  <!-- By default all layers will we rendered here -->
  <div id="layers"></div>
</body>

Now, nothing is stopping you to do this:

#layers {
  z-index: 1000;
}

All layers will now automatically inherent the z-index of this container.

If you want react-laag the mount the layers into another element, you have two options:

  • use the container option in useLayer():
const {} = useLayer({
  // pass in an id of the element
  container: "my-own-container-id",

  // pass in a callback returning an html-element
  container: () => myContainer,

  // pass in a html-element directly
  container: myContainer
});
  • set the container globally with setGlobalContainer():
// somewhere in the root of your application
import { setGlobalContainer } from "react-laag";

// works the same as the container-option above
setGlobalContainer("my-own-container-id");

Resize observer

If you want to take full advantage of react-laag's positioning change detection, make sure your target browser(s) support ResizeObserver. To get a detailed list which browsers support this feature consult Can I use. As of now, this sort of means all modern browsers except IE 11. If you need to support IE 11 you can optionally provide your app with a polyfill. If you don't want to pollute the global context you can also pass in the polyfill via the option in useLayer:

import ResizeObserver from "resize-observer-polyfill";
import { useLayer } from "react-laag";

useLayer({ ResizeObserver });

FAQ

Is there support for accessability?

No, unfortunately not. There are two primary reasons:

  • This library is primary focussed around positioning
  • Accessability not my area of expertise

I'm open to the idea in the future though. I would be happy to get some help with this!

Which browsers are supported?

react-laag works on all modern browsers. Is should also work in >= IE 11, although this may require a polyfill for stable-features

Will this work with server-side-rendering?

Yes, each build a small tests gets run in other to test compatibility.

Contributing

Want to contribute to react-laag? Your help is very much appreciated! Please consult the contribution guide how to get started.

License

MIT © everweij

FAQs

Package last updated on 16 Dec 2020

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc