Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
import {useLayoutEffect, useState} from 'react';
* React Hook to measure a DOM element and set its `position` depending on
* whether it would overflow left or right.
* Uses useLayoutEffect for batched DOM measurements and painting.
* @param toggle a boolean on whether to run the effect. Runs cleanup when it changes.
* You might want a toggle if the element is always present in the DOM, and its display toggled.
export function useLeftRightAutoPosition(toggle: boolean) {
// Create a callback ref, so that we get notified about changes to it (React will call it when setting it)
// @see
// This is because mutable refs do not notify us of changes; useCardRedundantClickArea does similar things
// Note that unlike the example in the docs, we need access to the node, so we useState for this
const [node, setNode] = useState<HTMLElement | null>(null);
() => {
if (node !== null && toggle === true) {
// NOTE: These are all expensive calculations, so be careful when touching them
// The good thing about useLayoutEffect is that it will run before React flushes to the DOM,
// so we have time to measure and "batch" paints.
// Want to learn more about DOM measurements?
// @see
const boundingRect = node.getBoundingClientRect();
const hasLeftOverflow = boundingRect.left < 0;
const hasRightOverflow =
boundingRect.right > (window.innerWidth || document.documentElement.clientWidth);
if (hasLeftOverflow && hasRightOverflow) {
// TODO: What do we do here? Is this even possible?
} else {
// If there's a left overflow, put it flowing to the right
if (hasLeftOverflow) { = '0';
// If there's a right overflow, put it flowing to the left
if (hasRightOverflow) { = '0';
} else {
// If there is no left or right overflow, then defer to the browser by setting nothing
// This might seem counter-intuitive, but the browser knows better about where to position things
// Simplifying a bit (not talking about margins), it will be left: 0 on ltr layouts or right: 0 on rtl layouts
// We'd rather not do that math here though :)
// @see
return () => { = null; = null;
[node, toggle],
return setNode;

In real use, here's what it could look like:

const UserIndicatorMenu = ({user, logout, translations}) => {
  const {on: isMenuOpen, toggle: toggleMenu} = useToggle({});

  // Decide whether the menu should be positioned left or right, depending on overflow
  const menuRef = useLeftRightAutoPosition(isMenuOpen);

  return (
    <Box position="relative">
      <button aria-expanded={isMenuOpen} onClick={toggleMenu} className={`${buttonCls} silver-40`}>
        {/* Use margin+negative margin for pseudo- gap */}
        <Flex mn="1" alignItems="center" flexWrap="wrap-reverse">
          <Box ma="1">
            <BlockText ma="0">{`${user.get('firstName')} ${user.get('lastName')}`}</BlockText>
            // Hide the alt, because the name is displayed already
            src={getProfileImagePlay(user, 'tiny')}
            className="db w2 ht2 br-100 ma1"
        display={isMenuOpen ? 'block' : 'none'}
        extraClassName="top-100 z-nav-popout"
        <ul className="pv2 vs3">
            <SimpleLink onClickInternal={toggleMenu} className={linkCls} href="/change-password">
            <button className={`${buttonCls} ph3 silver-10`} onClick={logout}>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment