All components
Progress Circle
Circular determinate progress with a track, range, and centered value text.
64%
1234567891011121314151617181920212223242526272829import { ProgressCircle } from "#components/ui/base-ui/progress-circle.tsx";import { indigoPalette } from "@stylexblocks/ui/themes/palettes.ts";
export function Example() { const value = 64; return ( // size resolves once on Root; parts inherit it via context and expose // data-size attributes, composing via `render`. The component carries // the ring geometry — give both circles pathLength={100} and express // the value as strokeDasharray. <ProgressCircle.Root size="lg" palette={indigoPalette} role="progressbar" aria-valuenow={value} aria-valuemin={0} aria-valuemax={100} > <ProgressCircle.Circle aria-hidden="true"> <ProgressCircle.CircleTrack pathLength={100} /> <ProgressCircle.CircleRange pathLength={100} strokeDasharray={`${value} 100`} /> </ProgressCircle.Circle> <ProgressCircle.ValueText>{value}%</ProgressCircle.ValueText> </ProgressCircle.Root> );}Sizes
64%
xs64%
sm64%
md64%
lg64%
xlInstall
1npx stylexblocks add progress-circleSource
components/ui/base-ui/progress-circle.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206import * as stylex from "@stylexjs/stylex";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe } from "#lib/recipe-props.ts";import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { letterSpacings } from '@stylexblocks/ui/tokens/letter-spacings.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'
const progressCircleRoot = stylex.create({ base: { display: 'inline-flex', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, position: 'relative', },})
const progressCircleCircle = stylex.create({ base: {},})
const progressCircleCircleSizes = stylex.create({ xs: { '--size': sizes._4, '--thickness': '3px', }, sm: { '--size': sizes._6, '--thickness': '5px', }, md: { '--size': sizes._10, '--thickness': '6px', }, lg: { '--size': sizes._14, '--thickness': '7px', }, xl: { '--size': sizes._16, '--thickness': '8px', },})
const progressCircleCircleTrack = stylex.create({ base: { '--track-color': colorPalette.muted, stroke: 'var(--track-color)', },})
const progressCircleCircleRange = stylex.create({ base: { stroke: colorPalette.solid, transitionProperty: 'stroke-dasharray', transitionDuration: '0.6s', animationName: { default: null, ':is([data-indeterminate], [data-state="indeterminate"])': 'circular-progress', }, animationDuration: { default: null, ':is([data-indeterminate], [data-state="indeterminate"])': '1.5s', }, animationTimingFunction: { default: null, ':is([data-indeterminate], [data-state="indeterminate"])': 'linear', }, animationIterationCount: { default: null, ':is([data-indeterminate], [data-state="indeterminate"])': 'infinite', }, },})
const progressCircleLabel = stylex.create({ base: { display: 'inline-flex', },})
const progressCircleValueText = stylex.create({ base: { lineHeight: 1, fontWeight: fontWeights.medium, letterSpacing: letterSpacings.tight, fontVariantNumeric: 'tabular-nums', },})
const progressCircleValueTextSizes = stylex.create({ xs: { fontSize: fontSizes._2xs, lineHeight: lineHeights._2xs, }, sm: { fontSize: fontSizes._2xs, lineHeight: lineHeights._2xs, }, md: { fontSize: fontSizes.xs, lineHeight: lineHeights.xs, }, lg: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, xl: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, },})
export type ProgressCircleSize = keyof typeof progressCircleCircleSizes
// The recipe ships the size custom properties (--size, --thickness) and the// stroke colors; the SVG geometry that turns them into a ring lives here// with the component. Both circles should set pathLength={100} so the range// can express its value as `strokeDasharray="<percent> 100"`.const progressCircleLocal = stylex.create({ svg: { width: "var(--size)", height: "var(--size)" }, circle: { cx: "50%", cy: "50%", r: "calc(50% - var(--thickness) / 2)", fill: "none", strokeWidth: "var(--thickness)", }, range: { strokeLinecap: "round", transform: "rotate(-90deg)", transformOrigin: "center", }, valueText: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", },});
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axisconst progressCircleSlotRecipe = defineSlotRecipe({ slots: { root: { styles: progressCircleRoot }, circle: { styles: progressCircleCircle, sizes: progressCircleCircleSizes }, circleTrack: { styles: progressCircleCircleTrack }, circleRange: { styles: progressCircleCircleRange }, label: { styles: progressCircleLabel }, valueText: { styles: progressCircleValueText, sizes: progressCircleValueTextSizes }, }, defaultVariants: { size: "md" },});
/** * ProgressCircle — recipe and component colocated in one file. * `<ProgressCircle.Root size>` resolves the recipe once and every part * inherits it through context; each part composes via `render`. Give the * root `role="progressbar"` plus aria value attributes, and drive the ring * via `<ProgressCircle.CircleRange pathLength={100} * strokeDasharray={`${value} 100`} />`; ValueText centers over the ring. */const { withProvider, withContext } = createSlotRecipeContext(progressCircleSlotRecipe);
export const ProgressCircle = { Root: withProvider("div", "root", "ProgressCircleRoot"), Circle: withContext( "svg", (v) => [ progressCircleCircle.base, progressCircleCircleSizes[v.size], progressCircleLocal.svg, ], "ProgressCircleCircle", ), CircleTrack: withContext( "circle", () => [progressCircleCircleTrack.base, progressCircleLocal.circle], "ProgressCircleCircleTrack", ), CircleRange: withContext( "circle", () => [ progressCircleCircleRange.base, progressCircleLocal.circle, progressCircleLocal.range, ], "ProgressCircleCircleRange", ), Label: withContext("span", "label", "ProgressCircleLabel"), ValueText: withContext( "span", (v) => [ progressCircleValueText.base, progressCircleValueTextSizes[v.size], progressCircleLocal.valueText, ], "ProgressCircleValueText", ),};