Centering rows around a specific element using React

Be your own layout engine

Centering rows around a specific element using React

(You can view the source code for this example and try it out yourself in this Replit)

Let's say we have a DOM structure like this:

<Row>
  <A>A</A>
  <B>B</B>
  <C>C</C>
</Row>

image.png

(vertical line added to illustrate the center of the page)

How do we make it such that all of the all of the elements in the row are centered around <B>?

You might try a naive approach - just center all of the elements of the row!

const Row = styled.div`
  width: 100%;
  display: flex;
  flex-direction: row;
  justify-content: center;
`

image.png

This approach would work if <A>, <B>, and <C> shared the same width. However, the layout engine will center the row as a whole, and the middle of that entire row doesn't fall in the middle of <B>. Ideally, we would have the layout engine ignore <A> and <C> when computing how to center the <Row>

Fortunately, there is a way for us to have the layout engine ignore an element when aligning a document! That way is position: absolute (MDN docs):

const A = styled(Box)`
  width: 100px;
  background-color: blue;
  position: absolute;
`;

const B = styled(Box)`
  width: 80px;
  background-color: green;
`;

const C = styled(Box)`
  width: 60px;
  background-color: red;
  position: absolute;
`;

image.png

Okay, new problem. We have successfully centered the <Row> around <B>, but the layout engine now doesn't know where to put <A> and <C>! In order to horizontally position <A> and <C>, we're going to need to do some of the work of the layout engine. This is where React comes in.

There are two React APIs that we're going to make use of:

  1. useRef, which will give us references to all of the elements we want to use.
  2. useLayoutEffect, which allows us to change the positions of <A> and <C> whenever needed.

First, we'll take our references:

export default function App() {
  const aRef = useRef<HTMLDivElement>(null);
  const bRef = useRef<HTMLDivElement>(null);
  const cRef = useRef<HTMLDivElement>(null);

  return <Page className="app">
    <VerticalLine />
    <Content>
      <Row>
        <A ref={aRef}>A</A>
        <B ref={bRef}>B</B>
        <C ref={cRef}>C</C>
      </Row>
    </Content>
  </Page>;
}

Next, inside of App(), we'll install our listener to useLayoutEffect (credit to Joe Purnell for this technique):

  function updatePosition() {
    /** TODO */
  }

  useLayoutEffect(() => {
    window.addEventListener("resize", updatePosition);
    updatePosition();
    return () => window.removeEventListener("resize", updatePosition);
  }, []);

Now that we have the ability to move each element (useRef), and the event listener for when to move each element (useLayoutEffect), we just need to know where to move each element.

Inside of updatePosition(), we want to move the right edge of <A> to the left edge of <B>, and the left edge of <C> to right edge of <B>:

function updatePosition() {
    const leftEdgeOfB =
      bRef.current!.getBoundingClientRect().left +
      bRef.current!.getBoundingClientRect().width;
      // I'm not sure why the width needs to be added 🤷
    aRef.current!.style.right = `${leftEdgeOfB}px`;

    const rightEdgeOfB =
      bRef.current!.getBoundingClientRect().right;
    cRef.current!.style.left = `${rightEdgeOfB}px`;
}

image.png

Done! Now <A> and <C> will always be properly aligned, even if the DOM moves around while the page is loaded.

The source code for this example is available here. To see this technique in action (at time of writing), check out my website, scowalt.com.

Did you find this article valuable?

Support Scott's development by becoming a sponsor. Any amount is appreciated!