Stepper

A set of steps that are used to indicate progress through a multi-step process.

Examples

Basic

PropDefaultTypeDescription
items[]TThe array of steps that is passed to the stepper.
defaultValue1numberThe value of the step that should be active when initially rendered.
dir-ltr | rtlSets the reading direction of the stepper.
lineartruebooleanWhether or not the steps must be completed in order.
modelValue-numberThe controlled value of the step to activate. Can be bound as v-model.
orientationhorizontalhorizontal | verticalThe orientation the steps are laid out. Mainly so arrow navigation is done accordingly (left & right vs. up & down).
disabledfalsebooleanWhen true, prevents the user from interacting with the stepper.
Preview
Code

Address

Add your address here

Shipping

Set your preferred shipping method

Checkout

Confirm your order

Step 0 of 0

Orientation

PropDefaultTypeDescription
orientationhorizontalhorizontal | verticalSet the orientation of the stepper.
Preview
Code

Address

Add your address here

Shipping

Set your preferred shipping method

Checkout

Confirm your order

Step 0 of 0

Variant and Color

PropDefaultTypeDescription
steppersolid-primary{variant}-{color}Set the stepper variant and color.
_stepperIndicator.steppersolid-primary{variant}-{color}Set the stepper indicator variant and color via _stepperIndicator.
Preview
Code

Address

Add your address here

Shipping

Set your preferred shipping method

Checkout

Confirm your order

Step 0 of 0

Address

Add your address here

Shipping

Set your preferred shipping method

Checkout

Confirm your order

Step 0 of 0

Size

Adjust the stepper size without limits. Use breakpoints (e.g., sm:sm, xs:lg) for responsive sizes or states (e.g., hover:lg, focus:3xl) for state-based sizes.

PropDefaultTypeDescription
sizemdstringAdjusts the overall size of the stepper component.
item.sizemdstringapplies only if not specified size
_stepperTrigger.sizemdstringModifies the size of the stepper trigger element.
_stepperTitle.sizemdstringAdjusts the size of the stepper title.
_stepperDescription.sizemdstringAdjusts the size of the stepper description.
Preview
Code

Address

Shipping

Checkout

Step 0 of 0

Exposed

NameTypeDescription
goToStep(step: number) => voidNavigates to the specified step by its index.
nextStep() => voidMoves to the next step (if available).
prevStep() => voidMoves to the previous step (if available).
hasNext() => booleanChecks if there is a next step available.
hasPrev() => booleanChecks if there is a previous step available.

Slots

NamePropsDescription
defaultmodelValue, totalSteps, isNextDisabled, isPrevDisabled, isFirstStep, isLastStep, goToStep, nextStep, prevStep, hasNext, hasPrev, currentStepDefault slot that overrides entire stepper content
wrapperitemsWraps all stepper items for custom layouts
itemitem, stepCustomizes each individual step
triggeritemOverrides clickable trigger area of each step
indicatoritem, stepCustomizes step indicator
headeritemCustomizes header section of each step
titleitemOverrides step title text
descriptionitemOverrides step description text
contentitemDynamically renders content for active step
#{{item.slot}}itemDynamic named slot when item.slot is defined
Preview
Code

Your details

Provide your name and email

Company details

A few details about your company

Invite your team

Start collaborating with your team

Your details

Step 0 of 0

Custom Rendering

Use the default slot for full control over the steppers's structure. This allows you to compose the stepper using its individual sub-components (like NStepperItem, NStepperTrigger, etc., listed in the Components section), similar to libraries like shadcn/ui.

Preview
Code

Address

Add your address here

Shipping

Set your preferred shipping method

Checkout

Confirm your order

Step 0 of 0

Presets

shortcuts/stepper.ts
type StepperPrefix = 'stepper'

export const staticStepper: Record<`${StepperPrefix}-${string}` | StepperPrefix, string> = {
  // configurations
  'stepper': 'flex gap-4',
  'stepper-horizontal': 'flex-col',

  // components
  'stepper-wrapper': 'flex',
  'stepper-wrapper-vertical': 'flex-col gap-6',

  'stepper-item': 'text-center relative w-full data-[disabled]:pointer-events-none',
  'stepper-item-vertical': 'flex text-start gap-2.5',

  'stepper-trigger': 'rounded-full font-medium text-center align-middle flex items-center justify-center font-semibold text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background',

  'stepper-header': 'flex flex-col',
  'stepper-header-horizontal': 'mt-2.5',

  'stepper-indicator': 'inline-flex items-center justify-center rounded-full square-2.25em text-foreground/50 bg-muted group-data-[state=active]:text-inverted group-data-[state=completed]:text-inverted',

  'stepper-separator': 'bg-muted group-data-[disabled]:bg-muted group-data-[state=active]:!bg-muted group-data-[disabled]:opacity-50 absolute rounded-full group-data-[disabled]:opacity-75',
  'stepper-separator-horizontal': 'top-[calc(50%-2px)] h-0.5 start-[calc(50%+1.5em)] end-[calc(-50%+1.5em)]',
  'stepper-separator-vertical': 'start-[calc(50%-1px)] bottom-[calc(50%-2.8em)] w-0.5 top-[calc(50%+1em)]',
  'stepper-title': 'text-md font-semibold whitespace-nowrap',
  'stepper-description': 'text-muted text-wrap text-sm',

  'stepper-container': 'relative',
  'stepper-container-horizontal': 'flex justify-center',

  // static variants
  'stepper-solid-black': 'group-data-[state=active]:bg-inverted group-data-[state=completed]:bg-inverted focus-visible:ring-$c-foreground',
}

export const dynamicStepper: [RegExp, (params: RegExpExecArray) => string][] = [
  // dynamic presets
  [/^stepper-solid(-(\S+))?$/, ([, , c = 'primary']) => `group-data-[state=completed]:bg-${c}-600 group-data-[state=active]:bg-${c}-600 focus-visible:ring-${c}-600 dark:(group-data-[state=completed]:bg-${c}-500 group-data-[state=active]:bg-${c}-500 focus-visible:ring-${c}-500)`],
]

export const stepper = [
  ...dynamicStepper,
  staticStepper,
]

Props

types/stepper.ts
import type {
  StepperDescriptionProps,
  StepperIndicatorProps,
  StepperItemProps,
  StepperRootEmits,
  StepperRootProps,
  StepperSeparatorProps,
  StepperTitleProps,
  StepperTriggerProps,
} from 'reka-ui'
import type { HTMLAttributes } from 'vue'

interface BaseExtensions {
  /** CSS class for the component */
  class?: HTMLAttributes['class']
  /** Size of the component */
  size?: HTMLAttributes['class']
}

export interface NStepperProps<T> extends StepperRootProps, Pick<StepperItemProps, 'disabled'>, BaseExtensions {
  /**
   * The array of steps that is passed to the stepper.
   *
   * @default []
   */
  items?: T[]
  /**
   * Allows you to add `UnaUI` stepper preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/stepper.ts
   * @example
   * stepper="solid-yellow"
   */
  stepper?: string

  /** Props for the stepper item */
  _stepperItem?: Partial<NStepperItemProps>
  /** Props for the stepper trigger */
  _stepperTrigger?: Partial<NStepperTriggerProps>
  /** Props for the stepper separator */
  _stepperSeparator?: Partial<NStepperSeparatorProps>
  /** Props for the stepper title */
  _stepperTitle?: Partial<NStepperTitleProps>
  /** Props for the stepper description */
  _stepperDescription?: Partial<NStepperDescriptionProps>
  /** Props for the stepper indicator */
  _stepperIndicator?: Partial<NStepperIndicatorProps>
  /** Props for the stepper header */
  _stepperHeader?: Partial<NStepperHeaderProps>
  /** Props for the stepper wrapper */
  _stepperWrapper?: Partial<NStepperWrapperProps>
  /** Props for the stepper container */
  _stepperContainer?: Partial<NStepperContainerProps>

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/stepper.ts
   */
  una?: NStepperUnaProps
}

export type NStepperEmits<T> = StepperRootEmits & {
  next: [payload: T]
  prev: [payload: T]
}

export interface NStepperItemProps extends StepperItemProps, Pick<NStepperProps<NStepperItemProps>, 'orientation'>,
  BaseExtensions {
  /** Slot of the stepper item */
  slot?: string
  /** Title of the stepper item. */
  title?: string
  /** Description of the stepper item. */
  description?: string

  value?: string | number
  /**
   * Icon name
   * @see @IconifyIcon
   *
   * @example
   * 'i-heroicons-book-open-20-solid'
   */
  icon?: string
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperItem'>
}

export interface NStepperTriggerProps extends StepperTriggerProps, Pick<NStepperProps<StepperItemProps>, 'stepper'>, BaseExtensions {
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperTrigger' | 'stepperMenuDefaultVariant'>
}

export interface NStepperSeparatorProps extends StepperSeparatorProps, Pick<NStepperProps<StepperItemProps>, 'stepper'>,
  Pick<BaseExtensions, 'class'> {
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperSeparator'>
}

export interface NStepperIndicatorProps extends StepperIndicatorProps, Pick<NStepperProps<StepperItemProps>, 'stepper'>, BaseExtensions {
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperIndicator'>
}

export interface NStepperHeaderProps extends Pick<BaseExtensions, 'class'>, Pick<NStepperProps<StepperItemProps>, 'orientation'> {
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperHeader'>
}

export interface NStepperTitleProps extends StepperTitleProps, BaseExtensions {
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperTitle'>
}

export interface NStepperDescriptionProps extends StepperDescriptionProps, BaseExtensions {
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperDescription'>
}

export interface NStepperContainerProps extends Pick<BaseExtensions, 'class'>, Pick<NStepperProps<StepperItemProps>, 'orientation'> {
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperContainer'>
}

export interface NStepperWrapperProps extends Pick<BaseExtensions, 'class'>, Pick<NStepperProps<StepperItemProps>, 'orientation'> {
  /** Additional properties for the una component */
  una?: Pick<NStepperUnaProps, 'stepperWrapper'>
}

interface NStepperUnaProps {
  /** CSS class for the stepper */
  stepper?: HTMLAttributes['class']
  /** CSS class for the stepper trigger */
  stepperTrigger?: HTMLAttributes['class']
  /** CSS class for the stepper trigger default variant */
  stepperMenuDefaultVariant?: HTMLAttributes['class']
  /** CSS class for the stepper item */
  stepperItem?: HTMLAttributes['class']
  /** CSS class for the stepper title */
  stepperTitle?: HTMLAttributes['class']
  /** CSS class for the stepper description */
  stepperDescription?: HTMLAttributes['class']
  /** CSS class for the stepper indicator */
  stepperIndicator?: HTMLAttributes['class']
  /** CSS class for the stepper separator */
  stepperSeparator?: HTMLAttributes['class']
  /** CSS class for the stepper header */
  stepperHeader?: HTMLAttributes['class']
  /** CSS class for the stepper container */
  stepperContainer?: HTMLAttributes['class']
  /** CSS class for the stepper wrapper */
  stepperWrapper?: HTMLAttributes['class']
}

Components

Stepper.vue
StepperContainer.vue
StepperDescription.vue
StepperHeader.vue
StepperIndicator.vue
StepperItem.vue
StepperSeparator.vue
StepperTitle.vue
StepperTrigger.vue
StepperWrapper.vue
<script lang="ts" setup generic="T extends Partial<NStepperItemProps>">
import type { ComputedRef } from 'vue'
import type { NStepperEmits, NStepperItemProps, NStepperProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { StepperRoot, useForwardPropsEmits } from 'reka-ui'
import { computed, useTemplateRef } from 'vue'
import { cn } from '../../utils'
import Icon from '../elements/Icon.vue'
import StepperContainer from './StepperContainer.vue'
import StepperDescription from './StepperDescription.vue'
import StepperHeader from './StepperHeader.vue'
import StepperIndicator from './StepperIndicator.vue'
import StepperItem from './StepperItem.vue'
import StepperSeparator from './StepperSeparator.vue'
import StepperTitle from './StepperTitle.vue'
import StepperTrigger from './StepperTrigger.vue'
import StepperWrapper from './StepperWrapper.vue'

const props = withDefaults(defineProps<NStepperProps<T>>(), {
  orientation: 'horizontal',
  stepper: 'solid-primary',
})
const emits = defineEmits<NStepperEmits<T>>()

const modelValue = defineModel<string | number>()

const delegatedProps = reactiveOmit(props, [
  'class',
  'una',
])
const forwarded = useForwardPropsEmits(delegatedProps, emits)

const stepper = useTemplateRef<InstanceType<typeof StepperRoot>>('stepper')

const currentStepIndex = computed({
  get() {
    const value = modelValue.value ?? props.defaultValue
    return ((typeof value === 'string')
      ? props.items?.findIndex(item => item.value === value)
      : value) ?? 0
  },
  set(value) {
    modelValue.value = props.items?.[value]?.value ?? value
  },
})
const currentStep = computed(() => props.items?.[currentStepIndex.value]) as ComputedRef<T>
const isEveryItemHasStep = computed(() => props.items?.every(item => item.step))

const hasNextStep = computed(() => currentStepIndex.value < props.items!.length - 1)
const hasPrevStep = computed(() => currentStepIndex.value > 0)

defineExpose({
  goToStep: (step: number) => {
    currentStepIndex.value = step
    stepper.value?.goToStep(step)
  },
  nextStep: () => {
    if (!hasNextStep.value)
      return
    currentStepIndex.value += 1
    stepper.value?.nextStep()
    emits('next', currentStep.value)
  },
  prevStep: () => {
    if (!hasPrevStep.value)
      return
    currentStepIndex.value -= 1
    stepper.value?.prevStep()
    emits('prev', currentStep.value)
  },
  hasNext: () => stepper.value?.hasNext(),
  hasPrev: () => stepper.value?.hasPrev(),
})
</script>

<template>
  <StepperRoot
    ref="stepper"
    v-slot="slotProps"
    v-bind="forwarded"
    v-model="currentStepIndex"
    :class="cn(
      'stepper',
      orientation === 'horizontal' && 'stepper-horizontal',
      props.class,
    )"
    :una
  >
    <slot v-bind="slotProps">
      <StepperWrapper :una v-bind="props._stepperWrapper" :orientation>
        <slot name="wrapper" :items>
          <StepperItem
            v-for="(item, idx) in items"
            :key="isEveryItemHasStep ? item.step : idx"
            :step="isEveryItemHasStep ? item.step! : idx"
            :disabled="item.disabled ?? props.disabled"
            :una
            v-bind="props._stepperItem"
            :orientation
          >
            <slot name="item" :item="item" :step="isEveryItemHasStep ? item.step! : idx">
              <StepperContainer :orientation :una v-bind="props._stepperContainer">
                <StepperTrigger v-bind="props._stepperTrigger" :una :stepper="props.stepper" :size="size ?? item.size">
                  <slot name="trigger" :item="item">
                    <StepperIndicator
                      v-slot="{ step }"
                      v-bind="props._stepperIndicator"
                      :una
                      :size="size ?? item.size"
                      :stepper="props.stepper"
                    >
                      <slot name="indicator" :item :step>
                        <Icon v-if="item.icon" :name="item.icon" :size="size ?? item.size" />
                        <template v-else>
                          {{ idx + 1 }}
                        </template>
                      </slot>
                    </StepperIndicator>
                  </slot>
                </StepperTrigger>
                <StepperSeparator
                  v-if="items && idx < items.length - 1"
                  v-bind="props._stepperSeparator"
                  :una
                  :stepper="props.stepper"
                  :orientation
                />
              </StepperContainer>
              <StepperHeader :una v-bind="props._stepperHeader" :orientation>
                <slot name="header" :item="item">
                  <StepperTitle v-if="item.title" :una v-bind="props._stepperTitle" :size="size ?? item.size">
                    <slot name="title" :item="item">
                      {{ item.title }}
                    </slot>
                  </StepperTitle>
                  <StepperDescription v-if="item.description" :una v-bind="props._stepperDescription" :size="size ?? item.size">
                    <slot name="description" :item="item">
                      {{ item.description }}
                    </slot>
                  </StepperDescription>
                </slot>
              </StepperHeader>
            </slot>
          </StepperItem>
        </slot>
      </StepperWrapper>
      <slot
        v-if="!!$slots.content || currentStep?.slot"
        :name="currentStep?.slot ?? 'content'"
        :item="currentStep"
      />
    </slot>
  </StepperRoot>
</template>