🔥 Oxide UI v0.1.0 — Dark mode only, copy-paste ready. Get started →
Skip to content

Stepper

<ol aria-label="Progress">

Guides users through a linear multi-step process. Shows completed, active, and upcoming steps with a connecting line.

Overview

Preview

Account

Profile

Plan

Review

Profile

Tell us a bit about yourself. This appears on your public profile.

Horizontal stepper

Preview
html
<nav aria-label="Progress">
  <ol class="flex items-center">

    <!-- Completed step -->
    <li class="flex flex-1 items-center">
      <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#D40C37] text-white text-xs font-semibold">
        <svg class="h-4 w-4" ...>✓</svg>
      </div>
      <div class="mx-2 flex-1 h-0.5 rounded-full bg-[#D40C37]"></div>
    </li>

    <!-- Active step -->
    <li class="flex flex-1 items-center">
      <div
        class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#D40C37] text-white
               text-xs font-semibold ring-4 ring-[#D40C37]/20"
        aria-current="step"
      >2</div>
      <div class="mx-2 flex-1 h-0.5 rounded-full bg-zinc-800"></div>
    </li>

    <!-- Upcoming step -->
    <li class="flex items-center">
      <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-2 border-zinc-700 text-zinc-500 text-xs font-semibold">
        3
      </div>
    </li>

  </ol>
</nav>

With labels

html
<!-- Labels sit below each step circle -->
<div class="mt-2 flex">
  <div class="flex-1 text-center">
    <p class="text-[10px] font-semibold uppercase tracking-widest text-zinc-500">Account</p>
  </div>
  <div class="flex-1 text-center">
    <p class="text-[10px] font-semibold uppercase tracking-widest text-zinc-200">Profile</p>
  </div>
  <div class="flex-1 text-center">
    <p class="text-[10px] font-semibold uppercase tracking-widest text-zinc-700">Plan</p>
  </div>
</div>

Full Vue implementation

vue
<script setup>
import { ref } from 'vue'

const current = ref(0)
const steps = [
  { label: 'Account', desc: 'Email and password.' },
  { label: 'Profile', desc: 'Your public profile details.' },
  { label: 'Plan',    desc: 'Choose a subscription plan.' },
  { label: 'Review',  desc: 'Confirm and submit.' },
]
</script>

<template>
  <!-- Step indicators -->
  <nav aria-label="Progress">
    <ol class="flex items-center">
      <li v-for="(step, i) in steps" :key="step.label" class="flex items-center" :class="{ 'flex-1': i < steps.length - 1 }">
        <button
          @click="current = i"
          class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold transition-all"
          :class="
            i < current  ? 'bg-[#D40C37] text-white' :
            i === current ? 'bg-[#D40C37] text-white ring-4 ring-[#D40C37]/20' :
                            'border-2 border-zinc-700 text-zinc-500 hover:border-zinc-500'
          "
          :aria-current="i === current ? 'step' : undefined"
        >
          <svg v-if="i < current" class="h-4 w-4" ...>✓</svg>
          <span v-else>{{ i + 1 }}</span>
        </button>
        <div v-if="i < steps.length - 1"
          class="mx-2 flex-1 h-0.5 rounded-full transition-colors duration-300"
          :class="i < current ? 'bg-[#D40C37]' : 'bg-zinc-800'"
        ></div>
      </li>
    </ol>
  </nav>

  <!-- Step content -->
  <div class="mt-6 rounded-xl border border-zinc-800 bg-zinc-900/50 p-5">
    <p class="text-sm font-semibold text-zinc-200">{{ steps[current].label }}</p>
    <p class="mt-1 text-xs text-zinc-500">{{ steps[current].desc }}</p>
  </div>

  <!-- Navigation -->
  <div class="mt-4 flex justify-between">
    <button
      @click="current--"
      :disabled="current === 0"
      class="rounded-md border border-zinc-700 px-4 py-2 text-sm font-medium text-zinc-300
             hover:bg-zinc-800 disabled:opacity-30 disabled:pointer-events-none transition-colors"
    >
      Back
    </button>
    <button
      @click="current < steps.length - 1 ? current++ : submit()"
      class="rounded-md bg-[#D40C37] px-4 py-2 text-sm font-medium text-white
             hover:bg-[#b50a2f] transition-colors"
    >
      {{ current === steps.length - 1 ? 'Submit' : 'Continue' }}
    </button>
  </div>
</template>

Accessibility notes

  • Use <ol> (ordered list) — step order is semantically meaningful
  • Mark the active step with aria-current="step"
  • Wrap in <nav aria-label="Progress"> to create a navigation landmark
  • If steps are clickable (go back), ensure they receive keyboard focus and have meaningful labels
  • Completed steps should visually distinguish from upcoming steps — don't rely on colour alone (use the checkmark icon)

Released under the MIT License.