shadimage
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 · light
ui-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";

More from turbopills-ui

Browse all components in this library →