TIL

Floating UI - Coordinate System

文章發表於
...

Introduction

Ever wondered how libraries like Floating UI know exactly where to place a tooltip? It all comes down to understanding the browser's coordinate system and some clever math. In this article, we'll break down the positioning logic step by step, from basic rectangles to the complete computeCoordsFromPlacement function.

Floating UI uses the browser's coordinate system, where the origin (0, 0) is at the top-left corner of the viewport. Let's start by understanding how a single rectangle is defined:

interface Rect {
x: number;
y: number;
width: number;
height: number;
}
  • x/y – X/Y-coordinates of the rectangle origin relative to window,
  • width/height – width/height of the rectangle (can be negative).

As long as we know the XY coordinates and the element's height and width, we can derive the properties below.

  • top/bottom – Y-coordinate for the top/bottom rectangle edge,
  • left/right – X-coordinate for the left/right rectangle edge.

The diagram above illustrates these relationships visually. With this foundation, the following calculations should be straightforward:

left = x
top = y
right = x + width
bottom = y + height
centerX = x + width / 2
centerY = y + height / 2`

The Two Elements

There are two elements in the floating UI world: reference, which is the anchor element (button, link, etc.), and floating, which is the positioned element (tooltip, dropdown, etc.).

Once we know how to identify the positions of the two elements, we can start calculating the relationship between them.

Placement Types

Sides

There are 4 possible sides where the floating element can be positioned:

type Side = 'top' | 'right' | 'bottom' | 'left';

Alignments

There are 2 alignment options that determine how the floating element aligns along the reference edge:

type Alignment = 'start' | 'end';

Combined Placements

When you combine a side with an alignment (or leave it centered by default), you get the 12 possible placements:

type Placement =
| 'top' | 'top-start' | 'top-end'
| 'right' | 'right-start' | 'right-end'
| 'bottom' | 'bottom-start' | 'bottom-end'
| 'left' | 'left-start' | 'left-end';

Axes Explained

The Side Axis (The "Attachment" Axis)

This axis is perpendicular to the edge of the reference element. It determines how far away the floating element is from the anchor.

  • For top / bottom: The Side Axis is y. You change the y coordinate to move the tooltip further up or down from the button.
  • For left / right: The Side Axis is x. You change the x coordinate to move the tooltip further left or right.

The Alignment Axis (The "Sliding" Axis)

This axis is parallel to the edge of the reference element. It determines where the element "slides" along that edge to satisfy start, center, or end.

  • For top / bottom: The Alignment Axis is x. The tooltip slides left or right to align its corner or center with the button.
  • For left / right: The Alignment Axis is y. The tooltip slides up or down to align with the button's height.
function getSide(placement: Placement): Side {
return placement.split('-')[0];
}
function getSideAxis(placement: Placement): Axis {
const side = getSide(placement); // 'top' | 'right' | 'bottom' | 'left'
return (side === 'top' || side === 'bottom') ? 'y' : 'x';
}
function getAlignmentAxis(placement: Placement): Axis {
return getOppositeAxis(getSideAxis(placement));
}
function getOppositeAxis(axis: Axis): Axis {
return axis === 'x' ? 'y' : 'x';
}
function getAxisLength(axis: Axis): 'width' | 'height' {
return axis === 'x' ? 'width' : 'height';
}

The Math - computeCoordsFromPlacement

When you say placement: 'bottom', you're telling Floating UI: "Put the floating element below the reference." But what does "below" actually mean in terms of x and y coordinates? This is what computeCoordsFromPlacement figures out. Let's break it down step by step.

For any placement, we need to answer:

  • Where does the floating element's LEFT edge go? (the x coordinate)
  • Where does the floating element's TOP edge go? (the y coordinate)

Calculate Center Points

Before we position by side, we calculate where the floating element would be if it were perfectly centered on the reference:

// Center horizontally relative to reference
const commonX = reference.x + reference.width / 2 - floating.width / 2;
// Center vertically relative to reference
const commonY = reference.y + reference.height / 2 - floating.height / 2;

So what does this formula actually mean?

Step 1: Start at the Left Edge

As mentioned above, every positioned element in the browser has an x property that represents its left edge. When we write ref.x, we're starting at the button's left edge. Think of this as our starting line. We know where the button begins, and that's our anchor point.

Step 2: Move to the Center Line

Now we need to find the center of the button. To do this, we add half of the button's width: + (ref.width / 2). If a button starts at 300px and is 200px wide, its center is at 400px (300 + 100). This gives us the vertical center line where both elements should align.

Step 3: Back Up by Half the Tooltip Width

Here's the crucial step that trips people up. If we placed our tooltip starting at the center line, it would be off-center—the tooltip's left edge would be at the center, pushing the whole tooltip too far right.

We need to back up by half the tooltip's width: - (float.width / 2). This "backs up" the tooltip so that its center (not its left edge) sits on the center line.

The formula essentially says: "Find the reference center, then offset backwards by half the floating element's width." This ensures both centers align perfectly.

Position by Side

Once we know the starting point for the floating element in x, we place the element above the reference and center it horizontally.

case 'top':
coords = {
x: commonX, // Use centered x
y: reference.y - floating.height // Position above
};

The y calculation explained:

We want floating's BOTTOM edge to touch reference's TOP edge. But we set the TOP edge (y coordinate), not the bottom. So: floating.y = reference.y - floating.height

┌─────────────────┐ ◄── floating.y = ref.y - float.height
│ │ = 100 - 60 = 40
│ FLOATING │
│ (height: 60) │
└─────────────────┘ ◄── floating bottom = 40 + 60 = 100
┌─────────────────┐ ◄── reference.y = 100
│ REFERENCE │
└─────────────────┘

The floating element's bottom (100) meets reference's top (100)

Calculate Alignment Offset

This step we will need to know how far to shift for start/end alignment

const alignLength = getAxisLength(alignmentAxis); // If it's top/bottom placements, it returns "width" otherwise returns "height"
const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2;

What is commonAlign?

For horizontal alignment (top/bottom placements):

reference.width / 2 - floating.width / 2
  • If the reference wider than floating, the common align will be positive value.
  • If the floating wider than reference, the common align will be negative value.

Apply Alignment

const alignment = getAlignment(placement); // 'start' | 'end' | undefined
const alignmentAxis = getAlignmentAxis(placement);
const isVertical = sideAxis === 'y'; // true for top/bottom
switch (alignment) {
case 'start':
coords[alignmentAxis] -= commonAlign;
break;
case 'end':
coords[alignmentAxis] += commonAlign;
break;
// default (center): no adjustment needed
}
  • 'start' alignment: Align floating's start edge with reference's start edge
  • 'end' alignment: Align floating's end edge with reference's end edge

If the placement is top, for start alignment, subtract commonAlign from x, coords.x -= commonAlign, and for end, add commonAlign from x, coords.x -= commonAlign.

Final Formula Table

Here's the complete reference for all placement calculations:

Placementxy
topref.x + ref.w/2 - float.w/2ref.y - float.h
top-startref.xref.y - float.h
top-endref.x + ref.w - float.wref.y - float.h
bottomref.x + ref.w/2 - float.w/2ref.y + ref.h
bottom-startref.xref.y + ref.h
bottom-endref.x + ref.w - float.wref.y + ref.h
leftref.x - float.wref.y + ref.h/2 - float.h/2
left-startref.x - float.wref.y
left-endref.x - float.wref.y + ref.h - float.h
rightref.x + ref.wref.y + ref.h/2 - float.h/2
right-startref.x + ref.wref.y
right-endref.x + ref.wref.y + ref.h - float.h

Conclusion

Understanding Floating UI's coordinate system is fundamental to mastering tooltip and popup positioning. Let's recap the key concepts:

  1. The Coordinate System: Browser coordinates start at the top-left corner (0, 0), with X increasing rightward and Y increasing downward.

  2. Two Elements: Every floating interaction involves a reference element (the anchor) and a floating element (the positioned content).

  3. Placements: There are 12 placements combining 4 sides (top, right, bottom, left) with alignments (start, end, or centered by default).

  4. Two Axes:

    • The Side Axis determines the distance between elements
    • The Alignment Axis determines where the floating element slides along the reference edge
  5. The Centering Formula: ref.x + ref.width/2 - float.width/2 ensures perfect center alignment by finding the reference center and then backing up by half the floating element's size.

With this foundation, you're now equipped to debug positioning issues, customize Floating UI's behavior, or even build your own positioning logic. The next time a tooltip appears in the wrong place, you'll know exactly which axis and calculation to investigate!

Playground

Now that you understand the math behind Floating UI's positioning system, it's time to put that knowledge into practice! Use the interactive playground below to experiment with different placements and dimensions. Try adjusting the reference and floating element sizes to see how the coordinates change in real-time. Pay attention to how the Side Axis and Align Axis indicators update as you switch between placements, this will help solidify your understanding of how the two axes work together.

If you enjoyed this article, please click the buttons below to share it with more people. Your support means a lot to me as a writer.
Share on X
Buy me a coffee