# Flex/Grid Over JS Measurement

**SHOULD** · **ID:** `layout-flex-over-measurement` · **Category:** layout
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/layout-flex-over-measurement

> SHOULD: Solve layout with flex and grid instead of reading `getBoundingClientRect`/`offsetWidth` and writing the result back as inline styles — measurement forces a synchronous reflow, can only run after first paint (so the UI visibly jumps), is wrong during SSR, and goes stale without a `ResizeObserver`.

Solve layout with flex and grid instead of measuring elements and positioning them in JavaScript

Reading getBoundingClientRect or offsetWidth forces a synchronous style and layout recalculation, and writing the result back into inline styles triggers another one — layout thrash. Worse, the measurement can only run after the first paint, so the UI visibly jumps into place, is wrong during SSR and before hydration, and goes stale the moment the content or the container changes unless you also wire up a ResizeObserver. Flex and grid express the same intent declaratively: the browser solves it during its own layout pass, correct on the first paint and on every resize.

## Bad — do not do this

`layout-flex-over-measurement-bad`

```tsx
import { useEffect, useRef, useState } from 'react';

const MOUNT_PCT = 62;

export function FlexOverMeasurementBad() {
  const barRef = useRef<HTMLDivElement>(null);
  const pillRef = useRef<HTMLSpanElement>(null);
  const [left, setLeft] = useState<number | null>(null);
  const [pct, setPct] = useState(MOUNT_PCT);

  // Measure-then-position. It runs once, positioning the label at the mount-time
  // value, and never re-runs when the value (or container) changes.
  useEffect(() => {
    const bar = barRef.current;
    const pill = pillRef.current;
    if (!bar || !pill) return;
    const barWidth = bar.getBoundingClientRect().width;
    const pillWidth = pill.getBoundingClientRect().width;
    setLeft(barWidth * (MOUNT_PCT / 100) - pillWidth / 2);
  }, []);

  return (
    <div className="w-full max-w-sm space-y-3">
      <label className="flex items-center gap-2 text-xs text-muted-foreground">
        <span className="shrink-0">Storage used</span>
        <input
          type="range"
          min={30}
          max={70}
          value={pct}
          onChange={(e) => setPct(Number(e.target.value))}
          aria-label="Storage used"
          className="flex-1 accent-primary"
        />
        <span className="w-9 text-right font-mono">{pct}%</span>
      </label>

      <div className="rounded-lg border border-border bg-card p-4">
        <p className="mb-3 text-sm font-medium text-foreground">Storage used</p>
        <div ref={barRef} className="relative h-16">
          <div className="h-2 w-full overflow-hidden rounded-full bg-muted">
            <div className="h-full rounded-full bg-primary" style={{ width: `${pct}%` }} />
          </div>
          {/* The tick tracks the live value; the JS-placed label does not. */}
          <div className="absolute top-0 h-5 w-px bg-foreground/40" style={{ left: `${pct}%` }} />
          <span
            ref={pillRef}
            style={{ left: left ?? undefined }}
            className="absolute top-7 whitespace-nowrap rounded-md border border-border bg-muted px-2 py-1 text-xs text-foreground"
          >
            {pct}% used
          </span>
        </div>
      </div>

      <p className="text-xs text-error">
        getBoundingClientRect placed the label once, at mount ({MOUNT_PCT}%). Move the slider: the fill and
        tick follow, but the frozen label drifts off the mark it points at — the measurement never re-runs.
      </p>
    </div>
  );
}
```

## Good — do this

`layout-flex-over-measurement-good`

```tsx
import { useState } from 'react';

export function FlexOverMeasurementGood() {
  const [pct, setPct] = useState(62);

  return (
    <div className="w-full max-w-sm space-y-3">
      <label className="flex items-center gap-2 text-xs text-muted-foreground">
        <span className="shrink-0">Storage used</span>
        <input
          type="range"
          min={30}
          max={70}
          value={pct}
          onChange={(e) => setPct(Number(e.target.value))}
          aria-label="Storage used"
          className="flex-1 accent-primary"
        />
        <span className="w-9 text-right font-mono">{pct}%</span>
      </label>

      <div className="rounded-lg border border-border bg-card p-4">
        <p className="mb-3 text-sm font-medium text-foreground">Storage used</p>
        <div className="h-16">
          <div className="h-2 w-full overflow-hidden rounded-full bg-muted">
            <div className="h-full rounded-full bg-primary" style={{ width: `${pct}%` }} />
          </div>
          {/* The grid does the arithmetic: the column boundary IS the fill mark, and
              translate-x-1/2 centers the label on it for any value — no measuring. */}
          <div className="grid" style={{ gridTemplateColumns: `${pct}fr ${100 - pct}fr` }}>
            <div className="flex flex-col items-end">
              <div className="h-5 w-px bg-foreground/40" />
              <span className="mt-2 translate-x-1/2 whitespace-nowrap rounded-md border border-border bg-muted px-2 py-1 text-xs text-foreground">
                {pct}% used
              </span>
            </div>
          </div>
        </div>
      </div>

      <p className="text-xs text-success">
        Pure grid + transform: the label rides the fill mark at every value — correct on first paint and it
        tracks the slider for free, with no measurement code.
      </p>
    </div>
  );
}
```

## References

- [MDN: getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)
- [MDN: CSS grid layout](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout)
