Circular progress bar with handle — CSS slider example
A progress bar with a handle is really a circular slider in disguise. The fill shows the current value; the handle suggests the user can grab it and turn it. Even when the control is read-only, the handle does useful work — it’s a sharp endpoint that draws the eye to the exact value the bar is currently at, instead of leaving you to guess where the colored fill ends.
This example places an <o-progress> arc and a satellite handle on the same orbit. The handle’s angle (angle-225) lines up with the end of the 60% fill. To make this interactive, you’d compute the angle from the value and update both at once.
Examples include the current Orbit stylesheet and runtime. Get both files for your project.
How it works
Section titled “How it works”<o-progress>and the handle satellite share the same parent orbit (orbit-4). That’s what keeps them visually aligned: when the orbit’s radius changes, both move together without you doing math.shrink-50on the progress thins the ring relative to the parent orbit, leaving room for the handle to sit on the centerline of the ring instead of jutting out.angle-225points the handle to roughly the 7 o’clock position — that’s where 60% of a full circle starting from 0° (the 3 o’clock position) lands. The math:angle = (value / 100) * 360, but since<o-progress>defaults to starting at 12 o’clock (which is angle-90in standard coordinates), you’ll need to subtract 90° in your computation.- The handle is a satellite with no border and a solid background — that’s all that’s needed to make a circular puck. Add a
box-shadowto give it depth, or wrap it in acapsulefor a pill shape.
Customization
Section titled “Customization”Make it interactive with a pointer event listener:
const orbit = document.querySelector('.orbit-4');const handle = document.querySelector('.handle');const progress = document.querySelector('o-progress');
orbit.addEventListener('pointermove', (e) => { if (e.buttons !== 1) return; const rect = orbit.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; const angle = Math.atan2(e.clientY - cy, e.clientX - cx) * 180 / Math.PI; const normalized = (angle + 90 + 360) % 360; const value = Math.round((normalized / 360) * 100); handle.className = 'satellite handle angle-' + Math.round(normalized); progress.setAttribute('value', value);});Make the handle visually grabbable by adding a ring:
.handle { background: var(--o-cyan); box-shadow: 0 0 0 3px white, 0 0 0 4px var(--o-cyan-darker);}Related
Section titled “Related”- Solid progress with background
- Knob with progress bar — uses a similar handle pattern
satelliteelement reference- Use case: audio app UI
Use this in your project
Section titled “Use this in your project”Install Orbit and copy the markup above.