Exploring Infinite Canvas: My Journey & Insights

Let me show you how you can make infinite canvas with only 2 HTML elements

Today I’m exploring infinite canvas. Here’s a digest of what you’ll get after reading this post:

  1. An infinite canvas of your own

  2. My take on how to reverse-engineer some of web apps’ cool features

Background

I’ve been fascinated by design tools like framer and figma for the canvas (not canvas tag in HTML) that the user is able to interact with and edit objects (frames, images, shapes, text) inside that canvas. Today, we’re going to explore Framer’s infinite canvas.

The UI of Framer showcases an editable canvas in the center.

In this canvas, you can perform wheel gestures and move the frames inside the canvas with your mouse. This type of canvas is usually called an infinite canvas

A couple months back, I was working on Typedream (acq by Beehiiv) which is a website builder similar to Framer. However, we did not have this functionality early on and postponed the development of this for a while.

Now, I have more time to re-focus building the website builder on Beehiiv and decided to give it a shot. Most of these things were unknown to me just a few weeks back, but let’s dive into the unknown!

How to figure out the inner working of web apps

The first step is to find out how things are currently implemented. Here’s how I dive deep into how Framer did theirs.

Step 1: Play with the product

Strictly focus on the product you want to replicate. Figure out what it can or can’t do. Revisit this step often because you will find new observations over time.

Step 2: Inspect & find HTML element

All things on the web consist of HTML, JavaScript, and CSS. No matter the framework, it all boils down to these three core elements. I usually inspect the final result (HTML, CSS) and try to reverse-engineer the JavaScript.

Open your inspect elements tab, go to the elements tab, and find the specific HTML element you are interested in. In my case, it’s the moving canvas.

Finding HTML element that changes when we move objects in canvas

Step 3: Reverse engineer

I ask myself: “How can I achieve this HTML & CSS behavior using <insert-framework>?”

Frameworks can be core web app libraries like remix, next.js, react and/or it can be the more specific gesture or dnd libraries like use-gesture/react , react-dnd , etc.

My take on how it is implemented

The core to this infinite canvas behavior is the combination of CSS translate and event listeners (MouseEvent or KeyboardEvent)

Step 1: Setup the elements

For demonstration purposes, you really only need 2 element:

  1. The canvas window, referred as main with id: infinite-canvas

  2. The canvas object, referred as div with id: object-to-move

Here’s the code snippet:

    // This canvas will be scrollable
    <main
      id="infinite-canvas"
      style={{ width: "100dvw", height: "100dvh", overscrollBehavior: "none" }}
    >
      {/* Second: setup object to move */}
      <div
        id="object-to-move"
        style={{
          position: "absolute",
          backgroundColor: "red",
          height: "300px",
          width: "300px",
          placeContent: "center",
        }}
      >
        <p style={{ textAlign: "center" }}>Scroll to move or pinch to zoom</p>
      </div>
    </main>

Step 2: Add in necessary CSS for movement of main canvas frame

Similar to how you have a single frame you can edit in Framer, you attach a little bit of CSS combined with some Javascript

    // This canvas will be scrollable
    <main 
      ...
    >
      <div
        id="object-to-move"
        style={{
          ... the rest of the styles
          transform: `scale(${scale}) translate(${x}px, ${y}px)`, // 👈 Add this line!
        }}
      >
        ...
      </div>
    </main>

Note that scale , x ,and y are variables that we haven’t defined yet.

Step 3: Define event listeners & handlers

We now need to know what events to listen to. There are 2 events I care about:

  1. When user scroll using touch pad

  2. When user pinch (for zooming in or zooming out)

You can add more event listener and keyboard shortcut as you like to modify how the infinite canvas to react on user input.

I’m going to cheat a little bit here and use a library called @use-gesture/react (docs) to help me with attaching listeners I need

Here’s the setup:

  const gestureRef = useRef<HTMLElement>(null);

  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  const [scale, setScale] = useState(1);

  useGesture(
    {
      // Here's where I define event listener 
      // for scroll behavior
      onWheel: (event) => {
        // requestAnimationFrame for performance!
        requestAnimationFrame(() => {
          // We're setting the x & y transform
          // based on the distance user has
          // travelled using their trackpad/scrollwheel
          setX((prev) => prev - event.delta[0]);
          setY((prev) => prev - event.delta[1]);
        });
      },
      // Define pinch event listener here
      onPinch: ({ offset }) => {
        requestAnimationFrame(() => {
          // Zoom in or out based on the offset size
          // try console logging the values provided by
          // the listener!
          setScale(offset[0]);
        });
      },
    },
    {
      // TODO: where should we attach the handlers?
      target: gestureRef,
    },
  );

I’m using react to render my component. You can do it with vanilla javascript, but I’m just so used to react ecosystem so I’m going to use them for simplicity.

Step 4: Attach listeners/handlers

Final step is to attach the event listeners.

    // This canvas will be scrollable
    <main
      ref={gestureRef} // 👈 Add the gestureRef here
      id="infinite-canvas"
      style={{ width: "100dvw", height: "100dvh", overscrollBehavior: "none" }}
    >

Final Result

Here’s the final look. It’s far from being as polished as Framer’s. But It does the job.

Hope you learned a thing or two. Cheers 🍻

Suggestion

Appreciate you reading up to this point, I would love to get your thoughts!

If you enjoy this post, consider subscribing to my newsletter!