Navigation

Connector Progress

Steps where the progress lives inside the connector: the line before the current step is filled only part of the way, and the same fraction is printed as a number.

  • stepper
  • progress
  • connector
  • loading

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-005?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-005" (Connector Progress) 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-005.json

Registry item: https://vibeui.ru/r/stepper-005.json
Installs to: components/vibeui/stepper-005.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 where the progress lives inside the connector: the line before the current step is filled only part of the way, and the same fraction is printed as a number.

Steps with a partly filled connector: readiness shows as both a bar and a number. Zero dependencies, one file, no client JS.

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

<Stepper005
  current={2}
  progress={45}
/>

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-005-* palette — do not swap it for your theme tokens
- the unitless fraction: a percentage inside calc(100% * …) does not resolve to a width
- the printed percentage beside the bar: a fill is not announced on its own
- aria-current="step" on the current step: without it the percentage has nothing to attach to
- the prefers-reduced-motion rule: the fill animates its width and that must be switchable
- the component's own light surface: dark text disappears on the dark catalog card

## 6. You may change
- the step names through steps
- the active step number through current
- the readiness fraction through progress
- the list's accessible name through label

## 7. Rules
- progress is clamped to 0–100: an out-of-range value will not break the layout but will not render literally.
- The fraction describes the transition into the current step, not the work inside it — different quantities.
- The first step has no connector, so progress cannot show there: start from current={1}.
- 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-005.json
https://vibeui.ru/r/stepper-005.json

Компонент самодостаточен: один файл, без зависимостей, палитра в переменных --vibeui-stepper-005-*. Серверный: хуков нет. Дорожка и заливка соединителя — два псевдоэлемента одного пункта; ширина заливки считается как calc(100% * var(--vibeui-stepper-005-fill)), где переменная приходит инлайново безразмерным числом: умножать проценты на проценты CSS не умеет. Пройденные шаги получают заливку 1, текущий — долю готовности, будущие 0. Процент продублирован текстом под кружком: полоска для скринридера не существует. Текущий шаг помечен 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 Stepper005Props = Omit<
  ComponentPropsWithoutRef<"nav">,
  "children"
> & {
  steps?: string[]
  /** Номер текущего шага, считая с нуля. */
  current?: number
  /** Готовность текущего шага в процентах: заполняет соединитель перед ним. */
  progress?: number
  label?: string
  accent?: string
}

// Идея компонента: прогресс живёт внутри соединителя. Линия перед текущим
// шагом залита не целиком, а на долю готовности — видно, что шаг начат, но
// не закончен. Долю несёт unitless-переменная, ширина считается через
// calc(100% * var(...)), поэтому проценты не приходится складывать с
// процентами. Число продублировано текстом: полоска сама по себе неозвучиваема.
const STYLES = `
:where([data-vibeui-block="stepper-005"]){
--vibeui-stepper-005-bg:oklch(1 0 0);
--vibeui-stepper-005-fg:oklch(0.24 0.016 265);
--vibeui-stepper-005-muted:oklch(0.56 0.014 265);
--vibeui-stepper-005-border:oklch(0.92 0.006 265);
--vibeui-stepper-005-line:oklch(0.9 0.006 265);
--vibeui-stepper-005-accent:oklch(0.55 0.2 262);
--vibeui-stepper-005-accent-fg:oklch(1 0 0);
--vibeui-stepper-005-size:1.75rem;
--vibeui-stepper-005-fill:0;
--vibeui-stepper-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="stepper-005"]{
container-type:inline-size;width:100%;box-sizing:border-box;
font-family:var(--vibeui-stepper-005-font);color:var(--vibeui-stepper-005-fg);
}
[data-vibeui-block="stepper-005"] [data-part="shell"]{
background:var(--vibeui-stepper-005-bg);
border:1px solid var(--vibeui-stepper-005-border);
border-radius:1rem;padding:1.125rem 1rem 1rem;
}
[data-vibeui-block="stepper-005"] ol{display:flex;margin:0;padding:0;list-style:none}
[data-vibeui-block="stepper-005"] li{
position:relative;flex:1 1 0;min-width:0;
display:flex;flex-direction:column;align-items:center;text-align:center;gap:0.375rem;
}
/* Дорожка соединителя и её заливка — два псевдоэлемента одного пункта. */
[data-vibeui-block="stepper-005"] li:not(:first-child)::before,
[data-vibeui-block="stepper-005"] li:not(:first-child)::after{
content:"";position:absolute;left:-50%;
top:calc(var(--vibeui-stepper-005-size) / 2 - 2px);height:4px;border-radius:9999px;
}
[data-vibeui-block="stepper-005"] li:not(:first-child)::before{
width:100%;background:var(--vibeui-stepper-005-line);
}
[data-vibeui-block="stepper-005"] li:not(:first-child)::after{
width:calc(100% * var(--vibeui-stepper-005-fill));
background:var(--vibeui-stepper-005-accent);
transition:width .25s ease;
}
[data-vibeui-block="stepper-005"] [data-part="mark"]{
position:relative;z-index:1;
display:flex;align-items:center;justify-content:center;
width:var(--vibeui-stepper-005-size);height:var(--vibeui-stepper-005-size);
border-radius:9999px;border:2px solid var(--vibeui-stepper-005-line);
background:var(--vibeui-stepper-005-bg);color:var(--vibeui-stepper-005-muted);
font-size:0.75rem;font-weight:700;line-height:1;
}
[data-vibeui-block="stepper-005"] li[data-state="done"] [data-part="mark"]{
background:var(--vibeui-stepper-005-accent);border-color:var(--vibeui-stepper-005-accent);
color:var(--vibeui-stepper-005-accent-fg);
}
[data-vibeui-block="stepper-005"] li[data-state="current"] [data-part="mark"]{
border-color:var(--vibeui-stepper-005-accent);color:var(--vibeui-stepper-005-accent);
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-stepper-005-accent) 18%,transparent);
}
[data-vibeui-block="stepper-005"] [data-part="label"]{font-size:0.8125rem;font-weight:500;line-height:1.25}
[data-vibeui-block="stepper-005"] li[data-state="todo"] [data-part="label"]{color:var(--vibeui-stepper-005-muted)}
[data-vibeui-block="stepper-005"] li[data-state="current"] [data-part="label"]{font-weight:650}
[data-vibeui-block="stepper-005"] [data-part="percent"]{
font-size:0.6875rem;font-weight:650;color:var(--vibeui-stepper-005-accent);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="stepper-005"] [data-part="done-word"]{
font-size:0.6875rem;color:var(--vibeui-stepper-005-muted);
}
@container (max-width: 28rem){
[data-vibeui-block="stepper-005"] [data-part="shell"] [data-part="done-word"]{display:none}
[data-vibeui-block="stepper-005"] [data-part="shell"] [data-part="label"]{font-size:0.6875rem}
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="stepper-005"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_STEPS = ["Загрузка", "Проверка", "Обработка", "Отчёт"]

/**
 * Шаги, где соединитель перед текущим залит на долю готовности.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Stepper005({
  steps = DEFAULT_STEPS,
  current = 2,
  progress = 45,
  label = "Импорт каталога",
  accent,
  className,
  style,
  ...props
}: Stepper005Props) {
  const palette = {
    ...(accent ? { "--vibeui-stepper-005-accent": accent } : null),
    ...style,
  } as CSSProperties

  const clamped = Math.min(100, Math.max(0, progress))

  return (
    <>
      <style href="vibeui-stepper-005" precedence="medium">
        {STYLES}
      </style>
      <nav
        {...props}
        data-vibeui-block="stepper-005"
        aria-label={label}
        className={className}
        style={palette}
      >
        <div data-part="shell">
          <ol>
            {steps.map((step, index) => {
              const state =
                index < current
                  ? "done"
                  : index === current
                    ? "current"
                    : "todo"
              const fill =
                index <= current - 1 ? 1 : index === current ? clamped / 100 : 0

              return (
                <li
                  key={step}
                  data-state={state}
                  style={
                    {
                      "--vibeui-stepper-005-fill": String(fill),
                    } as CSSProperties
                  }
                  aria-current={state === "current" ? "step" : undefined}
                >
                  <span data-part="mark" aria-hidden="true">
                    {state === "done" ? "✓" : index + 1}
                  </span>
                  <span data-part="label">{step}</span>
                  {state === "current" ? (
                    <span data-part="percent">{clamped}% готово</span>
                  ) : null}
                  {state === "done" ? (
                    <span data-part="done-word">Готово</span>
                  ) : null}
                </li>
              )
            })}
          </ol>
        </div>
      </nav>
    </>
  )
}