Accessible CSS Conic Gradient Progress Ring: Color Stops Are Not a Value

A CSS color stop can paint 70 percent of a circle, but it cannot tell the page that a task is 70 percent complete. If you are building an accessible CSS conic gradient progress ring for a single task, such as a file upload, treat the gradient as styling and author the value separately in HTML.

This guide walks through the sequence I would follow: see what the gradient alone fails to communicate, prototype the ring, add a native progress element and visible text, keep every representation in sync, and check contrast and states. It covers one determinate value only. It does not cover pie chart slices or meters.

Circular progress ring on a laptop screen with an indigo arc covering most of a pale track

See why the colored arc is not a progress value

MDN’s conic-gradient() reference explains that stops run around a circle from the top, clockwise. It also notes that CSS background images are not announced to screen readers and have no native way to assign alternative text. So a ring like this looks finished but says nothing:

.ring {
  width: 8rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background: conic-gradient(#3437c9 70%, #dfe3f5 0);
}

The arc shows 70 percent to anyone who can see and tell the two colors apart. There is no name, no value in the accessibility tree, and no text if the stylesheet fails to load. The whole message lives in a color boundary.

WCAG 2.2’s Use of Color guidance (Success Criterion 1.4.1, Level A) addresses relying on color as the sole visual means of conveying information. A programmatic value for assistive technology is useful, but it does not replace a visible alternative for someone who cannot distinguish the ring colors. A clearly labeled visible percentage does. The same habit applies to checkout form error border colors, where a red border needs text or an icon beside it to carry the message.

Prototype the ring colors and stops in a gradient generator

Styling is the part a generator is good for. Open the CSS Gradient Generator, switch to Conic mode, and adjust the pieces a ring depends on: the start angle, the center position, and the color stops. The tool lists up to five stops, CSS output, and exports such as SVG, PNG and JSON, plus a share link that reopens the same gradient for review.

A single-value ring only needs two colors: a progress arc and a track. Place the arc color first, set the boundary at your value, and let the track color take over. If the tool does not let you put two stops at the same position for a hard edge, copy the CSS and edit the second stop by hand. Extra stops are optional decoration, and the generator’s own tips suggest that two or three stops keep blends clean.

Keep the limits of the tool in mind. Its output is a background declaration. I have not tested any generator export for accessibility, and the page does not claim it produces progress markup, so treat everything it gives you as styling only. The hard-edged arc and the inner disk that turns the circle into a ring are CSS you write around it.

Build an accessible CSS conic gradient progress ring in HTML

Now write the semantics yourself. Use a native progress element with a label, keep it available to assistive technology, and draw the ring around it. This markup is an illustrative example written for this article, not a generator export:

<label for="upload">Uploading file</label>
<div class="ring" style="--value: 70">
  <progress id="upload" class="visually-hidden" value="70" max="100">70%</progress>
  <span class="ring-text" aria-hidden="true">70%</span>
</div>
.ring {
  display: grid;
  place-items: center;
  width: 8rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background: conic-gradient(#3437c9 calc(var(--value) * 1%), #dfe3f5 0);
}
.ring::before {
  content: "";
  grid-area: 1 / 1;
  width: 76%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: #ffffff;
}
.ring-text { grid-area: 1 / 1; color: #1f2340; font-weight: 600; }
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

Three details matter. First, MDN’s progress element reference documents its implicit progressbar role, its value and max attributes, and its association with a label. It also says text inside the element is a fallback for older browsers, not its accessible label, so the label element does the naming.

Second, hide the progress element visually, never with display none or aria-hidden, or it disappears from the accessibility tree. Third, the visible percentage is hidden from assistive technology here so the value is not read twice, and it stays visible if the background never paints.

Keep the ring, the text and the value in sync

For live progress, one application state should drive three things: the native value, the visible text, and the CSS stop. Changing the stop alone repaints the arc but leaves the semantic value stale.

const progress = document.querySelector('#upload');
const ring = document.querySelector('.ring');
const label = ring.querySelector('.ring-text');

function setProgress(next) {
  const value = Math.min(100, Math.max(0, Math.round(next)));
  progress.value = value;
  ring.style.setProperty('--value', value);
  label.textContent = value + '%';
}

If a native progress element genuinely does not fit your component, W3C’s range-related properties guidance describes the alternative role=”progressbar” pattern. Give it an accessible name and keep aria-valuenow current whenever the value is known. Do not add ARIA to an already labeled native progress element, since it does not need it.

Clamping the number before it reaches any of the three outputs also prevents a stray 104 percent from reaching the screen.

Check contrast for the parts that carry meaning

WCAG 2.2’s Non-text Contrast guidance (Success Criterion 1.4.11, Level AA) asks for at least 3:1 contrast against adjacent colors for graphical parts required to understand information. Its testing advice for gradients is to inspect the least-contrasting area and ask whether the graphic is still understandable if that area could not be seen. It excludes decorative graphics and graphics whose information is equivalently available as visible text.

So do not claim that every color stop must pass 3:1. If your visible “70%” is present and clearly labeled, the ring may be closer to reinforcement. If the arc is still needed to perceive progress, test the meaningful arc against the track, and the track against the card behind it. This is a contrast check only, not proof of full accessibility.

These values are a suggested starting set for an illustrative upload card. I have not measured them, so run each pair through the Contrast Checker before you commit.

RoleSuggested valueWhat to test
Progress arc#3437C9Against track and card
Track#DFE3F5Against card
Card background#FFFFFFAdjacent to track
Percentage text#1F2340Against the inner disk, using text thresholds

A pale track is the usual weak spot. If the track is not needed to understand the value, its contrast matters less. If the empty portion carries meaning, darken it or add a thin outline.

Test the accessible CSS conic gradient progress ring before shipping

Run this short checklist on the finished component, not on the prototype:

  1. Turn off styles. The label and the value should still read sensibly, for example “Uploading file” with a percentage.
  2. Inspect the accessibility tree. You should see one named progress indicator with the correct value, not two duplicate announcements.
  3. Test 0 percent, an intermediate value and 100 percent. Confirm the arc, the text and the native value agree at each.
  4. Trigger live updates and verify all three outputs change together.
  5. Check the visible text and, if the arc is necessary, the arc and track against adjacent colors across the whole gradient.
  6. Preview the ring in the Color Blindness Simulator. A simulation is a design check, not a diagnosis of any person’s vision, and the visible percentage should still do the work.

If the ring sits inside a card or button that takes keyboard focus, review the two color focus indicator CSS approach as well, because the ring’s colors can affect how visible a focus outline is.

Frequently asked questions

Can a conic gradient have alternative text?

Not natively. MDN notes that CSS background images are not announced to screen readers and cannot be given alternative text. Add the name and value through separate HTML, such as a labeled native progress element.

Should I use a native progress element or role=progressbar?

Prefer the native progress element when it fits, since it has an implicit progressbar role, value and max attributes, and label support. Use role=progressbar only when it does not fit, with an accessible name and a current aria-valuenow.

Why hide the progress element visually instead of using display none?

Display none and aria-hidden remove the element from the accessibility tree, so assistive technology would not find the value. A conventional visually-hidden utility keeps it available while the ring shows the visual version.

Does every color stop in the gradient need 3:1 contrast?

No. WCAG’s non-text contrast guidance covers graphical parts required to understand information and excludes decorative graphics and graphics whose information is available as visible text. If the arc is necessary, test the meaningful arc against adjacent colors.

Is a visible percentage enough on its own?

It supplies the visible alternative to color, but sighted keyboard and screen reader users are served differently. Keep the visible text and the native value, and make sure both match the arc.

Does changing the CSS stop update the accessible value?

No. The stop only repaints the background. Update the native progress value and the visible text from the same state whenever the CSS stop changes.

Is the Gradient Generator output accessible progress markup?

No. Its page documents conic mode, up to five stops and CSS or image exports. Treat the output as styling and write the label, native value and visible text yourself.

Next steps

An accessible CSS conic gradient progress ring is two separate jobs done together: the gradient draws the arc, and authored HTML supplies the name, the value and the visible text. Prototype the colors and stops, write the native progress element and label yourself, drive everything from one state, and test the meaningful parts for contrast. Then the ring can be as decorative as you like without carrying the message alone.

Related tools & resources on 66Colorful:

Sources and further reading