Gap Arc

A pie-arc indicator for a quantity versus its complement on the same ring. The filled segment grows clockwise from startAngle; the muted remainder reads as the gap. Useful for progress, completion, "X out of Y" indicators, and any two-value-versus-whole readout where a slice diagram reads more clearly than a bar.

Customize
Value
0.65
Thickness
14px
Tone
accent

Installation

npx shadcn@latest add https://craftbits.dev/r/gap-arc.json

Usage

import { GapArc } from "@craft-bits/core";
 
<GapArc value={0.65} />

Use a percentage scale by setting max:

<GapArc value={42} max={100} tone="success" />

Render an "X out of Y" readout in the middle:

<GapArc value={3} max={8} label="3 / 8" tone="warning" />

Rotate the start angle so the fill begins somewhere other than twelve o'clock:

<GapArc value={0.4} startAngle={180} />

Understanding the component

  1. One ring, two strokes. A muted background circle marks the whole; a foreground circle with stroke-dasharray equal to the circumference is dashed off by C * (1 - fraction) so the visible stroke length matches the filled fraction. The circumference is computed once from size and thickness.
  2. Motion-value driven. A useMotionValue holds the current dashoffset. On every value change, the spring animates the offset toward the new target. React does not re-render every frame — the spring drives the DOM directly.
  3. startAngle rotates the entire ring. A single SVG group wraps the track and the fill; the default rotation places twelve o'clock at the start, and startAngle adds further clockwise rotation in degrees.
  4. Rounded line caps on both strokes. The track and the fill both use rounded caps so they meet flush when the fill closes the ring, and so the leading edge of a partial fill reads as a soft endpoint instead of a hard corner.
  5. Tone-driven palette. Five semantic tones. The track always uses a muted border color at 40% opacity so the bar reads softly until there is progress to show.
  6. Reduced motion. When prefers-reduced-motion: reduce is set, the arc snaps to its new target with no spring.

Props

PropTypeDefaultDescription
valuenumberFilled amount. Clamped to the inclusive range from 0 to max.
maxnumber1Total amount that value is measured against.
sizenumber160Outer diameter, in pixels.
thicknessnumber14Stroke width of both the track and the fill.
showLabelbooleantrueRender the centered readout.
labelReactNoderounded percentageCustom centered label.
tone'default' | 'accent' | 'success' | 'warning' | 'error''accent'Semantic color for the filled arc.
startAnglenumber0Where the fill begins, in degrees clockwise from twelve o'clock.
transitionTransitionSPRINGS.smoothSpring transition for the arc fill.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • Renders with role="img" and an aria-labelledby heading that reads the filled percentage (e.g. "65 percent filled").
  • The SVG and the centered label are both marked aria-hidden="true" — assistive tech reads only the visually hidden label.
  • The component is non-interactive. Wrap it in a button or link if you need to make the ratio actionable.
  • prefers-reduced-motion: reduce snaps the arc to its target with no spring.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/viz/GapArc.tsx). The source rendered an arc segment between two pointers on a cycle ring (used to visualize the closing gap in Floyd's algorithm). The library extract generalizes it to a stand-alone donut-arc primitive: drops the cycle-position math, swaps the inline hex stroke and drop-shadow for a tone-driven stroke and a muted track, replaces the inline spring with SPRINGS.smooth, and adds a centered label.