turbopills-ui
componentprogress-cellssource: ok · hit
Progress (Cells)
Segmented progress bar with optional labels/tooltips.
npx shadcn add "https://ui.turbopills.com/r/progress-cells.json"free
Interaction
tooltip
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@radix-ui/react-progress
Registry dependencies
tooltip
Files2 · source cached 10/8/2026, 8:15:32 PM
registry/turbopills/ui/progress/progress.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
export interface ProgressStep {
label?: string;
tooltip?: string;
}
export interface ProgressProps {
value?: number;
animated?: boolean;
filledClassName?: string;
unfilledClassName?: string;
currentClassName?: string;
className?: string;
}
export function useProgressHelpers({
value = 0,
stepCount,
highlightCurrent = false,
animated = true,
filledClassName,
unfilledClassName,
currentClassName,
}: {
value?: number;
stepCount: number;
highlightCurrent?: boolean;
animated?: boolean;
filledClassName?: string;
unfilledClassName?: string;
currentClassName?: string;
}) {
const transitionClass = animated ? "transition-all duration-500" : "";
const clampedValue = Math.min(Math.max(value, 0), 100);
const getStepState = (index: number) => {
const midpoint = ((index + 0.5) / stepCount) * 100;
const isFilled = value >= midpoint;
const nextMidpoint = ((index + 1.5) / stepCount) * 100;
const isCurrent = isFilled && value < nextMidpoint;
return { isFilled, isCurrent };
};
const getStateClasses = (isFilled: boolean, isCurrent: boolean) => {
if (isCurrent) {
return cn(
"bg-primary",
highlightCurrent ? "ring-2 ring-primary/70" : "",
currentClassName,
);
}
return isFilled
? cn("bg-primary", filledClassName)
: cn("bg-muted", unfilledClassName);
};
return {
transitionClass,
clampedValue,
getStepState,
getStateClasses,
};
}
export function StepLabel({ label }: { label?: string }) {
if (!label) return null;
return (
<span className="absolute top-full mt-2 text-xs text-muted-foreground whitespace-nowrap">
{label}
</span>
);
}
export function WithTooltip({
children,
tooltip,
}: {
children: React.ReactElement;
tooltip?: string;
}) {
if (!tooltip) return children;
return (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
);
}
export const ProgressContainer = React.forwardRef<
HTMLDivElement,
{
children: React.ReactNode;
hasLabels: boolean;
}
>(({ children, hasLabels }, ref) => {
return (
<div ref={ref} className={cn("relative w-full", hasLabels && "pb-6")}>
{children}
</div>
);
});
ProgressContainer.displayName = "ProgressContainer";
export function renderStepsArray(
count: number,
steps: ProgressStep[] | undefined,
renderItem: (index: number, step?: ProgressStep) => React.ReactNode,
) {
return Array.from({ length: count }, (_, index) =>
renderItem(index, steps?.[index]),
);
}
registry/turbopills/ui/progress/progress-cells.tsx
"use client";
import { forwardRef } from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
import {
type ProgressProps,
type ProgressStep,
useProgressHelpers,
ProgressContainer,
StepLabel,
WithTooltip,
renderStepsArray,
} from "./progress";
export type ProgressCellsProps = ProgressProps & {
highlightCurrent?: boolean;
cellGap?: number;
steps?: ProgressStep[] | number;
};
export const ProgressCells = forwardRef<HTMLDivElement, ProgressCellsProps>(
(
{
className,
value = 0,
animated = true,
highlightCurrent = false,
filledClassName,
unfilledClassName,
currentClassName,
cellGap = 8,
steps,
},
ref,
) => {
const effectiveStepCount = Array.isArray(steps)
? steps.length
: typeof steps === "number"
? steps
: 4;
const hasLabels = Array.isArray(steps) ? steps.some((s) => s.label) : false;
const { transitionClass, getStepState, getStateClasses } =
useProgressHelpers({
value,
stepCount: effectiveStepCount,
highlightCurrent,
animated,
filledClassName,
unfilledClassName,
currentClassName,
});
return (
<ProgressContainer ref={ref} hasLabels={hasLabels}>
<ProgressPrimitive.Root
className={cn("relative rounded-full w-full h-2", className)}
>
<div className="flex h-full w-full" style={{ gap: `${cellGap}px` }}>
{renderStepsArray(
effectiveStepCount,
Array.isArray(steps) ? steps : undefined,
(index, step) => {
const { isFilled, isCurrent } = getStepState(index);
return (
<div
key={index}
className="relative flex-1 flex flex-col items-center"
>
<WithTooltip tooltip={step?.tooltip}>
<div
className={cn(
"h-full w-full rounded-full",
transitionClass,
getStateClasses(isFilled, isCurrent),
)}
/>
</WithTooltip>
<StepLabel label={step?.label} />
</div>
);
},
)}
</div>
</ProgressPrimitive.Root>
</ProgressContainer>
);
},
);
ProgressCells.displayName = "ProgressCells";