Implementing a Steps Component in Vue 3

This comopnent provides a visual indicator for a process consisting of sequential steps. It supports horizontal and vertical layouts, configurable sizes, and interactive step selection.

Core Properties

  • items: An array of step objects, each containing optional title and description fields.
  • width: Controls the total width of the component; accepts a number (pixels) or string value.
  • size: Determines the step size; options are 'default' or 'small'.
  • vertical: A boolean flag to render steps vertically. When enabled, labelPlacement defaults to 'right'.
  • labelPlacement: Positions the label relative to the step icon; accepts 'right' or 'bottom'.
  • dotted: A boolean flag to use a dot-based step indicator. When true and vertical is false, labelPlacement defaults to 'bottom'.
  • current: The currently active step (1-indexed). Making this property reactive enables clickable steps via v-model:current.

Component Structure

The component is built using Vue 3's Composition API with TypeScript.

<script setup lang="ts">
import { computed } from 'vue';
import { useInject } from './utils';

export interface StepItem {
  title?: string;
  description?: string;
}

export interface StepProps {
  items?: StepItem[];
  width?: number | string;
  size?: 'default' | 'small';
  vertical?: boolean;
  labelPlacement?: 'right' | 'bottom';
  dotted?: boolean;
  current?: number;
}

const props = withDefaults(defineProps<StepProps>(), {
  items: () => [],
  width: 'auto',
  size: 'default',
  vertical: false,
  labelPlacement: 'right',
  dotted: false,
  current: 1
});

const { colorPalettes } = useInject('Steps');
const emit = defineEmits(['update:current', 'change']);

const computedWidth = computed(() => {
  return typeof props.width === 'number' ? `${props.width}px` : props.width;
});

const stepCount = computed(() => props.items.length);

const activeStep = computed(() => {
  if (props.current < 1) return 1;
  if (props.current > stepCount.value + 1) return stepCount.value + 1;
  return props.current;
});

function handleStepClick(stepIndex: number): void {
  if (activeStep.value !== stepIndex) {
    emit('update:current', stepIndex);
    emit('change', stepIndex);
  }
}
</script>

<template>
  <div
    class="steps-container"
    :class="[
      `steps-${size}`,
      { 'steps-vertical': vertical },
      { 'steps-label-bottom': !vertical && (labelPlacement === 'bottom' || dotted) },
      { 'steps-dotted': dotted }
    ]"
    :style="`
      --steps-width: ${computedWidth};
      --steps-primary: ${colorPalettes[5]};
      --steps-primary-hover: ${colorPalettes[5]};
      --steps-icon-base: ${colorPalettes[0]};
      --steps-icon-hover: ${colorPalettes[5]};
    `"
  >
    <div
      v-for="(item, index) in items"
      :key="index"
      class="step-item"
      :class="{
        'step-completed': activeStep > index + 1,
        'step-active': activeStep === index + 1,
        'step-pending': activeStep < index + 1
      }"
    >
      <div
        tabindex="0"
        class="step-content-wrapper"
        @click="handleStepClick(index + 1)"
      >
        <div class="step-connector"></div>
        <div class="step-icon">
          <template v-if="!dotted">
            <span v-if="activeStep <= index + 1" class="step-number">{{ index + 1 }}</span>
            <svg
              v-else
              class="icon-checkmark"
              focusable="false"
              width="1em"
              height="1em"
              fill="currentColor"
              viewBox="64 64 896 896"
            >
              <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
            </svg>
          </template>
          <template v-else>
            <span class="step-dot"></span>
          </template>
        </div>
        <div class="step-details">
          <div class="step-title">{{ item.title }}</div>
          <div v-if="item.description" class="step-description">{{ item.description }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

Styling with CSS/LESS

The component uses a comprehensive stylesheet to manage layout, states, and visual themes. Key styling concepts include:

  • Dynamic CSS Variables: Colors and dimensions are controlled via CSS custom properties for theme consistency.
  • State-Based Styling: Different visual styles are applied for step-completed, step-active, and step-pending states.
  • Layout Variations: Conditional classes handle horizontal vs. vertical layouts, label positioning, and dotted styles.
  • Transitions: Smmooth transitions are applied for interactive states like hover and active steps.

Usage Example

The component can be integrated into a parent Vue component as shown below:

<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import Steps from './Steps.vue';

const stepList = ref([
  { title: 'Step 1', description: 'First step description' },
  { title: 'Step 2', description: 'Second step description' },
  { title: 'Step 3', description: 'Third step description' },
  { title: 'Step 4', description: 'Fourth step description' },
  { title: 'Step 5', description: 'Fifth step description' }
]);

const activeStepIndex = ref(3);

watchEffect(() => {
  console.log('Active step changed to:', activeStepIndex.value);
});

function handleStepChange(newIndex: number) {
  console.log('Step changed to index:', newIndex);
}
</script>

<template>
  <div>
    <h2>Basic Steps</h2>
    <Steps :items="stepList" :current="activeStepIndex" @change="handleStepChange" />

    <h2>Vertical Steps</h2>
    <Steps :items="stepList" vertical :current="activeStepIndex" />

    <h2>Interactive Steps</h2>
    <Steps :items="stepList" v-model:current="activeStepIndex" />
  </div>
</template>

Interactive Features

  • Step Navigation: When the current prop is bound via v-model, steps become clickable, allowing users to navigate directly.
  • Event Handling: The component emits update:current and change events when the active step changes.
  • Customizable Appearance: Through a combinasion of props, the steps' size, layout, label position, and indicator style can be adjusted.
  • Theme Integration: The component leverages a centralized color palette via dependency injection for consistent theming.

Tags: Vue 3 TypeScript Component Design UI Development Steps

Posted on Thu, 07 May 2026 12:54:00 +0000 by Barand