Display

Surface Ring Stack

A stack whose ring matches the surface: the same group is shown on a light and a dark strip, making it obvious that the ring is the background, not a white border.

  • iconstack
  • ring
  • surface
  • contrast

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

На светлой панели
the ring equals the surface
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 "iconstack-005" (Surface Ring Stack) 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/iconstack-005.json

Registry item: https://vibeui.ru/r/iconstack-005.json
Installs to: components/vibeui/iconstack-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
A stack whose ring matches the surface: the same group is shown on a light and a dark strip, making it obvious that the ring is the background, not a white border.

A stack with a surface-coloured ring: light and dark strips side by side prove the ring equals the background. Zero dependencies, one file, no client JS.

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

<Iconstack005
  names={["Anna Rebrova", "Ilya Mokhov", "Kim Son"]}
  ring="#ffffff"
  label="the ring equals the surface"
/>

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-iconstack-005-* palette — do not swap it for your theme tokens
- the ring as a strip variable: set on the circle it eventually drifts away from the background
- both strips side by side — without the comparison the trick just looks like a white border
- the 2px ring width: thinner it disappears, thicker it turns into a halo
- aria-hidden on the circles: initials say nothing out loud
- the outer light surface: the light strip caption is dark and vanishes on a dark background

## 6. You may change
- the ring and light strip fill through ring
- the dark strip caption through label
- the group roster through names
- the dark strip colour through the --vibeui-iconstack-005-dark variable

## 7. Rules
- The ring must match the real background: over an image or a gradient the trick fails.
- Both strips render the same group — this is a demonstration, production needs one.
- The ring value is passed through as is: any valid CSS colour works, not only #rrggbb.
- 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/iconstack-005.json
https://vibeui.ru/r/iconstack-005.json

Серверный компонент, хуков нет. Цвет обводки вынесен в переменную --vibeui-iconstack-005-ring и задаётся полосой, а не кружком: тёмная полоса переопределяет её своим фоном, светлая берёт значение из пропа ring и красит им же собственную заливку. Из-за этого обводка физически не может разойтись с подложкой. Обе полосы показаны одновременно, чтобы разница читалась без переключения темы. Перекрытие собрано обратным флексом, оттенок кружка выводится из имени хешем. Внешняя подложка светлая: подпись светлой полосы тёмная.

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 Iconstack005Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  names?: string[]
  ring?: string
  label?: string
}

// Идея компонента: обводка кружка — это не «белая рамка», а цвет подложки под
// стопкой. Как только стопку кладут на тёмную панель, белая обводка
// превращается в лишний контур, поэтому цвет обводки вынесен в переменную и
// задаётся полосой, а не самим кружком. Компонент показывает обе полосы
// сразу — светлую и тёмную, — чтобы разница была видна без переключения темы.
const STYLES = `
:where([data-vibeui-block="iconstack-005"]){
--vibeui-iconstack-005-size:2rem;
--vibeui-iconstack-005-overlap:0.625rem;
--vibeui-iconstack-005-ring:oklch(1 0 0);
--vibeui-iconstack-005-surface:oklch(1 0 0);
--vibeui-iconstack-005-dark:oklch(0.26 0.02 265);
--vibeui-iconstack-005-border:oklch(0.9 0.006 265);
--vibeui-iconstack-005-fg:oklch(0.26 0.014 265);
--vibeui-iconstack-005-muted:oklch(0.55 0.014 265);
--vibeui-iconstack-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="iconstack-005"]{
display:inline-flex;flex-direction:column;gap:0.5rem;
box-sizing:border-box;padding:0.625rem;
background:var(--vibeui-iconstack-005-surface);
border:1px solid var(--vibeui-iconstack-005-border);border-radius:1rem;
font-family:var(--vibeui-iconstack-005-font);color:var(--vibeui-iconstack-005-fg);
}
[data-vibeui-block="iconstack-005"] [data-part="row"]{
display:flex;align-items:center;gap:0.75rem;
padding:0.625rem 0.875rem;border-radius:0.75rem;
}
/* Обводка берётся у полосы, а не у кружка: цвет подложки задаёт её сама. */
[data-vibeui-block="iconstack-005"] [data-part="row"][data-tone="light"]{
background:var(--vibeui-iconstack-005-ring);
border:1px solid var(--vibeui-iconstack-005-border);
}
[data-vibeui-block="iconstack-005"] [data-part="row"][data-tone="dark"]{
background:var(--vibeui-iconstack-005-dark);
--vibeui-iconstack-005-ring:var(--vibeui-iconstack-005-dark);
color:oklch(0.96 0.004 265);
}
[data-vibeui-block="iconstack-005"] [data-part="stack"]{
display:inline-flex;flex-direction:row-reverse;justify-content:flex-end;flex:none;
}
[data-vibeui-block="iconstack-005"] [data-part="stack"] > *{
margin-right:calc(var(--vibeui-iconstack-005-overlap) * -1);
}
[data-vibeui-block="iconstack-005"] [data-part="stack"] > *:first-child{margin-right:0}
[data-vibeui-block="iconstack-005"] [data-part="face"]{
display:inline-flex;align-items:center;justify-content:center;flex:none;box-sizing:border-box;
width:var(--vibeui-iconstack-005-size);height:var(--vibeui-iconstack-005-size);
border-radius:9999px;
border:2px solid var(--vibeui-iconstack-005-ring);
background:oklch(0.88 0.08 var(--vibeui-iconstack-005-hue,265));
color:oklch(0.34 0.13 var(--vibeui-iconstack-005-hue,265));
font-size:0.6875rem;font-weight:700;line-height:1;
}
[data-vibeui-block="iconstack-005"] [data-part="caption"]{
font-size:0.75rem;line-height:1.3;
}
[data-vibeui-block="iconstack-005"] [data-part="row"][data-tone="light"] [data-part="caption"]{
color:var(--vibeui-iconstack-005-muted);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="iconstack-005"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_NAMES = ["Анна Реброва", "Илья Мохов", "Ким Сон", "Пётр Гай"]

function hue(name: string) {
  let hash = 2166136261
  for (const symbol of name) {
    hash ^= symbol.codePointAt(0)!
    hash = Math.imul(hash, 16777619)
  }
  return ((hash >>> 0) % 12) * 30
}

function initials(name: string) {
  return name
    .split(" ")
    .slice(0, 2)
    .map((part) => part.charAt(0).toUpperCase())
    .join("")
}

/**
 * Стопка с обводкой под цвет подложки: светлая и тёмная полосы рядом.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Iconstack005({
  names = DEFAULT_NAMES,
  ring = "#ffffff",
  label = "обводка равна цвету подложки",
  className,
  style,
  ...props
}: Iconstack005Props) {
  const faces = [...names].reverse().map((name) => (
    <span
      key={name}
      data-part="face"
      style={{ "--vibeui-iconstack-005-hue": hue(name) } as CSSProperties}
    >
      {initials(name)}
    </span>
  ))

  return (
    <>
      <style href="vibeui-iconstack-005" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="iconstack-005"
        className={className}
        style={style as CSSProperties}
      >
        <div
          data-part="row"
          data-tone="light"
          style={{ "--vibeui-iconstack-005-ring": ring } as CSSProperties}
        >
          <span data-part="stack" aria-hidden="true">
            {faces}
          </span>
          <span data-part="caption">На светлой панели</span>
        </div>
        <div data-part="row" data-tone="dark">
          <span data-part="stack" aria-hidden="true">
            {faces}
          </span>
          <span data-part="caption">{label}</span>
        </div>
      </div>
    </>
  )
}