dotUI
dotUI
beta
  1. Components
  2. Feedback
  3. Progress

Progress

Progress shows the completion progress of a task in shape of a bar, displayed as a progress bar..

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

function Demo() {
  return <Progress aria-label="loading" value={75} />;
}

Installation

Install the following dependencies:

npm install react-aria-components

Copy and paste the following code into your project.

"use client";

import * as React from "react";
import {
  ProgressBar as AriaProgress,
  composeRenderProps,
  type ProgressBarProps as AriaProgressProps,
} from "react-aria-components";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils/classes";
import { Label } from "./field";

const progressStyles = tv({
  slots: {
    root: "w-full grid [grid-template-areas:'label_valueLabel''progress_progress'] grid-cols-[1fr_auto] gap-1",
    indicator: "[grid-area:progress] relative h-2.5 w-full overflow-hidden rounded-full",
    filler:
      "h-full animate-progress-grow w-full flex-1 bg-fg transition-transform origin-left min-w-14",
    valueLabel: "[grid-area:valueLabel] text-sm",
    label: "[grid-area:label] ",
  },
  variants: {
    variant: {
      default: {
        indicator: "bg-bg-muted",
        filler: "bg-bg-primary",
      },
      accent: {
        indicator: "bg-bg-accent-muted",
        filler: "bg-bg-accent",
      },
      warning: {
        indicator: "bg-bg-warning-muted",
        filler: "bg-bg-warning",
      },
      danger: {
        indicator: "bg-bg-danger-muted",
        filler: "bg-bg-danger",
      },
      success: {
        indicator: "bg-bg-success-muted",
        filler: "bg-bg-success",
      },
    },
    size: {
      sm: {
        indicator: "h-1",
      },
      md: {
        indicator: "h-2.5",
      },
      lg: {
        indicator: "h-4",
      },
    },
  },
  defaultVariants: {
    variant: "default",
    shape: "bar",
    size: "md",
  },
});

type ProgressSlots = keyof ReturnType<typeof progressStyles>;
type ProgressClassNames = {
  [key in ProgressSlots]?: string;
};

interface ProgressProps extends ProgressRootProps, VariantProps<typeof progressStyles> {
  label?: string;
  showValueLabel?: boolean;
  duration?: string;
  classNames?: ProgressClassNames;
}
const Progress = ({
  label,
  showValueLabel = false,
  variant,
  size,
  duration,
  className,
  classNames,
  ...props
}: ProgressProps) => {
  return (
    <ProgressRoot duration={duration} className={cn(className, classNames?.root)} {...props}>
      {label && <Label className={classNames?.label}>{label}</Label>}
      {showValueLabel && <ProgressValueLabel className={classNames?.valueLabel} />}
      <ProgressIndicator
        variant={variant}
        size={size}
        duration={duration}
        classNames={{ indicator: classNames?.indicator, filler: classNames?.filler }}
      />
    </ProgressRoot>
  );
};

interface ProgressRootProps extends Omit<AriaProgressProps, "className"> {
  duration?: string;
  className?: string;
}
const ProgressRoot = ({ className, isIndeterminate, duration, ...props }: ProgressRootProps) => {
  const { root } = progressStyles();
  return (
    <AriaProgress
      className={root({ className })}
      isIndeterminate={isIndeterminate || !!duration}
      {...props}
    >
      {composeRenderProps(
        props.children,
        (children, { percentage, isIndeterminate, valueText }) => (
          <ProgressContext.Provider value={{ percentage, isIndeterminate, valueText }}>
            {children}
          </ProgressContext.Provider>
        )
      )}
    </AriaProgress>
  );
};

interface ProgressIndicatorProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof progressStyles> {
  duration?: string;
  classNames?: {
    indicator?: string;
    filler?: string;
  };
}
const ProgressIndicator = ({
  duration,
  className,
  variant,
  size,
  classNames,
  ...props
}: ProgressIndicatorProps) => {
  const { indicator, filler } = progressStyles({ variant, size });
  const { isIndeterminate, percentage } = useProgressContext();
  return (
    <div className={cn(indicator(), classNames?.indicator, className)} {...props}>
      <div
        className={cn(
          filler(),
          isIndeterminate && "animate-progress-indeterminate",
          classNames?.filler
        )}
        style={
          {
            "--progress-duration": duration ?? "0s",
            ...(percentage ? { transform: `scaleX(${percentage / 100})` } : {}),
            ...(isIndeterminate
              ? {
                  maskImage: "linear-gradient(75deg, rgb(0, 0, 0) 30%, rgba(0, 0, 0, 0.65) 80%)",
                  maskSize: "200%",
                }
              : {}),
          } as React.CSSProperties
        }
      />
    </div>
  );
};

type ProgressValueLabelProps = React.HTMLAttributes<HTMLSpanElement>;
const ProgressValueLabel = ({ children, className, ...props }: ProgressValueLabelProps) => {
  const { valueLabel } = progressStyles();
  const { valueText } = useProgressContext();
  return (
    <span className={valueLabel({ className })} {...props}>
      {children ?? valueText}
    </span>
  );
};

type ProgressContextValue = {
  percentage?: number;
  isIndeterminate: boolean;
  valueText?: string;
};
const ProgressContext = React.createContext<ProgressContextValue | null>(null);
const useProgressContext = () => {
  const context = React.useContext(ProgressContext);
  if (!context) {
    throw new Error("useProgressContext must be used within a ProgressProvider");
  }
  return context;
};

export type { ProgressProps, ProgressRootProps, ProgressIndicatorProps, ProgressValueLabelProps };
export { Progress, ProgressRoot, ProgressIndicator, ProgressValueLabel };

Update the import paths to match your project setup.

Usage

Use Progress to show the completion progress of a task. It can represent either determinate or indeterminate progress.

Best pratices

When possible, use a determinate progress indicator.
An indeterminate progress indicator shows that a process is occurring, but it doesn’t help people estimate how long a task will take.
A determinate progress indicator can help people decide whether to do something else while waiting for the task to complete, restart the task at a different time, or abandon the task.

Options

Variants

Use the variant prop to control the visual style of the Progress.
The default variant is default.

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

const variants = ["default", "success", "accent", "danger", "warning"] as const;

function Demo() {
  return (
    <div className="w-full space-y-4">
      {variants.map((variant) => (
        <Progress key={variant} value={75} label={variant} variant={variant} />
      ))}
    </div>
  );
}

Sizes

Use the size prop to control the size of the Progress. The default variant is md.

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

const sizes = ["sm", "md", "lg"] as const;

function Demo() {
  return (
    <div className="w-full space-y-4">
      {sizes.map((size) => (
        <Progress key={size} value={75} size={size} label={size} />
      ))}
    </div>
  );
}

Label

A visual label can be provided for the TextField using the label prop, or a hidden label using aria-label prop.

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

function Demo() {
  return (
    <div className="flex w-full flex-col gap-4">
      <Progress aria-label="Loading" value={75} />
      <Progress label="Loading..." value={75} />
    </div>
  );
}

Value label

Set the showValueLabel prop to true to display the current value of the progress bar.

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

function Demo() {
  return <Progress label="Loading" showValueLabel value={75} />;
}

Format options

Values are formatted as a percentage by default, but this can be modified by using the formatOptions prop to specify a different format.

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

function Demo() {
  return (
    <Progress
      label="Sending…"
      showValueLabel
      formatOptions={{ style: "currency", currency: "JPY" }}
      value={60}
    />
  );
}

Custom value label

The valueLabel prop allows the formatted value to be replaced with a custom string.

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

function Demo() {
  return <Progress label="Feeding…" valueLabel="30 of 100 dogs" value={30} />;
}

Indeterminate

The isIndeterminate prop can be used to represent an interdeterminate operation.

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

function Demo() {
  return <Progress isIndeterminate />;
}

Duration

Use the duration prop to indicate an approximate duration of an indeterminate task. Once the duration times out, the progress bar will start an indeterminate animation.

"use client";

import * as React from "react";
import { Button } from "@/lib/components/core/default/button";
import { Progress } from "@/lib/components/core/default/progress";
import { RotateCwIcon } from "@/lib/icons";

function Demo() {
  const [key, setKey] = React.useState(0);
  const restart = () => setKey((prev) => prev + 1);
  return (
    <div className="flex w-full flex-col items-center gap-6">
      <Button prefix={<RotateCwIcon />} onPress={restart}>
        Restart animation
      </Button>
      <Progress key={key} duration="30s" />
    </div>
  );
}

Min and max values

A custom value scale can be used by setting the minValue and maxValue props.

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

function Demo() {
  return <Progress minValue={50} maxValue={150} value={100} />;
}

Examples

Composition

If you need to customize things further, you can drop down to the composition level.

"use client";

import * as React from "react";
import { Label } from "react-aria-components";
import {
  ProgressIndicator,
  ProgressRoot,
  ProgressValueLabel,
} from "@/lib/components/core/default/progress";

function Demo() {
  return (
    <ProgressRoot value={50} className="flex-row items-center gap-4">
      <Label>Progress</Label>
      <ProgressIndicator />
      <ProgressValueLabel />
    </ProgressRoot>
  );
}

Custom color

"use client";

import * as React from "react";
import { Progress } from "@/lib/components/core/default/progress";

function Demo() {
  return (
    <Progress
      value={75}
      classNames={{
        indicator: "bg-slate-300 dark:bg-slate-800",
        filler: "bg-slate-800 dark:bg-slate-300",
      }}
    />
  );
}

Toolbar

"use client";

import * as React from "react";
import { Button } from "@/lib/components/core/default/button";
import { Input, InputRoot } from "@/lib/components/core/default/input";
import { Progress } from "@/lib/components/core/default/progress";
import { TextFieldRoot } from "@/lib/components/core/default/text-field";
import { ALargeSmallIcon, RotateCwIcon } from "@/lib/icons";

function Demo() {
  const [key, setKey] = React.useState(0);
  const refresh = () => setKey((key) => key + 1);
  return (
    <TextFieldRoot key={key} defaultValue="https://rcopy.dev">
      <InputRoot className="relative h-10 overflow-hidden px-1 pb-0.5">
        <Button size="sm" variant="quiet" shape="square" className="size-7">
          <ALargeSmallIcon />
        </Button>
        <Input className="text-center" />
        <Button onPress={refresh} size="sm" variant="quiet" shape="square" className="size-7">
          <RotateCwIcon />
        </Button>
        <Progress
          value={50}
          size="sm"
          variant="accent"
          duration="5s"
          className="absolute bottom-0 left-0 right-0"
        />
      </InputRoot>
    </TextFieldRoot>
  );
}

API Reference

PropTypeDefaultDescription
variant
"default" | "primary" | "danger" | "success" | "warning"
"default"
The visual style of the progress indicator.
size
"sm" | "md" | "lg"
"md"
The size of the progress indicator
isIndeterminate
boolean
-
Whether presentation is indeterminate when progress isn't known.
formatOptions
Intl.NumberFormatOptions
{style: 'percent'}
The display format of the value label.
valueLabel
ReactNode
-
The content to display as the value's label (e.g. 1 of 4).
showValueLabel
boolean
false
Whether the value's label is displayed.
value
number
0
The current value (controlled).
minValue
number
0
The smallest value allowed for the input.
maxValue
number
100
The largest value allowed for the input.
children
ReactNode | (values: ProgressBarRenderProps & {defaultChildren: ReactNode | undefined}) => ReactNode
-
The children of the component. A function may be provided to alter the children based on component state.
className
string
-
The CSS className for the element.
style
CSSProperties | (values: ProgressBarRenderProps & {defaultStyle: CSSProperties}) => CSSProperties
-
The inline style for the element. A function may be provided to compute the style based on component state.
CSS SelectorDescription
[aria-valuetext]
A formatted version of the value.
:not([aria-valuenow])
Whether the progress bar is indeterminate.

Built by mehdibha. The source code is available on GitHub.