V1.0

Base Components

Horizontal Stepper

A versatile horizontal step indicator providing a clear visual guide through sequential processes.

Installation

Install the following dependencies:

terminal
npm install @radix-ui/react-slot

Create a horizontal-stepper.tsx file and paste the following code into it.

/components/ui/horizontal-stepper.tsx
// AlignUI HorizontalStepper v0.0.0
 
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { RiArrowRightSLine } from '@remixicon/react';
 
import { cnExt } from '@/utils/cn';
import type { PolymorphicComponentProps } from '@/utils/polymorphic';
import { recursiveCloneChildren } from '@/utils/recursive-clone-children';
import { tv, type VariantProps } from '@/utils/tv';
 
const HORIZONTAL_STEPPER_ROOT_NAME = 'HorizontalStepperRoot';
const HORIZONTAL_STEPPER_SEPARATOR_NAME = 'HorizontalStepperSeparator';
const HORIZONTAL_STEPPER_ITEM_NAME = 'HorizontalStepperItem';
const HORIZONTAL_STEPPER_ITEM_INDICATOR_NAME = 'HorizontalStepperItemIndicator';
 
function HorizontalStepperRoot({
  asChild,
  children,
  className,
  ...rest
}: React.HTMLAttributes<HTMLDivElement> & {
  asChild?: boolean;
}) {
  const Component = asChild ? Slot : 'div';
 
  return (
    <Component
      className={cnExt('flex flex-wrap justify-center gap-4', className)}
      {...rest}
    >
      {children}
    </Component>
  );
}
HorizontalStepperRoot.displayName = HORIZONTAL_STEPPER_ROOT_NAME;
 
function HorizontalStepperSeparatorIcon<T extends React.ElementType>({
  className,
  as,
  ...rest
}: PolymorphicComponentProps<T>) {
  const Component = as || RiArrowRightSLine;
 
  return (
    <Component
      className={cnExt('size-5 shrink-0 text-text-soft-400', className)}
      {...rest}
    />
  );
}
HorizontalStepperSeparatorIcon.displayName = HORIZONTAL_STEPPER_SEPARATOR_NAME;
 
const horizontalStepperItemVariants = tv({
  slots: {
    root: [
      // base
      'flex items-center gap-2 text-paragraph-sm',
    ],
    indicator: [
      // base
      'flex size-5 shrink-0 items-center justify-center rounded-full text-label-xs',
    ],
  },
  variants: {
    state: {
      completed: {
        root: 'text-text-strong-950',
        indicator: 'bg-success-base text-static-white',
      },
      active: {
        root: 'text-text-strong-950',
        indicator: 'bg-primary-base text-static-white',
      },
      default: {
        root: 'text-text-sub-600',
        indicator:
          'bg-bg-white-0 text-text-sub-600 ring-1 ring-inset ring-stroke-soft-200',
      },
    },
  },
  defaultVariants: {
    state: 'default',
  },
});
 
type HorizontalStepperItemSharedProps = VariantProps<
  typeof horizontalStepperItemVariants
>;
 
type HorizontalStepperItemProps =
  React.ButtonHTMLAttributes<HTMLButtonElement> &
    VariantProps<typeof horizontalStepperItemVariants> & {
      asChild?: boolean;
    };
 
const HorizontalStepperItem = React.forwardRef<
  HTMLButtonElement,
  HorizontalStepperItemProps
>(({ asChild, children, state, className, ...rest }, forwardedRef) => {
  const uniqueId = React.useId();
  const Component = asChild ? Slot : 'button';
  const { root } = horizontalStepperItemVariants({ state });
 
  const sharedProps: HorizontalStepperItemSharedProps = {
    state,
  };
 
  const extendedChildren = recursiveCloneChildren(
    children as React.ReactElement[],
    sharedProps,
    [HORIZONTAL_STEPPER_ITEM_INDICATOR_NAME],
    uniqueId,
    asChild,
  );
 
  return (
    <Component
      ref={forwardedRef}
      className={root({ class: className })}
      {...rest}
    >
      {extendedChildren}
    </Component>
  );
});
HorizontalStepperItem.displayName = HORIZONTAL_STEPPER_ITEM_NAME;
 
function HorizontalStepperItemIndicator({
  state,
  className,
  children,
  ...rest
}: React.HTMLAttributes<HTMLDivElement> & HorizontalStepperItemSharedProps) {
  const { indicator } = horizontalStepperItemVariants({ state });
 
  if (state === 'completed') {
    return (
      <div className={indicator({ class: className })} {...rest}>
        <svg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='none'>
          <path
            fill='currentColor'
            d='M15.1 7.453 8.726 13.82 4.9 10l1.275-1.274 2.55 2.548 5.1-5.094L15.1 7.453Z'
          />
        </svg>
      </div>
    );
  }
 
  return (
    <div className={indicator({ class: className })} {...rest}>
      {children}
    </div>
  );
}
HorizontalStepperItemIndicator.displayName =
  HORIZONTAL_STEPPER_ITEM_INDICATOR_NAME;
 
export {
  HorizontalStepperRoot as Root,
  HorizontalStepperSeparatorIcon as SeparatorIcon,
  HorizontalStepperItem as Item,
  HorizontalStepperItemIndicator as ItemIndicator,
};

Update the import paths to match your project setup.

Examples

With Radix Tabs

API Reference

HorizontalStepper.Root

Outer container that holds the stepper items and separator icons. This component is based on the <div> element and supports all of its props. And adds:

PropTypeDefault
asChild
boolean

HorizontalStepper.SeparatorIcon

Icon used as a separator between stepper items. A polymorphic component, allowing you to change the underlying HTML element using the as prop.

PropTypeDefault
as
React.ElementType
<RiArrowRightSLine />

HorizontalStepper.Item

Individual stepper item which can indicate the state of the step. This component is based on the <button> element and supports all of its props. And adds:

PropTypeDefault
asChild
boolean
state
"default"|"active"|"completed"
"default"

HorizontalStepper.ItemIndicator

This component is based on the <div> element and supports all of its props.

© 2024 AlignUI Design System. All rights reserved.