Alert

Progress Alert

An alert for work in progress: the step, the percentage and a bar built into the block's bottom edge. Unknown duration is shown honestly as a travelling segment.

  • alert
  • progress
  • loading
  • operation

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/alert-008?lang=en

Publishing the projectBuilding pages — 12 of 1864%
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 "alert-008" (Progress Alert) 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/alert-008.json

Registry item: https://vibeui.ru/r/alert-008.json
Installs to: components/vibeui/alert-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
An alert for work in progress: the step, the percentage and a bar built into the block's bottom edge. Unknown duration is shown honestly as a travelling segment.

An in-progress alert: a CSS spinner, a heading, the current step, a percentage and a cancel button. The progress bar is set into the bottom edge of the block. With value={null} the duration counts as unknown: a segment travels and no percentage is shown. Zero dependencies, one file, its own palette.

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

<Alert008
  title="Publishing the project"
  step="Building pages — 12 of 18"
  value={64}
  onCancel={abort}
/>

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-alert-008-* palette — do not swap it for your theme tokens (bg-primary, bg-muted and the like)
- the bar in the block's edge rather than as a row: in a progress message every extra line pushes the point away
- the honest unknown-duration state: an invented percentage is worse than a travelling segment
- the progressbar semantics with aria-valuenow only when the value is known, and aria-busy on the root
- the CSS spinner: an icon library for one ring is unnecessary
- the ellipsis on the step line — a long file name must not change the block height
- the prefers-reduced-motion rule that stops both the spinner and the segment
- the <style> block inside the component — it holds the palette, the bar and the animations

## 6. You may change
- title and step — what is running and what is happening now
- value — 0–100, or null when the duration is unknown
- onCancel and cancelLabel — aborting the operation
- the accent through the accent prop — it colours the bar, the spinner and the percentage
- width and outer spacing through className

## 7. Rules
- The component runs and tracks nothing: the value and the step come from outside.
- Do not leave it on screen after completion — replace it with the result, or the spinner turns into nothing.
- For anything longer than a minute put an estimate in step: a bare percentage does not reassure.
- 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/alert-008.json
https://vibeui.ru/r/alert-008.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-alert-008-*. Полоса прогресса — абсолютно спозиционированная кромка блока, а не строка в потоке: сообщение остаётся плотным. Ширина заполнения приходит переменной --vibeui-alert-008-value. value={null} включает состояние неизвестной длительности: отрезок ходит по кромке, aria-valuenow не выставляется. Спиннер нарисован рамкой с одним окрашенным краем.

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 Alert008Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "title" | "children"
> & {
  title?: string
  /** Что происходит прямо сейчас: шаг, файл, этап. */
  step?: string
  /** 0–100. `null` — процесс идёт, но длительность неизвестна. */
  value?: number | null
  cancelLabel?: string
  onCancel?: () => void
  accent?: string
}

// Идея компонента: алерт о выполняющейся операции. Полоса встроена в нижнюю
// кромку блока, а не стоит отдельной строкой: сообщение остаётся плотным, а
// прогресс виден краем глаза. Неизвестная длительность честно показывается
// бегущим отрезком — выдуманный процент здесь хуже, чем его отсутствие.
const STYLES = `
:where([data-vibeui-block="alert-008"]){
--vibeui-alert-008-fg:oklch(0.24 0.016 265);
--vibeui-alert-008-muted:oklch(0.5 0.014 265);
--vibeui-alert-008-bg:oklch(1 0 0);
--vibeui-alert-008-border:oklch(0.9 0.006 265);
--vibeui-alert-008-track:oklch(0.93 0.006 265);
--vibeui-alert-008-accent:oklch(0.55 0.2 262);
--vibeui-alert-008-radius:0.75rem;
--vibeui-alert-008-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="alert-008"]{
position:relative;display:flex;align-items:flex-start;gap:0.75rem;
width:100%;box-sizing:border-box;overflow:hidden;
padding:0.875rem 1rem 1.0625rem;
border:1px solid var(--vibeui-alert-008-border);
border-radius:var(--vibeui-alert-008-radius);
background:var(--vibeui-alert-008-bg);color:var(--vibeui-alert-008-fg);
font-family:var(--vibeui-alert-008-font);
}
/* Спиннер на CSS: иконочная библиотека ради одного кольца не нужна. */
[data-vibeui-block="alert-008"] [data-part="spinner"]{
flex:none;width:1.125rem;height:1.125rem;margin-top:0.125rem;
border-radius:9999px;
border:2px solid color-mix(in oklab,var(--vibeui-alert-008-accent) 25%,transparent);
border-top-color:var(--vibeui-alert-008-accent);
animation:vibeui-alert-008-spin .9s linear infinite;
}
@keyframes vibeui-alert-008-spin{to{transform:rotate(360deg)}}
[data-vibeui-block="alert-008"] [data-part="text"]{display:flex;flex-direction:column;gap:0.1875rem;flex:1 1 auto;min-width:0}
[data-vibeui-block="alert-008"] [data-part="title"]{font-size:0.875rem;font-weight:600;line-height:1.4}
[data-vibeui-block="alert-008"] [data-part="step"]{
font-size:0.8125rem;line-height:1.5;color:var(--vibeui-alert-008-muted);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
[data-vibeui-block="alert-008"] [data-part="value"]{
flex:none;align-self:center;font-size:0.8125rem;font-weight:600;
font-variant-numeric:tabular-nums;color:var(--vibeui-alert-008-accent);
}
[data-vibeui-block="alert-008"] [data-part="cancel"]{
appearance:none;border:0;background:none;cursor:pointer;flex:none;align-self:center;
color:var(--vibeui-alert-008-muted);font:inherit;font-size:0.8125rem;
}
[data-vibeui-block="alert-008"] [data-part="cancel"]:hover{color:var(--vibeui-alert-008-fg)}
[data-vibeui-block="alert-008"] [data-part="cancel"]:focus-visible{outline:2px solid var(--vibeui-alert-008-accent);outline-offset:2px;border-radius:0.25rem}
/* Полоса — нижняя кромка блока, а не отдельная строка. */
[data-vibeui-block="alert-008"] [data-part="track"]{
position:absolute;left:0;right:0;bottom:0;height:0.1875rem;
background:var(--vibeui-alert-008-track);
}
[data-vibeui-block="alert-008"] [data-part="bar"]{
height:100%;background:var(--vibeui-alert-008-accent);
width:calc(var(--vibeui-alert-008-value,0) * 1%);
transition:width .3s cubic-bezier(.32,.72,0,1);
}
[data-vibeui-block="alert-008"][data-indeterminate="true"] [data-part="bar"]{
width:30%;transition:none;
animation:vibeui-alert-008-slide 1.4s ease-in-out infinite;
}
@keyframes vibeui-alert-008-slide{0%{transform:translateX(-110%)}100%{transform:translateX(360%)}}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="alert-008"] *{animation:none!important;transition:none!important}
[data-vibeui-block="alert-008"][data-indeterminate="true"] [data-part="bar"]{width:100%;opacity:.4}
}
`

/**
 * Алерт выполняющейся операции: шаг, процент и полоса в кромке блока.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Alert008({
  title = "Публикуем проект",
  step = "Собираем страницы — 12 из 18",
  value = 64,
  cancelLabel = "Отменить",
  onCancel,
  accent,
  className,
  style,
  ...props
}: Alert008Props) {
  const indeterminate = value === null || value === undefined
  const clamped = indeterminate ? 0 : Math.min(100, Math.max(0, value))
  const palette = {
    "--vibeui-alert-008-value": clamped,
    ...(accent ? { "--vibeui-alert-008-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-alert-008" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="alert-008"
        data-indeterminate={indeterminate || undefined}
        role="status"
        aria-busy="true"
        className={className}
        style={palette}
      >
        <span data-part="spinner" aria-hidden="true" />
        <span data-part="text">
          <span data-part="title">{title}</span>
          {step ? <span data-part="step">{step}</span> : null}
        </span>
        {indeterminate ? null : (
          <span data-part="value">{Math.round(clamped)}%</span>
        )}
        {onCancel && cancelLabel ? (
          <button data-part="cancel" type="button" onClick={onCancel}>
            {cancelLabel}
          </button>
        ) : null}
        <span
          data-part="track"
          role="progressbar"
          aria-label={title}
          aria-valuemin={0}
          aria-valuemax={100}
          aria-valuenow={indeterminate ? undefined : Math.round(clamped)}
        >
          <span data-part="bar" />
        </span>
      </div>
    </>
  )
}