Display

Details Only Tree

A section tree with no client code at all: native details holds the expansion, so it works before hydration and is found by the browser's own in-page search.

  • tree
  • details
  • server
  • 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/tree-003?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 "tree-003" (Details Only Tree) 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/tree-003.json

Registry item: https://vibeui.ru/r/tree-003.json
Installs to: components/vibeui/tree-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 section tree with no client code at all: native details holds the expansion, so it works before hydration and is found by the browser's own in-page search.

A section tree on native details: server rendered, with keyboard support and open-branch memory provided by the browser. Zero dependencies, one file.

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

<Tree003
  label="Documentation"
  nodes={[{ name: "Getting started", open: true, children: [{ name: "Install" }] }]}
/>

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-tree-003-* palette — do not swap it for your theme tokens
- native details as the source of state: a hand-rolled one has to be fixed per browser
- the explicit aria-level: details nesting does not imply depth for screen readers
- the removed summary marker together with ::-webkit-details-marker — otherwise a second triangle remains
- counting leaves across the whole branch: otherwise the badge number lies on nested sections
- its own light surface: without it the dark captions disappear on a dark background

## 6. You may change
- the tree contents through nodes, including the open flag on branches
- the tree caption through label
- the level indent and line colour in CSS
- the badge contents if a leaf count is not what you need

## 7. Rules
- There is nothing to animate the details expansion with: the browser does not know the content height in advance.
- Branches without the open flag arrive collapsed on both server and client — the state does not survive navigation.
- The node key is its name: two identical names on one level collide.
- 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/tree-003.json
https://vibeui.ru/r/tree-003.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-tree-003-*. Серверный: хуков нет, состояние веток хранит сам браузер. Вложенные details дают раскрытие, клавиатуру и память об открытых ветках даром; аria-level проставляется явно при обходе, потому что из вложенности details скринридер глубину не выводит. В бейдже стоит число листьев во всей ветке, а не в первом её уровне: countLeaves спускается до конца. Маркер summary снят и заменён треугольником на бордюрах, поэтому он поворачивается через transform и не зависит от отрисовки маркера в браузере.

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 Tree003Node = {
  name: string
  children?: Tree003Node[]
  open?: boolean
}

export type Tree003Props = Omit<ComponentPropsWithoutRef<"div">, "children"> & {
  nodes?: Tree003Node[]
  label?: string
}

// Идея компонента: дерево разделов вообще без клиентского кода. Раскрытие
// держит нативный details, поэтому компонент остаётся серверным, работает до
// гидратации и находится встроенным поиском браузера. aria-level проставлен
// явно при обходе: из вложенности details скринридер глубину не выводит.
// В бейдже — число страниц во всей ветке, а не только в первом её уровне.
const STYLES = `
:where([data-vibeui-block="tree-003"]){
--vibeui-tree-003-bg:oklch(1 0 0);
--vibeui-tree-003-fg:oklch(0.24 0.014 265);
--vibeui-tree-003-muted:oklch(0.56 0.014 265);
--vibeui-tree-003-border:oklch(0.9 0.006 265);
--vibeui-tree-003-hover:oklch(0.97 0.004 265);
--vibeui-tree-003-chip:oklch(0.955 0.004 265);
--vibeui-tree-003-accent:oklch(0.55 0.17 265);
--vibeui-tree-003-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="tree-003"]{
width:100%;max-width:20rem;box-sizing:border-box;padding:0.5rem;
background:var(--vibeui-tree-003-bg);
border:1px solid var(--vibeui-tree-003-border);border-radius:0.875rem;
font-family:var(--vibeui-tree-003-font);font-size:0.8125rem;
color:var(--vibeui-tree-003-fg);
}
[data-vibeui-block="tree-003"] [data-part="caption"]{
display:block;padding:0.125rem 0.5rem 0.5rem;
font-size:0.6875rem;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;
color:var(--vibeui-tree-003-muted);
}
/* Раскрытие держит details: клиентского кода нет вовсе. */
[data-vibeui-block="tree-003"] summary{
list-style:none;cursor:pointer;
display:flex;align-items:center;gap:0.5rem;
min-height:2rem;padding:0 0.5rem;border-radius:0.5rem;
font-weight:600;
}
[data-vibeui-block="tree-003"] summary::-webkit-details-marker{display:none}
[data-vibeui-block="tree-003"] summary:hover{background:var(--vibeui-tree-003-hover)}
[data-vibeui-block="tree-003"] summary:focus-visible{
outline:2px solid var(--vibeui-tree-003-accent);outline-offset:-2px;
}
[data-vibeui-block="tree-003"] [data-part="caret"]{
flex:none;width:0;height:0;
border-left:0.3125rem solid var(--vibeui-tree-003-muted);
border-top:0.25rem solid transparent;border-bottom:0.25rem solid transparent;
transition:transform .14s ease;
}
[data-vibeui-block="tree-003"] details[open] > summary [data-part="caret"]{transform:rotate(90deg)}
[data-vibeui-block="tree-003"] [data-part="title"]{
flex:1 1 auto;min-width:0;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* Число страниц во всей ветке: считается при обходе, а не по первому уровню. */
[data-vibeui-block="tree-003"] [data-part="count"]{
flex:none;padding:0.0625rem 0.375rem;border-radius:9999px;
background:var(--vibeui-tree-003-chip);color:var(--vibeui-tree-003-muted);
font-size:0.625rem;font-weight:700;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="tree-003"] [data-part="children"]{
margin-left:0.6875rem;padding-left:0.5rem;
border-left:1px solid var(--vibeui-tree-003-border);
}
[data-vibeui-block="tree-003"] [data-part="leaf"]{
display:flex;align-items:center;gap:0.5rem;
min-height:1.875rem;padding:0 0.5rem;border-radius:0.5rem;
color:var(--vibeui-tree-003-fg);text-decoration:none;
}
[data-vibeui-block="tree-003"] [data-part="leaf"]:hover{background:var(--vibeui-tree-003-hover)}
[data-vibeui-block="tree-003"] [data-part="leaf"]:focus-visible{
outline:2px solid var(--vibeui-tree-003-accent);outline-offset:-2px;
}
[data-vibeui-block="tree-003"] [data-part="dash"]{
flex:none;width:0.4375rem;height:1px;background:var(--vibeui-tree-003-muted);
}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="tree-003"] *{animation:none!important;transition:none!important}
}
`

const DEFAULT_NODES: Tree003Node[] = [
  {
    name: "Начало работы",
    open: true,
    children: [
      { name: "Установка" },
      { name: "Первый компонент" },
      {
        name: "Настройка темы",
        children: [{ name: "Токены" }, { name: "Тёмная тема" }],
      },
    ],
  },
  {
    name: "Реестр",
    children: [
      { name: "Формат registry.json" },
      { name: "Сборка" },
      { name: "Публикация" },
    ],
  },
  { name: "Часто задаваемые вопросы" },
]

function countLeaves(nodes: Tree003Node[]): number {
  return nodes.reduce(
    (total, node) =>
      total + (node.children?.length ? countLeaves(node.children) : 1),
    0,
  )
}

function renderNodes(nodes: Tree003Node[], level: number) {
  return nodes.map((node) =>
    node.children?.length ? (
      <details key={node.name} open={node.open}>
        <summary role="treeitem" aria-level={level} aria-selected={false}>
          <span data-part="caret" aria-hidden="true" />
          <span data-part="title">{node.name}</span>
          <span data-part="count">{countLeaves(node.children)}</span>
        </summary>
        <div data-part="children" role="group">
          {renderNodes(node.children, level + 1)}
        </div>
      </details>
    ) : (
      <a
        key={node.name}
        data-part="leaf"
        role="treeitem"
        aria-level={level}
        aria-selected={false}
        href="#"
      >
        <span data-part="dash" aria-hidden="true" />
        <span data-part="title">{node.name}</span>
      </a>
    ),
  )
}

/**
 * Дерево разделов на нативных details: серверный рендер без клиентского кода.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Tree003({
  nodes = DEFAULT_NODES,
  label = "Документация",
  className,
  style,
  ...props
}: Tree003Props) {
  return (
    <>
      <style href="vibeui-tree-003" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="tree-003"
        className={className}
        style={style as CSSProperties}
      >
        <span data-part="caption">{label}</span>
        <div role="tree" aria-label={label}>
          {renderNodes(nodes, 1)}
        </div>
      </div>
    </>
  )
}