Display

Pulse Skeleton Card

A card skeleton built on an opacity pulse rather than a sweeping shine: the pulse is handed to the compositor and costs almost nothing across twenty placeholders.

  • spinner
  • skeleton
  • pulse
  • placeholder

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-006?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 "spinner-006" (Pulse Skeleton Card) 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-006.json

Registry item: https://vibeui.ru/r/spinner-006.json
Installs to: components/vibeui/spinner-006.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 card skeleton built on an opacity pulse rather than a sweeping shine: the pulse is handed to the compositor and costs almost nothing across twenty placeholders.

A card skeleton: an opacity pulse instead of a sweeping shine, matching the metrics of the real card. Zero dependencies, one file, no client JS.

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

<Spinner006 lines={3} media label="Loading the card" />

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-006-* palette — do not swap it for your theme tokens
- the opacity pulse instead of a shine: a gradient sweep heats the CPU across a long list
- the metrics of the real card — a placeholder of a different height makes the content jump
- a single role=status with a name: twenty slabs must not read as twenty messages
- the shortened last line: it turns a set of slabs into a paragraph
- the component's own light surface: grey slabs get lost on a dark catalog background

## 6. You may change
- the number of rows through lines
- showing the image through media
- the slab colour through the --vibeui-spinner-006-base variable
- the message text through label

## 7. Rules
- The skeleton must match the real card: any mismatch shows exactly at the swap moment.
- More than six rows look like a paragraph of text rather than a card.
- Showing a skeleton for under 300ms is not worth it — the flash annoys more than the wait.
- 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-006.json
https://vibeui.ru/r/spinner-006.json

Серверный компонент, хуков нет. Бегущий блик — это анимация большого градиента, и на списке заглушек он заметно греет процессор; пульсация прозрачности анимирует только opacity, что браузер отдаёт композитору. Заглушка повторяет метрику будущей карточки: та же высота строк, те же промежутки и та же пропорция картинки, поэтому при подстановке данных ничего не прыгает. Строки получают небольшие задержки, чтобы карточка не мигала одним куском. Для скринридера это одно сообщение role=status с именем, а не двадцать серых прямоугольников. Подложка светлая: серые плашки на тёмном фоне теряются.

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 Spinner006Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  lines?: number
  media?: boolean
  label?: string
}

// Идея компонента: заглушка карточки на пульсации, а не на бегущем блике.
// Блик — это анимация большого градиента, и на списке из двадцати заглушек
// он заметно греет процессор; пульсация прозрачности отдаётся композитору и
// стоит почти ничего. Заглушка повторяет метрику будущей карточки, поэтому
// при подстановке данных ничего не прыгает. Для скринридера это одно
// сообщение role="status", а не двадцать серых прямоугольников.
const STYLES = `
:where([data-vibeui-block="spinner-006"]){
--vibeui-spinner-006-surface:oklch(1 0 0);
--vibeui-spinner-006-border:oklch(0.91 0.006 265);
--vibeui-spinner-006-base:oklch(0.92 0.005 265);
--vibeui-spinner-006-radius:0.5rem;
--vibeui-spinner-006-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: серые плашки на тёмном фоне теряются. */
[data-vibeui-block="spinner-006"]{
display:flex;flex-direction:column;gap:0.75rem;
width:100%;max-width:20rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-spinner-006-surface);
border:1px solid var(--vibeui-spinner-006-border);border-radius:0.875rem;
font-family:var(--vibeui-spinner-006-font);
}
[data-vibeui-block="spinner-006"] [data-part="media"]{
aspect-ratio:16 / 9;border-radius:0.625rem;
background:var(--vibeui-spinner-006-base);
animation:vibeui-spinner-006-pulse 1.6s ease-in-out infinite;
}
[data-vibeui-block="spinner-006"] [data-part="head"]{
display:flex;align-items:center;gap:0.625rem;
}
[data-vibeui-block="spinner-006"] [data-part="dot"]{
width:2rem;height:2rem;flex:none;border-radius:9999px;
background:var(--vibeui-spinner-006-base);
animation:vibeui-spinner-006-pulse 1.6s ease-in-out infinite;
}
[data-vibeui-block="spinner-006"] [data-part="lines"]{
display:flex;flex-direction:column;gap:0.5rem;flex:1 1 auto;
}
/* Метрика будущего текста: высота строки и промежуток те же, что у абзаца. */
[data-vibeui-block="spinner-006"] [data-part="bar"]{
height:0.75rem;border-radius:var(--vibeui-spinner-006-radius);
background:var(--vibeui-spinner-006-base);
animation:vibeui-spinner-006-pulse 1.6s ease-in-out infinite;
}
[data-vibeui-block="spinner-006"] [data-part="bar"]:nth-child(2){animation-delay:.12s}
[data-vibeui-block="spinner-006"] [data-part="bar"]:nth-child(3){animation-delay:.24s}
[data-vibeui-block="spinner-006"] [data-part="bar"]:last-child{width:64%}
@keyframes vibeui-spinner-006-pulse{0%,100%{opacity:1}50%{opacity:.45}}
/* Без движения пульс выключен, а плашки остаются приглушёнными. */
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="spinner-006"] *{animation:none!important;transition:none!important}
[data-vibeui-block="spinner-006"] [data-part="bar"],
[data-vibeui-block="spinner-006"] [data-part="dot"],
[data-vibeui-block="spinner-006"] [data-part="media"]{opacity:.7}
}
`

/**
 * Скелетон карточки на пульсации прозрачности вместо бегущего блика.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Spinner006({
  lines = 3,
  media = true,
  label = "Загружаем карточку компонента",
  className,
  style,
  ...props
}: Spinner006Props) {
  const count = Math.min(6, Math.max(1, Math.round(lines)))

  return (
    <>
      <style href="vibeui-spinner-006" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="spinner-006"
        role="status"
        aria-live="polite"
        aria-label={label}
        className={className}
        style={style as CSSProperties}
      >
        {media ? <span data-part="media" /> : null}
        <div data-part="head">
          <span data-part="dot" />
          <div data-part="lines">
            {Array.from({ length: count }, (_, index) => (
              <span key={index} data-part="bar" />
            ))}
          </div>
        </div>
      </div>
    </>
  )
}