AniUI

Progress Steps

Multi-step progress indicator for wizards and onboarding flows.

Installation#

npx @aniui/cli add progress-steps
Account
2
Profile
3
Review
Web preview — components render natively on iOS & Android
import { ProgressSteps, ProgressStep } from "@/components/ui/progress-steps";

export function MyScreen() {
  const [step, setStep] = useState(1);

  return (
    <ProgressSteps current={step}>
      <ProgressStep label="Account" />
      <ProgressStep label="Profile" />
      <ProgressStep label="Review" />
      <ProgressStep label="Done" />
    </ProgressSteps>
  );
}

Usage#

app/index.tsx
import { ProgressSteps, ProgressStep } from "@/components/ui/progress-steps";

export function MyScreen() {
  const [step, setStep] = useState(1);

  return (
    <ProgressSteps current={step}>
      <ProgressStep label="Account" />
      <ProgressStep label="Profile" />
      <ProgressStep label="Review" />
      <ProgressStep label="Done" />
    </ProgressSteps>
  );
}

Props#

ProgressSteps#

PropTypeDefault
current
number
-
className
string
-
children
React.ReactNode
-

ProgressStep#

PropTypeDefault
label
string
-
icon
React.ReactNode
-
className
string
-

Accessibility#

  • Multi-step wizard with accessibilityRole="list" on the container.
  • Current step and completion state are announced to screen readers.

Source#

components/ui/progress-steps.tsx
import React, { createContext, useContext } from "react";
import { View, Text } from "react-native";
import { cn } from "@/lib/utils";

const StepsContext = createContext<{ current: number }>({ current: 0 });

export interface ProgressStepsProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  current: number;
  children?: React.ReactNode;
}

export function ProgressSteps({ current, className, children, ...props }: ProgressStepsProps) {
  return (
    <StepsContext.Provider value={{ current }}>
      <View className={cn("flex-row items-center", className)} accessibilityRole="list" {...props}>
        {React.Children.map(children, (child, index) => (
          <>
            {index > 0 && (
              <View className={cn("flex-1 h-0.5 mx-2", index <= current ? "bg-primary" : "bg-muted")} />
            )}
            {React.isValidElement(child)
              ? React.cloneElement(child as React.ReactElement<{ _index?: number }>, { _index: index })
              : child}
          </>
        ))}
      </View>
    </StepsContext.Provider>
  );
}

export interface ProgressStepProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  label?: string;
  icon?: React.ReactNode;
  _index?: number;
}

export function ProgressStep({ label, icon, className, _index = 0, ...props }: ProgressStepProps) {
  const { current } = useContext(StepsContext);
  const isCompleted = _index < current;
  const isActive = _index === current;

  return (
    <View className={cn("items-center gap-1", className)} accessibilityRole="listitem" {...props}>
      <View
        className={cn(
          "h-8 w-8 rounded-full items-center justify-center",
          isCompleted ? "bg-primary" : isActive ? "border-2 border-primary bg-background" : "bg-muted"
        )}
      >
        {icon ?? (
          <Text
            className={cn(
              "text-sm font-semibold",
              isCompleted ? "text-primary-foreground" : isActive ? "text-primary" : "text-muted-foreground"
            )}
          >
            {isCompleted ? "✓" : _index + 1}
          </Text>
        )}
      </View>
      {label && (
        <Text
          className={cn(
            "text-xs",
            isActive ? "text-primary font-medium" : "text-muted-foreground"
          )}
          numberOfLines={1}
        >
          {label}
        </Text>
      )}
    </View>
  );
}