Display

Blocking Busy Overlay

An overlay that actually blocks: the content is marked inert, so no clickable buttons or tab-reachable fields survive underneath the veil.

  • spinner
  • overlay
  • busy
  • inert

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/spinner-005?lang=en

Профиль команды

НазваниеДомен
Saving changes
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 "spinner-005" (Blocking Busy Overlay) 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/spinner-005.json

Registry item: https://vibeui.ru/r/spinner-005.json
Installs to: components/vibeui/spinner-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
An overlay that actually blocks: the content is marked inert, so no clickable buttons or tab-reachable fields survive underneath the veil.

A busy overlay: a veil with a caption while the content beneath is marked inert and aria-busy. Zero dependencies, one file, no client JS.

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

<Spinner005 busy label="Saving changes" />

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-spinner-005-* palette — do not swap it for your theme tokens
- inert on the content: without it clickable buttons and tab-reachable fields survive under the veil
- aria-busy on the region — otherwise a screen reader says nothing about the block being busy
- the translucent veil rather than an opaque one: the user must see what is being saved
- the caption beside the ring: a ring without words never says what is being awaited
- the component's own light surface: both the content and the overlay caption are dark

## 6. You may change
- the overlay text through label
- turning the block on through busy
- the block content — any node in the slot
- the veil density through the color-mix share

## 7. Rules
- inert is supported by current browsers; older ones keep focus reachable under the veil.
- backdrop-filter is expensive on weak devices: drop it inside a long list.
- Focus inside blocked content does not move on its own — move it away before turning busy on.
- 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/spinner-005.json
https://vibeui.ru/r/spinner-005.json

Серверный компонент, хуков нет: состояние приходит пропом busy. Полупрозрачная плёнка сама по себе ничего не запрещает — под ней остаются рабочие кнопки и поля, до которых доходит таб. Здесь содержимое получает атрибут inert, который убирает его и из фокуса, и из дерева доступности, а на область ставится aria-busy, чтобы скринридер сообщил о работе. Плёнка сделана color-mix от цвета подложки плюс backdrop-filter: она приглушает содержимое, но не прячет его целиком, и пользователь видит, что именно сохраняется. При prefers-reduced-motion кольцо не крутится, а дышит.

Component source

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

import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react"

export type Spinner005Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  busy?: boolean
  label?: string
  children?: ReactNode
}

// Идея компонента: наложение поверх блока, которое действительно блокирует.
// Полупрозрачная плёнка сама по себе ничего не запрещает: под ней остаются
// кликабельные кнопки и достижимые табом поля. Здесь содержимое помечено
// inert — оно уходит и из фокуса, и из дерева доступности, — а на область
// поставлен aria-busy, чтобы скринридер сообщил о работе, а не молчал.
const STYLES = `
:where([data-vibeui-block="spinner-005"]){
--vibeui-spinner-005-surface:oklch(1 0 0);
--vibeui-spinner-005-border:oklch(0.9 0.006 265);
--vibeui-spinner-005-fg:oklch(0.24 0.014 265);
--vibeui-spinner-005-muted:oklch(0.55 0.014 265);
--vibeui-spinner-005-soft:oklch(0.96 0.004 265);
--vibeui-spinner-005-track:oklch(0.9 0.006 265);
--vibeui-spinner-005-accent:oklch(0.55 0.17 262);
--vibeui-spinner-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: и содержимое, и подпись наложения тёмные. */
[data-vibeui-block="spinner-005"]{
position:relative;overflow:hidden;
display:block;width:100%;max-width:22rem;box-sizing:border-box;
background:var(--vibeui-spinner-005-surface);
border:1px solid var(--vibeui-spinner-005-border);border-radius:1rem;
font-family:var(--vibeui-spinner-005-font);color:var(--vibeui-spinner-005-fg);
}
[data-vibeui-block="spinner-005"] [data-part="content"]{
display:flex;flex-direction:column;gap:0.625rem;padding:1rem;
}
[data-vibeui-block="spinner-005"] [data-part="title"]{margin:0;font-size:0.9375rem;font-weight:700}
[data-vibeui-block="spinner-005"] [data-part="field"]{
display:flex;flex-direction:column;gap:0.3125rem;
font-size:0.75rem;color:var(--vibeui-spinner-005-muted);
}
[data-vibeui-block="spinner-005"] [data-part="box"]{
height:2.25rem;border-radius:0.5rem;
border:1px solid var(--vibeui-spinner-005-border);
background:var(--vibeui-spinner-005-soft);
}
/* Плёнка не блокирует сама по себе: работу делает inert на содержимом. */
[data-vibeui-block="spinner-005"] [data-part="veil"]{
position:absolute;inset:0;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.5rem;
background:color-mix(in oklab,var(--vibeui-spinner-005-surface) 78%,transparent);
backdrop-filter:blur(2px);
}
[data-vibeui-block="spinner-005"] [data-part="ring"]{
width:1.5rem;height:1.5rem;box-sizing:border-box;
border:2px solid var(--vibeui-spinner-005-track);
border-top-color:var(--vibeui-spinner-005-accent);
border-radius:9999px;
animation:vibeui-spinner-005-spin .7s linear infinite;
}
@keyframes vibeui-spinner-005-spin{to{transform:rotate(360deg)}}
@keyframes vibeui-spinner-005-pulse{0%,100%{opacity:.35}50%{opacity:1}}
[data-vibeui-block="spinner-005"] [data-part="veil-label"]{
font-size:0.8125rem;font-weight:650;
}
/* Без движения кольцо не крутится, а дышит: состояние сохраняется. */
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="spinner-005"] [data-part="ring"]{
animation:vibeui-spinner-005-pulse 1.6s ease-in-out infinite;
border-color:var(--vibeui-spinner-005-accent);
}
}
`

/**
 * Наложение поверх блока с настоящей блокировкой содержимого через inert.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Spinner005({
  busy = true,
  label = "Сохраняем изменения",
  children,
  className,
  style,
  ...props
}: Spinner005Props) {
  return (
    <>
      <style href="vibeui-spinner-005" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="spinner-005"
        aria-busy={busy}
        className={className}
        style={style as CSSProperties}
      >
        <div data-part="content" inert={busy}>
          {children ?? (
            <>
              <p data-part="title">Профиль команды</p>
              <span data-part="field">
                Название
                <span data-part="box" />
              </span>
              <span data-part="field">
                Домен
                <span data-part="box" />
              </span>
            </>
          )}
        </div>
        {busy ? (
          <div data-part="veil">
            <span data-part="ring" aria-hidden="true" />
            <span data-part="veil-label" role="status">
              {label}
            </span>
          </div>
        ) : null}
      </div>
    </>
  )
}