SVG circle and ellipse: cx, cy, r, rx and ry

A circle is a centre and a radius. The same three numbers work as attributes, as CSS properties you can animate, and as values a script can drag around.

An SVG circle is one tag with three numbers: cx and cy place the centre, and r sets the radius. An ellipse swaps r for two radii, rx across and ry down. All of them are measured in the units of the viewBox.

<svg viewBox="0 0 300 160">
  <circle cx="80" cy="80" r="50" fill="#2563eb"/>
  <ellipse cx="220" cy="80" rx="60" ry="35" fill="#f59e0b"/>
</svg>

Move the sliders and watch the markup under the drawing change with them.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SVG circle and ellipse attributes</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  svg { display: block; width: 100%; max-width: 460px; background: #fff; border: 1px solid #dde1e7; border-radius: 10px; }
  .controls { display: grid; grid-template-columns: 1fr; gap: 4px; max-width: 460px; margin-top: 12px; }
  label { display: grid; grid-template-columns: 76px 1fr 34px; align-items: center; gap: 8px; font-size: 14px; }
  label b { font-family: ui-monospace, Consolas, monospace; }
  input { min-width: 0; }
  output { font: 13px ui-monospace, Consolas, monospace; text-align: right; }
  pre { max-width: 460px; margin: 12px 0 0; padding: 10px 12px; background: #1d2330; color: #e5e7eb; border-radius: 8px; font-size: 12.5px; white-space: pre-wrap; word-break: break-all; }
</style>
</head>
<body>
<svg viewBox="0 0 300 160" aria-label="A circle and an ellipse">
  <circle id="c" cx="80" cy="80" r="50" fill="#2563eb" />
  <ellipse id="e" cx="220" cy="80" rx="60" ry="35" fill="#f59e0b" />
</svg>

<div class="controls">
  <label><span>circle <b>cx</b></span><input type="range" data-el="c" data-attr="cx" min="0" max="300" value="80"><output></output></label>
  <label><span>circle <b>cy</b></span><input type="range" data-el="c" data-attr="cy" min="0" max="160" value="80"><output></output></label>
  <label><span>circle <b>r</b></span><input type="range" data-el="c" data-attr="r" min="0" max="80" value="50"><output></output></label>
  <label><span>ellipse <b>rx</b></span><input type="range" data-el="e" data-attr="rx" min="0" max="80" value="60"><output></output></label>
  <label><span>ellipse <b>ry</b></span><input type="range" data-el="e" data-attr="ry" min="0" max="80" value="35"><output></output></label>
</div>
<pre id="code"></pre>

<script>
  const code = document.getElementById('code');

  // print both tags exactly as their attributes stand now
  function show() {
    code.textContent = ['c', 'e'].map((id) => document.getElementById(id).outerHTML).join('\n');
  }

  document.querySelectorAll('input').forEach((input) => {
    const out = input.nextElementSibling;
    out.textContent = input.value;
    input.addEventListener('input', () => {
      document.getElementById(input.dataset.el).setAttribute(input.dataset.attr, input.value);
      out.textContent = input.value;
      show();
    });
  });
  show();
</script>
</body>
</html>
Each slider writes one attribute. The code box prints the two tags as they stand.

If you are new to the <svg> element itself, the SVG tag in HTML covers the viewBox and the other shapes. This page stays with circles, ellipses and round corners.

cx, cy, r, rx and ry

A circle is a centre and one radius. An ellipse is a centre and two.
A circle is a centre and one radius. An ellipse is a centre and two.
Attribute Used by Meaning If left out
cx circle, ellipse Horizontal position of the centre 0
cy circle, ellipse Vertical position of the centre 0
r circle Radius 0, nothing is drawn
rx ellipse, rect Horizontal radius auto: copies ry
ry ellipse, rect Vertical radius auto: copies rx

A few details decide whether you see anything:

  • Zero draws nothing. A circle with r="0", or with no r at all, is not rendered. The same goes for an ellipse whose rx or ry is 0.
  • The centre is the anchor. A circle at cx="0" is cut in half by the left edge of a viewBox that starts at 0. To touch that edge, set cx equal to r.
  • The stroke sits on the outline. Half of stroke-width falls outside the radius. A circle with r="50" and a 10-unit stroke reaches 55 units from the centre.
  • Percentages work. cx="50%" is half the viewport width. For r, a percentage is taken from the diagonal: the square root of (width² + height²) / 2.

An ellipse with one radius

In SVG 2, the default for rx and ry is auto, and an auto radius takes the value of the other one. So an ellipse with only ry="20" draws a circle 40 units wide.

We checked this in Chromium, Firefox and WebKit, and all three drew the circle.

To draw an oval on purpose, set both. Colouring the inside and the outline is covered in SVG fill and stroke.

cx, cy and r are CSS properties

SVG 2 turned the geometry attributes into CSS properties. That means a stylesheet can size and place a circle:

.dot { cx: 40px; cy: 40px; r: 12px; }
.dot:hover { r: 18px; }
A CSS rule beats the attribute. In CSS the value needs a unit.
A CSS rule beats the attribute. In CSS the value needs a unit.

Two rules follow from this:

  1. Any CSS rule wins over the attribute. The attribute acts as a default with the lowest priority, so a plain class selector overrides it. An inline style does too.
  2. CSS needs a unit. Write cx: 40px, not cx: 40. One px equals one user unit of the viewBox, so the circle lands where the attribute would have put it.

Without the unit the value is not valid CSS. In our test, Firefox and WebKit ignored the rule and the circle stayed at its attribute. Chromium accepted it. Always write the unit.

Animate cx and r with CSS

Because cx and r are CSS properties, a transition or a keyframe animation can move and grow a circle. The button below only toggles a class.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Animate SVG circle cx and r with CSS</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  svg { display: block; width: 100%; max-width: 460px; background: #fff; border: 1px solid #dde1e7; border-radius: 10px; }
  button { margin-top: 12px; font: 600 15px system-ui, sans-serif; padding: 9px 16px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer; }
  p { font-size: 14px; max-width: 460px; line-height: 1.5; }

  /* 1. cx is a CSS property: a class moves the ball, transition animates it */
  .ball { cx: 40px; fill: #2563eb; transition: cx .8s ease-in-out, fill .8s; }
  .moved .ball { cx: 260px; fill: #16a34a; }

  /* 2. r inside @keyframes: a pulsing ring */
  @keyframes pulse { from { r: 8px; opacity: 1; } to { r: 34px; opacity: 0; } }
  .ring { fill: none; stroke: #f59e0b; stroke-width: 3; animation: pulse 1.6s ease-out infinite; }

  /* 3. r with a transition on hover */
  .dot { r: 14px; fill: #db2777; transition: r .25s; cursor: pointer; }
  .dot:hover { r: 24px; }
</style>
</head>
<body>
<svg id="stage" viewBox="0 0 300 180" aria-label="Circles animated with CSS">
  <line x1="40" y1="55" x2="260" y2="55" stroke="#e5e7eb" stroke-width="2" />
  <circle class="ball" cy="55" r="22" />
  <circle class="ring" cx="80" cy="130" r="8" />
  <circle cx="80" cy="130" r="6" fill="#f59e0b" />
  <circle class="dot" cx="210" cy="130" r="14" />
</svg>
<button id="go" type="button">Move the ball</button>
<p>All three circles are animated by CSS. The button only toggles a class. Hover over the pink dot, or tap it on a phone.</p>

<script>
  const stage = document.getElementById('stage');
  document.getElementById('go').addEventListener('click', () => {
    stage.classList.toggle('moved');  // CSS does the rest
  });
</script>
</body>
</html>
The ball moves with a cx transition, the ring pulses with an r keyframe, and the pink dot grows on hover.
.ball { cx: 40px; transition: cx .8s ease-in-out; }
.moved .ball { cx: 260px; }

@keyframes pulse {
  from { r: 8px; opacity: 1; }
  to   { r: 34px; opacity: 0; }
}
.ring { animation: pulse 1.6s ease-out infinite; }

The transition also runs when a script changes the attribute with setAttribute('cx', 200), since the attribute feeds the same property. In our test all three engines animated that change.

For timing functions and what else can be transitioned, see CSS transition and CSS keyframes.

Rounded corners on rect with rx and ry

<rect> uses the same rx and ry names for its corners. Each corner becomes a quarter of an ellipse with those radii.

rx alone rounds all four corners evenly. Too large a value turns the rect into an ellipse.
rx alone rounds all four corners evenly. Too large a value turns the rect into an ellipse.
  • Only rx: ry copies it, so every corner is a circle arc.
  • Both: different values give flatter, oval corners.
  • Too large: rx is capped at half the width and ry at half the height. With rx="200" on a 140 × 70 rect, both caps apply and the result is an ellipse.

For a pill shape, set rx to half the height. On a rect 70 units tall, that is rx="35". Like the circle radius, rx and ry also work as CSS properties on a rect.

A draggable circle editor

The last example puts it together. Drag a circle to move it, drag the white handle to change its radius, and copy the markup from the box underneath.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Draggable SVG circle editor</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  svg {
    display: block; width: 100%; max-width: 480px; background: #fff;
    border: 1px solid #dde1e7; border-radius: 10px;
    touch-action: none;   /* a finger drags circles instead of scrolling the page */
    user-select: none;
  }
  .shape { cursor: grab; stroke: #1d2330; stroke-width: 0; }
  .shape.selected { stroke-width: 2; stroke-dasharray: 5 4; }
  .handle { fill: #fff; stroke: #1d2330; stroke-width: 2; cursor: ew-resize; }
  .bar { display: flex; gap: 8px; margin-top: 10px; }
  button { font: 600 14px system-ui, sans-serif; padding: 8px 14px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer; }
  button.ghost { background: #e5e7eb; color: #1d2330; }
  pre { max-width: 480px; margin: 10px 0 0; padding: 10px 12px; background: #1d2330; color: #e5e7eb; border-radius: 8px; font-size: 12.5px; white-space: pre-wrap; }
</style>
</head>
<body>
<svg id="svg" viewBox="0 0 320 200" aria-label="Circle editor">
  <circle class="shape" cx="90" cy="100" r="45" fill="#93c5fd" />
  <circle class="shape" cx="220" cy="80" r="30" fill="#fcd34d" />
  <circle id="handle" class="handle" r="7" />
</svg>
<div class="bar">
  <button id="add" type="button">Add circle</button>
  <button id="del" type="button" class="ghost">Delete selected</button>
</div>
<pre id="code"></pre>

<script>
  const svg = document.getElementById('svg');
  const handle = document.getElementById('handle');
  const code = document.getElementById('code');
  const colors = ['#93c5fd', '#fcd34d', '#86efac', '#f9a8d4', '#c4b5fd'];
  let selected = null, mode = null, dx = 0, dy = 0;

  // screen pixels -> SVG user units, at whatever size the SVG is shown
  function toSvg(e) {
    return new DOMPoint(e.clientX, e.clientY).matrixTransform(svg.getScreenCTM().inverse());
  }
  const num = (el, name) => Number(el.getAttribute(name));

  function render() {
    svg.querySelectorAll('.shape').forEach((c) => c.classList.toggle('selected', c === selected));
    handle.style.display = selected ? '' : 'none';
    if (selected) {
      // the handle sits on the right edge: cx + r
      handle.setAttribute('cx', num(selected, 'cx') + num(selected, 'r'));
      handle.setAttribute('cy', num(selected, 'cy'));
      svg.appendChild(handle);  // keep the handle on top
    }
    code.textContent = [...svg.querySelectorAll('.shape')]
      .map((c) => `<circle cx="${num(c, 'cx')}" cy="${num(c, 'cy')}" r="${num(c, 'r')}" fill="${c.getAttribute('fill')}"/>`)
      .join('\n');
  }

  svg.addEventListener('pointerdown', (e) => {
    const p = toSvg(e);
    if (e.target === handle) {
      mode = 'resize';
    } else if (e.target.classList.contains('shape')) {
      selected = e.target;
      mode = 'move';
      dx = p.x - num(selected, 'cx');  // grab point, so the circle does not jump
      dy = p.y - num(selected, 'cy');
    } else {
      selected = null; mode = null; render(); return;
    }
    svg.setPointerCapture(e.pointerId);
    render();
  });

  svg.addEventListener('pointermove', (e) => {
    if (!mode || !svg.hasPointerCapture(e.pointerId)) return;
    const p = toSvg(e);
    if (mode === 'move') {
      selected.setAttribute('cx', Math.round(p.x - dx));
      selected.setAttribute('cy', Math.round(p.y - dy));
    } else {
      // new radius = distance from the centre to the pointer
      const r = Math.hypot(p.x - num(selected, 'cx'), p.y - num(selected, 'cy'));
      selected.setAttribute('r', Math.max(4, Math.round(r)));
    }
    render();
  });

  svg.addEventListener('lostpointercapture', () => { mode = null; });

  document.getElementById('add').addEventListener('click', () => {
    const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    const n = svg.querySelectorAll('.shape').length;
    c.setAttribute('class', 'shape');
    c.setAttribute('cx', 60 + (n * 47) % 200);
    c.setAttribute('cy', 60 + (n * 31) % 80);
    c.setAttribute('r', 25);
    c.setAttribute('fill', colors[n % colors.length]);
    svg.insertBefore(c, handle);
    selected = c;
    render();
  });

  document.getElementById('del').addEventListener('click', () => {
    if (selected) { selected.remove(); selected = null; render(); }
  });

  selected = svg.querySelector('.shape');
  render();
</script>
</body>
</html>
Drag a circle to move it. Drag the handle to resize. The code box always holds the current markup.

The key step is turning a pointer position in screen pixels into SVG units. The SVG may be shown at any width, so pixels and user units rarely match. getScreenCTM() returns the matrix from SVG units to the screen, and its inverse goes back:

function toSvg(e) {
  const m = svg.getScreenCTM().inverse();
  return new DOMPoint(e.clientX, e.clientY).matrixTransform(m);
}

The rest follows the pattern from the draggable div:

  1. On pointerdown, store the grab point: the pointer minus the circle's centre. Then call setPointerCapture.
  2. On pointermove, set cx and cy to the pointer minus the grab point.
  3. For the radius handle, set r to the distance from the centre to the pointer, using Math.hypot.
  4. Put touch-action: none on the SVG so a finger drags instead of scrolling.

When it does not work

What you see Cause Fix
Nothing appears r is missing or 0 Give the circle a radius above 0
Only half the circle shows The centre is on the edge Move cx or cy in by at least r
The CSS rule does nothing cx: 40 has no unit Write cx: 40px
The attribute change is ignored A CSS rule sets the same property Remove the rule, or change the CSS instead
getAttribute shows an old number CSS overrides but does not rewrite it Read getComputedStyle(el).cx
A new circle from JS never shows It was made with createElement Use createElementNS, shown below
The dragged circle drifts from the finger Screen pixels used as SVG units Convert with getScreenCTM().inverse()
An ellipse comes out round Only one of rx, ry is set Set both

A circle made by script must be in the SVG namespace, or the browser treats it as an unknown HTML element and draws nothing:

const NS = 'http://www.w3.org/2000/svg';
const c = document.createElementNS(NS, 'circle');

An editor like this one is meant to be touched. A screenshot cannot be dragged, and an .html file sent as an attachment may open as plain code on a phone.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can drag and resize the circles themselves. If you change the code later, the same link shows the new version.

Questions people ask

How do I draw a circle in SVG?

Put <circle cx="50" cy="50" r="40"/> inside an <svg> element. cx and cy are the centre, r is the radius, all in the units of the SVG's viewBox. Without r, or with r="0", nothing is drawn.

Can I set cx, cy and r in CSS?

Yes. In SVG 2, cx, cy, r, rx and ry are CSS properties, so a rule such as circle { r: 30px; } works, and any CSS rule overrides the attribute. In CSS the value needs a unit such as px, which equals one SVG user unit.

How do I animate an SVG circle's position?

Put transition: cx 0.5s on the circle and change cx from a class, an inline style or the attribute. For a loop, use @keyframes with cx or r inside. No JavaScript animation loop is needed.

What is the difference between circle and ellipse?

A circle has one radius, r. An ellipse has two: rx across and ry down. If an ellipse has only ry, rx is auto and copies it, so the shape is a circle.

Why does getAttribute("cx") return the old value after I changed it in CSS?

CSS overrides the attribute without rewriting it. getAttribute and cx.baseVal still report the attribute. Read the value that is actually drawn with getComputedStyle(circle).cx.

Keep reading