Display

Labelled Progress

A progress bar with a label and an honest unknown-duration state: when the percentage is not known, the component does not invent one.

  • progress
  • loading
  • indicator
  • status

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/progress-001?lang=en

Building the project64%
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 "progress-001" (Labelled 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/progress-001.json

Registry item: https://vibeui.ru/r/progress-001.json
Installs to: components/vibeui/progress-001.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
A progress bar with a label and an honest unknown-duration state: when the percentage is not known, the component does not invent one.

A progress bar with a label on the left and a value on the right. Two honest states: known progress shows the fill and the percentage, unknown progress runs a segment along the track and leaves aria-valuenow unset. Two sizes, value clamped to 0–100, full progressbar semantics. Zero dependencies, one file, its own palette.

## 3. How to use it
import { Progress001 } from "@/components/vibeui/progress-001"

<Progress001 value={64} label="Building the project" />

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-progress-001-* palette — do not swap it for your theme tokens (bg-primary, bg-secondary and the like)
- the two states as genuinely different: an unknown duration must not be shown as an invented percentage
- the accessibility markup: role="progressbar", aria-valuemin, aria-valuemax and aria-valuenow only when the value is known
- clamping the value to 0–100: a bar wider than its track reads as a bug
- driving the fill width through the --vibeui-progress-001-value variable rather than a string in style
- tabular-nums on the value: without them the percentage jitters the label on every update
- the prefers-reduced-motion rule that stops the travelling segment
- the <style> block inside the component — it holds the palette, the sizes and the animation

## 6. You may change
- value — a number from 0 to 100, or null when the duration is unknown
- label — what is being done
- hint — the right-hand caption when a percentage is wrong: "3 of 8", "12 MB"
- size: md on its own, sm inside a list row
- the accent through the accent prop — it colours the fill
- width and outer spacing through className

## 7. Rules
- Do not use this as a slider: it is an indicator with no input.
- Do not animate the value on a timer for looks — progress has to reflect the real state.
- If the operation runs longer than ten seconds, add text about what is happening: a bar alone is not enough.
- 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/progress-001.json
https://vibeui.ru/r/progress-001.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-progress-001-*. Заполнение считается из переменной --vibeui-progress-001-value, поэтому ширина полосы задаётся числом, а не строкой со знаком процента. value={null} включает состояние неизвестной длительности: отрезок ходит по дорожке анимацией, aria-valuenow не выставляется. Значение зажимается в диапазон 0–100.

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 Progress001Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  /** Значение 0–100. `null` — процесс идёт, но длительность неизвестна. */
  value?: number | null
  label?: string
  /** Правая подпись: по умолчанию проценты. */
  hint?: string
  size?: "sm" | "md"
  accent?: string
}

// Идея компонента: у полосы два честных состояния. Известен прогресс — она
// показывает долю и число; неизвестен — по дорожке ходит отрезок, и никакой
// выдуманный процент не рисуется.
const STYLES = `
:where([data-vibeui-block="progress-001"]){
--vibeui-progress-001-fg:oklch(0.28 0.016 265);
--vibeui-progress-001-muted:oklch(0.54 0.014 265);
--vibeui-progress-001-track:oklch(0.92 0.006 265);
--vibeui-progress-001-accent:oklch(0.55 0.2 262);
--vibeui-progress-001-height:0.5rem;
--vibeui-progress-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="progress-001"]{
display:flex;flex-direction:column;gap:0.5rem;width:100%;
font-family:var(--vibeui-progress-001-font);color:var(--vibeui-progress-001-fg);
}
[data-vibeui-block="progress-001"][data-size="sm"]{--vibeui-progress-001-height:0.25rem}
[data-vibeui-block="progress-001"] [data-part="head"]{
display:flex;align-items:baseline;justify-content:space-between;gap:1rem;
font-size:0.8125rem;
}
[data-vibeui-block="progress-001"] [data-part="hint"]{
color:var(--vibeui-progress-001-muted);font-variant-numeric:tabular-nums;
}
[data-vibeui-block="progress-001"] [data-part="track"]{
position:relative;overflow:hidden;
height:var(--vibeui-progress-001-height);
border-radius:9999px;background:var(--vibeui-progress-001-track);
}
[data-vibeui-block="progress-001"] [data-part="bar"]{
height:100%;border-radius:inherit;
background:var(--vibeui-progress-001-accent);
width:calc(var(--vibeui-progress-001-value,0) * 1%);
transition:width .3s cubic-bezier(.32,.72,0,1);
}
/* Неизвестная длительность: отрезок ходит по дорожке, процент не выдумываем. */
[data-vibeui-block="progress-001"][data-indeterminate="true"] [data-part="bar"]{
width:35%;transition:none;
animation:vibeui-progress-001-slide 1.4s ease-in-out infinite;
}
@keyframes vibeui-progress-001-slide{
0%{transform:translateX(-110%)}
100%{transform:translateX(320%)}
}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="progress-001"] *{animation:none!important;transition:none!important}
[data-vibeui-block="progress-001"][data-indeterminate="true"] [data-part="bar"]{width:100%;opacity:.5}
}
`

/**
 * Индикатор выполнения с честным состоянием неизвестной длительности.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Progress001({
  value = 64,
  label = "Сборка проекта",
  hint,
  size = "md",
  accent,
  className,
  style,
  ...props
}: Progress001Props) {
  const indeterminate = value === null || value === undefined
  const clamped = indeterminate ? 0 : Math.min(100, Math.max(0, value))
  const palette = {
    "--vibeui-progress-001-value": clamped,
    ...(accent ? { "--vibeui-progress-001-accent": accent } : null),
    ...style,
  } as CSSProperties

  const note = hint ?? (indeterminate ? "идёт" : `${Math.round(clamped)}%`)

  return (
    <>
      <style href="vibeui-progress-001" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="progress-001"
        data-size={size}
        data-indeterminate={indeterminate || undefined}
        className={className}
        style={palette}
      >
        {label || note ? (
          <div data-part="head">
            <span data-part="label">{label}</span>
            {note ? <span data-part="hint">{note}</span> : null}
          </div>
        ) : null}
        <div
          data-part="track"
          role="progressbar"
          aria-label={label || undefined}
          aria-valuemin={0}
          aria-valuemax={100}
          aria-valuenow={indeterminate ? undefined : Math.round(clamped)}
        >
          <div data-part="bar" />
        </div>
      </div>
    </>
  )
}