turbopills-ui
componentprogress-solidsource: ok · hit
Progress (Solid)
Simple solid progress bar.
npx shadcn add "https://ui.turbopills.com/r/progress-solid.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@radix-ui/react-progress
Registry dependencies
tooltip
Files2 · source cached 10/15/2026, 4:41:27 AM
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-solid.tsx
"use client";
import { forwardRef } from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
import {
type ProgressProps,
useProgressHelpers,
ProgressContainer,
} from "./progress";
export const ProgressSolid = forwardRef<HTMLDivElement, Omit<ProgressProps, "currentClassName">>(
(
{ className, value = 0, animated = true, filledClassName, unfilledClassName },
ref,
) => {
const { transitionClass, clampedValue } = useProgressHelpers({
value,
stepCount: 1,
animated,
filledClassName,
unfilledClassName,
});
return (
<ProgressContainer ref={ref} hasLabels={false}>
<ProgressPrimitive.Root
className={cn("relative overflow-hidden rounded-full w-full h-2", className)}
>
<div
className={cn("absolute inset-0 rounded-full bg-muted", unfilledClassName)}
/>
<div
className={cn(
"absolute inset-0 rounded-full bg-primary",
transitionClass,
animated && "duration-300",
filledClassName,
)}
style={{ width: `${clampedValue}%` }}
/>
</ProgressPrimitive.Root>
</ProgressContainer>
);
},
);
ProgressSolid.displayName = "ProgressSolid";