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 optionaltitleanddescriptionfields.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,labelPlacementdefaults 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. Whentrueandverticalisfalse,labelPlacementdefaults to'bottom'.current: The currently active step (1-indexed). Making this property reactive enables clickable steps viav-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, andstep-pendingstates. - 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
currentprop is bound viav-model, steps become clickable, allowing users to navigate directly. - Event Handling: The component emits
update:currentandchangeevents 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.