Navigation

Optional Steps

Steps with optional stages among them: those are marked with a word and a dashed outline, while the counter on top counts only the required ones so the progress does not lie.

  • stepper
  • optional
  • onboarding
  • progress

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/stepper-008?lang=en

Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "stepper-008" (Optional Steps) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/stepper-008.json

Registry item: https://vibeui.ru/r/stepper-008.json
Installs to: components/vibeui/stepper-008.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
Steps with optional stages among them: those are marked with a word and a dashed outline, while the counter on top counts only the required ones so the progress does not lie.

Steps with optional stages: a word label, a dashed outline and a counter over required steps only. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Stepper008 } from "@/components/vibeui/stepper-008"

<Stepper008
  current={2}
  optionalLabel="Optional"
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-stepper-008-* palette — do not swap it for your theme tokens
- the "optional" word label: a dashed outline without a caption reads as a layout bug
- counting required steps only: otherwise progress looks understated and people abandon setup
- aria-current="step" on the current step: a wrapping list otherwise loses its focal point
- wrapping instead of horizontal scrolling: six steps do not fit one ribbon
- the component's own light surface: dark text disappears on the dark catalog card

## 6. You may change
- the steps and their optional flag through steps
- the active step number through current
- the badge text through optionalLabel
- the list's accessible name through label

## 7. Rules
- Completion is derived from the index: a skipped optional step looks done, there is no separate state.
- If more than half the steps are optional the counter stops helping — split the process in two.
- The items are not clickable: navigating to a step is the form's job, not the indicator's.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/stepper-008.json
https://vibeui.ru/r/stepper-008.json

Компонент самодостаточен: один файл, без зависимостей, палитра в переменных --vibeui-stepper-008-*. Серверный: счёт обязательных шагов идёт при рендере. Необязательность помечена трижды: словом в метке, пунктирной рамкой пункта и пунктирным кружком — цвет здесь не участвует вовсе. Счётчик сверху намеренно считает только обязательные шаги: «2 из 6» пугает, когда четыре шага можно пропустить. Пункты лежат в ol с переносом, поэтому длинный список не уезжает в прокрутку, а перестраивается в несколько рядов. Текущий шаг помечен aria-current="step", состояния продублированы скрытой фразой.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Stepper008Step = {
  title: string
  optional?: boolean
}

export type Stepper008Props = Omit<
  ComponentPropsWithoutRef<"nav">,
  "children"
> & {
  steps?: Stepper008Step[]
  /** Номер текущего шага, считая с нуля. */
  current?: number
  /** Подпись метки у необязательного шага. */
  optionalLabel?: string
  label?: string
  accent?: string
}

// Идея компонента: не все шаги обязательны, и это должно быть видно до того,
// как человек в них зайдёт. Необязательный шаг помечен словом и пунктирным
// кружком, а счётчик сверху считает только обязательные — иначе прогресс
// врёт: «2 из 6» пугает, когда четыре шага можно пропустить.
const STYLES = `
:where([data-vibeui-block="stepper-008"]){
--vibeui-stepper-008-bg:oklch(1 0 0);
--vibeui-stepper-008-fg:oklch(0.24 0.016 265);
--vibeui-stepper-008-muted:oklch(0.56 0.014 265);
--vibeui-stepper-008-border:oklch(0.92 0.006 265);
--vibeui-stepper-008-line:oklch(0.9 0.006 265);
--vibeui-stepper-008-accent:oklch(0.55 0.2 262);
--vibeui-stepper-008-accent-fg:oklch(1 0 0);
--vibeui-stepper-008-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="stepper-008"]{
width:100%;max-width:34rem;box-sizing:border-box;
font-family:var(--vibeui-stepper-008-font);color:var(--vibeui-stepper-008-fg);
}
[data-vibeui-block="stepper-008"] [data-part="shell"]{
background:var(--vibeui-stepper-008-bg);
border:1px solid var(--vibeui-stepper-008-border);
border-radius:1rem;padding:0.875rem 1rem 1rem;
}
[data-vibeui-block="stepper-008"] [data-part="counter"]{
margin:0 0 0.75rem;font-size:0.75rem;color:var(--vibeui-stepper-008-muted);
}
[data-vibeui-block="stepper-008"] [data-part="counter"] b{
color:var(--vibeui-stepper-008-fg);font-variant-numeric:tabular-nums;
}
[data-vibeui-block="stepper-008"] ol{
display:flex;flex-wrap:wrap;gap:0.375rem;margin:0;padding:0;list-style:none;
}
[data-vibeui-block="stepper-008"] li{
display:flex;align-items:center;gap:0.5rem;
padding:0.4375rem 0.6875rem;border-radius:9999px;
border:1px solid var(--vibeui-stepper-008-border);
background:var(--vibeui-stepper-008-bg);
}
[data-vibeui-block="stepper-008"] li[data-optional="true"]{border-style:dashed}
[data-vibeui-block="stepper-008"] li[data-state="current"]{
border-color:var(--vibeui-stepper-008-accent);
background:color-mix(in oklab,var(--vibeui-stepper-008-accent) 8%,transparent);
}
[data-vibeui-block="stepper-008"] [data-part="mark"]{
display:flex;align-items:center;justify-content:center;
width:1.125rem;height:1.125rem;border-radius:9999px;
border:1.5px solid var(--vibeui-stepper-008-line);
color:var(--vibeui-stepper-008-muted);font-size:0.625rem;font-weight:700;line-height:1;
}
[data-vibeui-block="stepper-008"] li[data-optional="true"] [data-part="mark"]{border-style:dashed}
[data-vibeui-block="stepper-008"] li[data-state="done"] [data-part="mark"]{
background:var(--vibeui-stepper-008-accent);border-color:var(--vibeui-stepper-008-accent);
color:var(--vibeui-stepper-008-accent-fg);
}
[data-vibeui-block="stepper-008"] li[data-state="current"] [data-part="mark"]{
border-color:var(--vibeui-stepper-008-accent);color:var(--vibeui-stepper-008-accent);
}
[data-vibeui-block="stepper-008"] [data-part="title"]{font-size:0.8125rem;font-weight:600;line-height:1.2}
[data-vibeui-block="stepper-008"] li[data-state="todo"] [data-part="title"]{
font-weight:500;color:var(--vibeui-stepper-008-muted);
}
[data-vibeui-block="stepper-008"] [data-part="tag"]{
padding:0.0625rem 0.375rem;border-radius:9999px;
background:color-mix(in oklab,var(--vibeui-stepper-008-line) 45%,transparent);
color:var(--vibeui-stepper-008-muted);
font-size:0.625rem;font-weight:650;letter-spacing:0.01em;white-space:nowrap;
}
[data-vibeui-block="stepper-008"] [data-part="sr"]{
position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="stepper-008"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_STEPS: Stepper008Step[] = [
  { title: "Профиль" },
  { title: "Команда", optional: true },
  { title: "Тариф" },
  { title: "Интеграции", optional: true },
  { title: "Домен", optional: true },
  { title: "Публикация" },
]

/**
 * Шаги, среди которых есть необязательные: они помечены словом и пунктиром.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Stepper008({
  steps = DEFAULT_STEPS,
  current = 2,
  optionalLabel = "Необязательно",
  label = "Настройка рабочего пространства",
  accent,
  className,
  style,
  ...props
}: Stepper008Props) {
  const palette = {
    ...(accent ? { "--vibeui-stepper-008-accent": accent } : null),
    ...style,
  } as CSSProperties

  const required = steps.filter((step) => !step.optional)
  const requiredDone = steps.filter(
    (step, index) => !step.optional && index < current,
  )

  return (
    <>
      <style href="vibeui-stepper-008" precedence="medium">
        {STYLES}
      </style>
      <nav
        {...props}
        data-vibeui-block="stepper-008"
        aria-label={label}
        className={className}
        style={palette}
      >
        <div data-part="shell">
          <p data-part="counter">
            Обязательных пройдено{" "}
            <b>
              {requiredDone.length} из {required.length}
            </b>
            . Остальное можно пропустить и вернуться позже.
          </p>
          <ol>
            {steps.map((step, index) => {
              const state =
                index < current
                  ? "done"
                  : index === current
                    ? "current"
                    : "todo"

              return (
                <li
                  key={step.title}
                  data-state={state}
                  data-optional={step.optional ? "true" : undefined}
                  aria-current={state === "current" ? "step" : undefined}
                >
                  <span data-part="mark" aria-hidden="true">
                    {state === "done" ? "✓" : index + 1}
                  </span>
                  <span data-part="title">{step.title}</span>
                  {step.optional ? (
                    <span data-part="tag">{optionalLabel}</span>
                  ) : null}
                  <span data-part="sr">
                    {state === "done"
                      ? " — шаг пройден"
                      : state === "current"
                        ? " — текущий шаг"
                        : " — впереди"}
                  </span>
                </li>
              )
            })}
          </ol>
        </div>
      </nav>
    </>
  )
}