Badge

Overflow Group

A badge group whose tail collapses into "+4" and expands on click — on native details, without a single line of client code.

  • badge
  • group
  • overflow
  • details

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/badge-024?lang=en

TypeScriptReactTailwind
+4СвернутьPostgreSQLRedisDockerPlaywright
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 "badge-024" (Overflow Group) 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/badge-024.json

Registry item: https://vibeui.ru/r/badge-024.json
Installs to: components/vibeui/badge-024.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 badge group whose tail collapses into "+4" and expands on click — on native details, without a single line of client code.

A badge group with a collapsible "+N" tail that expands through native details with no client JS. Zero dependencies, one file.

## 3. How to use it
import { Badge024 } from "@/components/vibeui/badge-024"

<Badge024
  items={["TypeScript", "React", "Tailwind", "PostgreSQL"]}
  visible={3}
  label="Stack"
/>

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-badge-024-* palette — do not swap it for your theme tokens
- native details instead of useState: it gives keyboard, focus and state announcement for free
- details as a flex container — otherwise the expanded tail drops below the button as a block
- the removed triangle marker: two pointers to the same button are confusing
- the visible focus ring on summary: it is the only interactive node in the group
- the badges' own light surfaces: without them the dark text disappears on a dark background

## 6. You may change
- the list through items
- the number of visible badges through visible
- the group name through label
- the badge shape — radius and height in the block rules

## 7. Rules
- Expansion is not animated: the details height is unknown in advance, and a jumping animation is worse than an instant reveal.
- The text itself is used as the key — duplicate labels in items produce a React warning.
- On a narrow width the group wraps onto a second line: if the row must stay single, cap visible.
- 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/badge-024.json
https://vibeui.ru/r/badge-024.json

Один файл, ноль зависимостей, палитра в локальных переменных --vibeui-badge-024-*. Серверный: раскрытие держит нативная пара details/summary, поэтому ни useState, ни обработчика, ни директивы use client здесь нет. Сам details объявлен flex-контейнером: summary и раскрытый хвост становятся его элементами и встают в тот же ряд, а не блоком под ним. Маркер треугольника убран (list-style:none плюс правило для WebKit), подпись «+4» подменяется на «Свернуть» селектором [open]. Клавиатура, фокус и объявление состояния «раскрыто/свёрнуто» достаются от браузера бесплатно.

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 Badge024Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  items?: string[]
  visible?: number
  label?: string
}

// Идея компонента: группа плашек, хвост которой не просто сворачивается в
// «+4», а раскрывается по нажатию. Раскрытие держит нативный details: summary
// уже кнопка, уже фокусируется, уже объявляет состояние — клиентский
// обработчик и useState здесь не нужны, компонент остаётся серверным.
const STYLES = `
:where([data-vibeui-block="badge-024"]){
--vibeui-badge-024-bg:oklch(0.97 0.004 265);
--vibeui-badge-024-fg:oklch(0.32 0.014 265);
--vibeui-badge-024-border:oklch(0.89 0.006 265);
--vibeui-badge-024-accent:oklch(0.54 0.16 265);
--vibeui-badge-024-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="badge-024"]{
display:flex;flex-wrap:wrap;align-items:center;gap:0.375rem;
max-width:100%;
font-family:var(--vibeui-badge-024-font);
}
[data-vibeui-block="badge-024"] [data-part="item"]{
display:inline-flex;align-items:center;box-sizing:border-box;
height:1.625rem;padding:0 0.625rem;
border:1px solid var(--vibeui-badge-024-border);border-radius:9999px;
background:var(--vibeui-badge-024-bg);color:var(--vibeui-badge-024-fg);
font-size:0.75rem;font-weight:500;line-height:1;white-space:nowrap;
}
/* details сам становится flex-контейнером: summary и хвост — его элементы,
   поэтому раскрытые плашки встают в тот же ряд, а не блоком под ним. */
[data-vibeui-block="badge-024"] [data-part="more"]{
display:flex;flex-wrap:wrap;align-items:center;gap:0.375rem;min-width:0;
}
[data-vibeui-block="badge-024"] summary{
display:inline-flex;align-items:center;gap:0.25rem;
box-sizing:border-box;height:1.625rem;padding:0 0.625rem;
border:1px dashed var(--vibeui-badge-024-border);border-radius:9999px;
background:transparent;color:var(--vibeui-badge-024-accent);
font-size:0.75rem;font-weight:700;line-height:1;
font-variant-numeric:tabular-nums;cursor:pointer;list-style:none;
transition:border-color .16s ease,background-color .16s ease;
}
[data-vibeui-block="badge-024"] summary::-webkit-details-marker{display:none}
[data-vibeui-block="badge-024"] summary:hover{
border-color:color-mix(in oklab,var(--vibeui-badge-024-accent) 45%,oklch(1 0 0));
background:color-mix(in oklab,var(--vibeui-badge-024-accent) 8%,oklch(1 0 0));
}
[data-vibeui-block="badge-024"] summary:focus-visible{
outline:2px solid var(--vibeui-badge-024-accent);outline-offset:2px;
}
[data-vibeui-block="badge-024"] [data-part="less"]{display:none}
[data-vibeui-block="badge-024"] [data-part="more"][open] [data-part="less"]{display:inline}
[data-vibeui-block="badge-024"] [data-part="more"][open] [data-part="rest"]{display:none}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="badge-024"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ITEMS = [
  "TypeScript",
  "React",
  "Tailwind",
  "PostgreSQL",
  "Redis",
  "Docker",
  "Playwright",
]

/**
 * Группа плашек с переполнением «+N», которое раскрывается без JS.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Badge024({
  items = DEFAULT_ITEMS,
  visible = 3,
  label = "Технологии",
  className,
  style,
  ...props
}: Badge024Props) {
  const shown = items.slice(0, Math.max(1, Math.round(visible)))
  const rest = items.slice(shown.length)

  return (
    <>
      <style href="vibeui-badge-024" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="badge-024"
        className={className}
        style={style as CSSProperties}
        role="group"
        aria-label={label}
      >
        {shown.map((item) => (
          <span key={item} data-part="item">
            {item}
          </span>
        ))}
        {rest.length > 0 ? (
          <details data-part="more">
            <summary aria-label={`Ещё ${rest.length} из списка «${label}»`}>
              <span data-part="rest">+{rest.length}</span>
              <span data-part="less">Свернуть</span>
            </summary>
            {rest.map((item) => (
              <span key={item} data-part="item">
                {item}
              </span>
            ))}
          </details>
        ) : null}
      </div>
    </>
  )
}