Display

Sticky Group Headers

A scroll area whose section headers stick to its own top rather than the page's: the area itself is the scrolling ancestor that sticky measures against.

  • scrollarea
  • sticky
  • list
  • display

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/scrollarea-004?lang=en

Contacts
Сегодня3
Анна Реброва
Игорь Ким
Мария Пак
Вчера4
Дмитрий Гнатюк
Лиза Орлова
Пётр Савин
Тимур Хан
На прошлой неделе4
Ольга Дюжева
Сергей Мун
Юлия Ким
Артём Волошин
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 "scrollarea-004" (Sticky Group Headers) 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/scrollarea-004.json

Registry item: https://vibeui.ru/r/scrollarea-004.json
Installs to: components/vibeui/scrollarea-004.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 scroll area whose section headers stick to its own top rather than the page's: the area itself is the scrolling ancestor that sticky measures against.

An area with sticky section headers: sticky measures against the area, and the markup is one flat dl with no extra wrappers. Zero dependencies, one file, server rendered.

## 3. How to use it
import { Scrollarea004 } from "@/components/vibeui/scrollarea-004"

<Scrollarea004
  title="Contacts"
  groups={[{ title: "Today", items: ["Anna Rebrova"] }]}
  height="14rem"
/>

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-scrollarea-004-* palette — do not swap it for your theme tokens
- the flat dl markup with no wrappers around groups: any ancestor with overflow cancels sticking
- the opaque header background — a transparent one lets the departing rows show through
- the fixed height and overflow-y on the area itself: without them sticky measures against the page
- tabindex="0" and role="region" with aria-label on the scroller
- its own light surface: without it the dark text disappears on a dark background

## 6. You may change
- the area heading through title
- the sections and their contents through groups
- the area height through height
- the colour and height of the sticky bar in CSS

## 7. Rules
- A header stays put until its section ends and then yields to the next: that is sticky behaviour, not animation.
- The header needs a z-index, otherwise the next section's rows paint over it while scrolling.
- The dl groups are wrapped in fragments: replacing them with divs breaks the sticking.
- 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/scrollarea-004.json
https://vibeui.ru/r/scrollarea-004.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-scrollarea-004-*. Серверный: хуков нет. Список собран как один dl: dt — заголовок раздела с position: sticky и top: 0, dd — строки; группы обёрнуты фрагментами, поэтому между dl и dt нет лишнего узла с overflow, который отменил бы прилипание. Sticky считает верх от ближайшего предка с прокруткой, поэтому область обязана быть этим предком — отсюда фиксированная высота и overflow-y на ней. Прокручиваемый узел получает tabindex="0" и role="region" с aria-label. Непрозрачный фон заголовка обязателен: сквозь прозрачный просвечивают уезжающие строки.

Component source

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

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

export type Scrollarea004Group = {
  title: string
  items: string[]
}

export type Scrollarea004Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "title"
> & {
  title?: string
  groups?: Scrollarea004Group[]
  height?: string
}

// Идея компонента: заголовок раздела липнет к верху области, а не страницы.
// Sticky считает верх от ближайшего предка с прокруткой, поэтому область
// обязана быть этим предком, а сам заголовок — не иметь предка с overflow.
// Отсюда плоская разметка: секция не оборачивает список в лишний контейнер.
const STYLES = `
:where([data-vibeui-block="scrollarea-004"]){
--vibeui-scrollarea-004-bg:oklch(1 0 0);
--vibeui-scrollarea-004-fg:oklch(0.24 0.014 265);
--vibeui-scrollarea-004-muted:oklch(0.55 0.014 265);
--vibeui-scrollarea-004-border:oklch(0.9 0.006 265);
--vibeui-scrollarea-004-sticky:oklch(0.965 0.004 265);
--vibeui-scrollarea-004-accent:oklch(0.55 0.17 265);
--vibeui-scrollarea-004-height:14rem;
--vibeui-scrollarea-004-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="scrollarea-004"]{
display:flex;flex-direction:column;
width:100%;max-width:20rem;box-sizing:border-box;overflow:hidden;
background:var(--vibeui-scrollarea-004-bg);
border:1px solid var(--vibeui-scrollarea-004-border);border-radius:0.875rem;
font-family:var(--vibeui-scrollarea-004-font);color:var(--vibeui-scrollarea-004-fg);
}
[data-vibeui-block="scrollarea-004"] [data-part="bar"]{
padding:0.625rem 0.875rem;border-bottom:1px solid var(--vibeui-scrollarea-004-border);
font-size:0.8125rem;font-weight:650;
}
[data-vibeui-block="scrollarea-004"] [data-part="area"]{
height:var(--vibeui-scrollarea-004-height);
overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;
}
[data-vibeui-block="scrollarea-004"] [data-part="area"]:focus-visible{
outline:2px solid var(--vibeui-scrollarea-004-accent);outline-offset:-2px;
}
[data-vibeui-block="scrollarea-004"] dl{margin:0}
/* Липкий заголовок раздела: верх считается от области, а не от страницы. */
[data-vibeui-block="scrollarea-004"] dt{
position:sticky;top:0;z-index:1;
display:flex;align-items:center;justify-content:space-between;gap:0.5rem;
padding:0.375rem 0.875rem;
background:var(--vibeui-scrollarea-004-sticky);
border-bottom:1px solid var(--vibeui-scrollarea-004-border);
font-size:0.6875rem;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;
color:var(--vibeui-scrollarea-004-muted);
}
[data-vibeui-block="scrollarea-004"] dd{
margin:0;display:flex;align-items:center;
min-height:2.125rem;padding:0 0.875rem;font-size:0.8125rem;
}
[data-vibeui-block="scrollarea-004"] dd + dd{border-top:1px solid oklch(0.95 0.004 265)}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="scrollarea-004"] *{animation:none!important;transition:none!important}
}
`

const DEFAULT_GROUPS: Scrollarea004Group[] = [
  { title: "Сегодня", items: ["Анна Реброва", "Игорь Ким", "Мария Пак"] },
  {
    title: "Вчера",
    items: ["Дмитрий Гнатюк", "Лиза Орлова", "Пётр Савин", "Тимур Хан"],
  },
  {
    title: "На прошлой неделе",
    items: ["Ольга Дюжева", "Сергей Мун", "Юлия Ким", "Артём Волошин"],
  },
]

/**
 * Область прокрутки, где заголовок раздела липнет к её верху.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Scrollarea004({
  title = "Контакты",
  groups = DEFAULT_GROUPS,
  height = "14rem",
  className,
  style,
  ...props
}: Scrollarea004Props) {
  const palette = {
    "--vibeui-scrollarea-004-height": height,
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-scrollarea-004" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="scrollarea-004"
        className={className}
        style={palette}
      >
        <div data-part="bar">{title}</div>
        <div data-part="area" tabIndex={0} role="region" aria-label={title}>
          <dl>
            {groups.map((group) => (
              <Fragment key={group.title}>
                <dt>
                  <span>{group.title}</span>
                  <span>{group.items.length}</span>
                </dt>
                {group.items.map((item) => (
                  <dd key={item}>{item}</dd>
                ))}
              </Fragment>
            ))}
          </dl>
        </div>
      </div>
    </>
  )
}