Positioning Conic Gradients in React: Centering Radial Origins in Flex Containers

Quick Fix First: Set position: relative on Your Flex Parent Before Touching the Gradient

If your conic gradient's radial origin is landing in the top-left corner instead of dead center, stop debugging the gradient syntax. Nine times out of ten, the flex container eating your component doesn't have position: relative declared. Add it. Watch the origin snap into place. Then keep reading, because that fix alone won't survive a responsive breakpoint.

We're writing this for the React component library crowd — specifically those of you shipping data visualization primitives like circular gauges, progress rings, and pie segments into design systems. You know the drill: a consumer drops your <RadialGauge> into a flexbox layout, and suddenly your beautifully centered conic gradient drifts like a compass near a magnet. The gradient itself isn't broken. The layout context is. Let's walk through the pitfalls and their fixes, step by step.

1. Understand What "Center" Actually Means Inside a Flex Child

The Origin Reference Point Trap

A conic gradient's at <position> clause resolves relative to the element's own box — not its parent, not the viewport, and certainly not whatever flex algorithm arranged things. This sounds obvious until you realize that flex stretching (align-items: stretch, the default) silently resizes your element's box in ways your CSS didn't explicitly request.

Here's the scenario that bites people: you set background: conic-gradient(from 0deg, #4F46E5 0% 65%, #E5E7EB 65% 100%) on a div that's a flex child. You expect it to fill a 200×200 pixel square. But flex stretching made it 200×187 because the sibling next to it is shorter. Your gradient still centers — but on a non-square box, "center" produces an oval sweep that looks off-center to the eye.

The Fix: Explicit Dimensions or Aspect Ratio

Either lock the dimensions:

.gauge-track {
  width: 200px;
  height: 200px;
  flex-shrink: 0; /* prevent flex from squishing it */
  background: conic-gradient(from 0deg, #4F46E5 0% 65%, #E5E7EB 65% 100%);
  border-radius: 50%;
}

Or enforce aspect ratio and let flex handle the rest:

.gauge-track {
  aspect-ratio: 1 / 1;
  flex: 0 0 auto;
  background: conic-gradient(from 0deg, #4F46E5 0% 65%, #E5E7EB 65% 100%);
  border-radius: 50%;
}

The flex-shrink: 0 matters more than people think. Without it, a crowded flex row will compress your gauge to 178 pixels wide while leaving the height at 200, and your radial origin — mathematically correct at (89px, 100px) — now looks like it's been shoved left.

2. Stop Using Percentages for the at Position in Dynamic Layouts

Why at 50% 50% Lies to You

Percentage positions in conic gradients resolve against the element's box dimensions. That's fine for static layouts. In React, where components re-render into different container sizes, percentages create a subtle problem: they work perfectly in your Storybook sandbox at 320×320, then drift in production because the consumer's flex container imposes different constraints.

The percentage approach:

background: conic-gradient(from 0deg at 50% 50%, #4F46E5, #E5E7EB);

This is fine — until it isn't. The moment your element's box becomes non-square due to flex stretching, 50% 50% still technically centers, but the visual result is an ellipse, not a circle. Users perceive this as "the gradient is off-center" even though it mathematically isn't.

The Fix: Use background-size and background-position Together

For components that must survive arbitrary flex contexts, decouple the gradient from the element's box:

.gauge-track {
  position: relative;
  width: 200px;
  height: 200px;
  flex-shrink: 0;
  background-color: #E5E7EB;
  background-image: conic-gradient(from 0deg, #4F46E5 0% 65%, transparent 65% 100%);
  background-size: 200px 200px;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: 50%;
}

By setting background-size explicitly, the gradient renders in a fixed 200×200 painting area regardless of what flex does to the element's box. The background-position: center then centers that painting area within the element. If flex stretches the element to 200×240, the gradient stays a 200×200 circle, centered visually. No drift.

3. Handle the from Angle Rotation Without Breaking the Origin

The Rotation Red Herring

Here's a mistake that wastes hours: you notice your gauge's starting segment begins at 12 o'clock (the default from 0deg), but your design spec calls for it to start at 9 o'clock. So you rotate. But you rotate the element instead of the gradient.

// WRONG — this rotates the element, shifting the gradient origin
// relative to the flex container
.gauge-track {
  transform: rotate(-90deg);
  background: conic-gradient(from 0deg, #4F46E5 0% 65%, #E5E7EB 65% 100%);
}

Rotating the element with transform doesn't move the gradient's origin — the origin stays at the element's center. But it does rotate the entire box, which means any children (labels, tick marks, needle indicators) rotate too. If you've got a <GaugeLabel> absolutely positioned at the bottom, it's now on the left. Chaos.

The Fix: Rotate the Gradient, Not the Element

.gauge-track {
  background: conic-gradient(from -90deg, #4F46E5 0% 65%, #E5E7EB 65% 100%);
}

from -90deg starts the sweep at 9 o'clock. The element stays put. The origin stays centered. Your labels stay where you put them. This is the approach you should default to in your React component — expose a startAngle prop and pass it directly into the gradient string:

function RadialGauge({ value = 65, startAngle = -90, size = 200 }) {
  const pct = Math.max(0, Math.min(100, value));
  return (
    <div
      className="gauge-track"
      style={{
        width: size,
        height: size,
        flexShrink: 0,
        backgroundImage: `conic-gradient(from ${startAngle}deg, #4F46E5 0% ${pct}%, #E5E7EB ${pct}% 100%)`,
        backgroundSize: `${size}px ${size}px`,
        backgroundPosition: 'center',
        borderRadius: '50%',
      }}
    />
  );
}

4. Survive the align-items: stretch Default Without a Fight

The Silent Resizer

Flex's default align-items: stretch is the single biggest source of "my gradient origin is wrong" tickets. It stretches flex children along the cross axis to fill the container. If your gauge is in a row with a taller sibling, it gets taller. If it's in a column with a wider sibling, it gets wider. Either way, the box changes shape and the gradient — even if perfectly centered mathematically — looks wrong.

You have three defensive options, ranked by robustness:

Option A: Override on the Parent

If you control the flex container (e.g., it's internal to your component), set align-items: flex-start or center on it. This prevents stretching entirely. Best for internal layouts where you own both sides.

Option B: Self-Align on the Child

.gauge-track {
  align-self: center; /* or flex-start */
}

This lets the consumer's flex container keep its default stretch behavior while your component opts out. More polite. Works well for library components shipped to unknown consumers.

Option C: The Nuclear Option — Fixed Background Size

As shown in section 2, background-size with explicit pixel values makes the gradient immune to box shape changes. The element can stretch all it wants; the gradient paints in a fixed area. This is the most defensive approach and the one I recommend for design system components where you cannot predict the consumer's layout.

5. Debug Like You Mean It: Visualize the Origin

When a conic gradient looks wrong, your first instinct is probably to fiddle with the at position. Resist that. Instead, temporarily overlay a visual marker at the gradient's computed origin so you can see where the browser thinks "center" actually is.

.gauge-track::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 6px;
  height: 6px;
  background: red;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 10;
}

If the red dot is centered but the gradient looks off, your box is non-square — go back to section 1. If the red dot is off-center, something is overriding your positioning context — check for inherited transforms or a misconfigured position on an ancestor.

One more number to keep in your back pocket: a conic gradient with from 0deg sweeps clockwise starting from the top (12 o'clock). A 65% fill stops at 234 degrees. If your gauge reads 65% but the visual fill looks like it stops at roughly 180 degrees, you've got a non-square box compressing the sweep into an ellipse — and you need the background-size fix, not an angle adjustment.

The gradient isn't broken. The layout context is. Fix the box, and the origin fixes itself.