Display

Expandable Stack Count

A stack whose "+N" counter is an entrance rather than a dead end: the disclosure runs on details, so the names are available without a line of client code.

  • iconstack
  • details
  • overflow
  • avatars

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-003?lang=en

7 members
  • Анна Реброва
  • Илья Мохов
  • Ким Сон
  • Пётр Гай
  • Мария Лоза
  • Олег Дин
  • Ната Кир
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-003" (Expandable Stack Count) 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-003.json

Registry item: https://vibeui.ru/r/iconstack-003.json
Installs to: components/vibeui/iconstack-003.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 "+N" counter is an entrance rather than a dead end: the disclosure runs on details, so the names are available without a line of client code.

A stack with a "+N" counter that expands into the full member list using native details. Zero dependencies, one file, no client JS.

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

<Iconstack003
  names={["Anna Rebrova", "Ilya Mokhov", "Kim Son", "Pyotr Gai", "Maria Loza"]}
  max={4}
  label="members"
/>

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-003-* palette — do not swap it for your theme tokens
- details instead of JS state: the disclosure works before hydration and without client code
- the visible focus ring on summary — otherwise the tap target is invisible from the keyboard
- the counter to the left of the stack: on the right it reads as one more member
- the removed details marker, which otherwise draws a triangle over the layout
- the component's own light surface: the name list is dark and disappears on a dark catalog background

## 6. You may change
- the number of visible circles through max
- the group caption and the roster through names
- the circle size through the --vibeui-iconstack-003-size variable
- the overlap through the --vibeui-iconstack-003-overlap variable

## 7. Rules
- details cannot animate its height without allow-discrete: the abrupt opening here is deliberate.
- Interactive elements do not belong inside summary — a nested button steals the click.
- The name list is not virtualised: a hundred members expand into one long column.
- 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-003.json
https://vibeui.ru/r/iconstack-003.json

Серверный компонент, хуков нет: раскрытие держит нативный details, состояние живёт в DOM. Стопка и счётчик лежат внутри summary, у которого убран стандартный маркер (list-style:none и ::-webkit-details-marker) и добавлена видимая обводка фокуса — summary фокусируется по табу, и без обводки цель нажатия невидима. Счётчик поставлен слева от стопки: справа он читается как ещё один участник. Перекрытие собрано обратным флексом, оттенок кружка выводится из имени FNV-хешем. Подложка светлая, потому что раскрытый список имён набран тёмным.

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 Iconstack003Props = Omit<
  ComponentPropsWithoutRef<"details">,
  "children"
> & {
  names?: string[]
  max?: number
  label?: string
}

// Идея компонента: «+N» не тупик, а вход в полный список. Стопка лежит
// внутри summary, поэтому раскрытие работает на details без строчки
// клиентского кода и без состояния. Кому нужны остальные — раскрывает и
// читает имена; кому нет — видит компактную строку. Счётчик стоит слева от
// стопки, потому что справа он читается как ещё один участник.
const STYLES = `
:where([data-vibeui-block="iconstack-003"]){
--vibeui-iconstack-003-size:1.875rem;
--vibeui-iconstack-003-overlap:0.5625rem;
--vibeui-iconstack-003-surface:oklch(1 0 0);
--vibeui-iconstack-003-border:oklch(0.9 0.006 265);
--vibeui-iconstack-003-fg:oklch(0.26 0.014 265);
--vibeui-iconstack-003-muted:oklch(0.55 0.014 265);
--vibeui-iconstack-003-ring:oklch(0.55 0.17 262 / 60%);
--vibeui-iconstack-003-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: имена в раскрытом списке тёмные. */
[data-vibeui-block="iconstack-003"]{
display:inline-block;box-sizing:border-box;
min-width:14rem;padding:0.5rem 0.75rem;
background:var(--vibeui-iconstack-003-surface);
border:1px solid var(--vibeui-iconstack-003-border);border-radius:0.875rem;
font-family:var(--vibeui-iconstack-003-font);color:var(--vibeui-iconstack-003-fg);
}
[data-vibeui-block="iconstack-003"] summary{
display:flex;align-items:center;gap:0.625rem;cursor:pointer;list-style:none;
padding:0.25rem;margin:-0.25rem;border-radius:0.625rem;
}
[data-vibeui-block="iconstack-003"] summary::-webkit-details-marker{display:none}
[data-vibeui-block="iconstack-003"] summary:focus-visible{outline:2px solid var(--vibeui-iconstack-003-ring);outline-offset:2px}
[data-vibeui-block="iconstack-003"] [data-part="stack"]{
display:inline-flex;flex-direction:row-reverse;justify-content:flex-end;flex:none;
}
[data-vibeui-block="iconstack-003"] [data-part="stack"] > *{
margin-right:calc(var(--vibeui-iconstack-003-overlap) * -1);
}
[data-vibeui-block="iconstack-003"] [data-part="stack"] > *:first-child{margin-right:0}
[data-vibeui-block="iconstack-003"] [data-part="face"],
[data-vibeui-block="iconstack-003"] [data-part="more"]{
display:inline-flex;align-items:center;justify-content:center;flex:none;box-sizing:border-box;
width:var(--vibeui-iconstack-003-size);height:var(--vibeui-iconstack-003-size);
border-radius:9999px;border:2px solid var(--vibeui-iconstack-003-surface);
font-size:0.625rem;font-weight:700;line-height:1;
}
[data-vibeui-block="iconstack-003"] [data-part="face"]{
background:oklch(0.9 0.06 var(--vibeui-iconstack-003-hue,265));
color:oklch(0.36 0.12 var(--vibeui-iconstack-003-hue,265));
}
/* Счётчик слева от стопки: справа он читается как ещё один участник. */
[data-vibeui-block="iconstack-003"] [data-part="more"]{
background:oklch(0.28 0.014 265);color:oklch(0.99 0 0);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="iconstack-003"] [data-part="summary-text"]{
font-size:0.8125rem;color:var(--vibeui-iconstack-003-muted);
}
[data-vibeui-block="iconstack-003"] [data-part="chevron"]{
margin-left:auto;flex:none;font-size:0.6875rem;color:var(--vibeui-iconstack-003-muted);
transition:transform .16s ease;
}
[data-vibeui-block="iconstack-003"][open] [data-part="chevron"]{transform:rotate(180deg)}
[data-vibeui-block="iconstack-003"] ul{
list-style:none;margin:0.625rem 0 0;padding:0.625rem 0 0;
border-top:1px solid var(--vibeui-iconstack-003-border);
display:flex;flex-direction:column;gap:0.375rem;
font-size:0.8125rem;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="iconstack-003"] *{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("")
}

/**
 * Стопка со счётчиком «+N», раскрывающая полный список на details.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Iconstack003({
  names = DEFAULT_NAMES,
  max = 4,
  label = "участников",
  className,
  style,
  ...props
}: Iconstack003Props) {
  const shown = names.slice(0, max)
  const rest = names.length - shown.length

  return (
    <>
      <style href="vibeui-iconstack-003" precedence="medium">
        {STYLES}
      </style>
      <details
        {...props}
        data-vibeui-block="iconstack-003"
        className={className}
        style={style as CSSProperties}
      >
        <summary>
          <span data-part="stack" aria-hidden="true">
            {[...shown].reverse().map((name) => (
              <span
                key={name}
                data-part="face"
                style={
                  { "--vibeui-iconstack-003-hue": hue(name) } as CSSProperties
                }
              >
                {initials(name)}
              </span>
            ))}
            {rest > 0 ? <span data-part="more">+{rest}</span> : null}
          </span>
          <span data-part="summary-text">
            {names.length} {label}
          </span>
          <span data-part="chevron" aria-hidden="true">
            ▾
          </span>
        </summary>
        <ul>
          {names.map((name) => (
            <li key={name}>{name}</li>
          ))}
        </ul>
      </details>
    </>
  )
}