Code Block

File Tree Tabs

Several example files in one block: the tabs stand in a column on the left, like in an editor, and a radio group switches them without a single line of JavaScript.

  • code
  • tabs
  • files
  • no-js

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/codeblock-015?lang=en

src/app
export default function Page() {
  return <Catalog />
}
// общая оболочка каталога
export const revalidate = 3600
export default function Loading() {
  return <Skeleton rows={6} />
}
"use client" // ошибки ловим на клиенте
export default function Error() {
  return <p>Каталог не загрузился</p>
}
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 "codeblock-015" (File Tree Tabs) 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/codeblock-015.json

Registry item: https://vibeui.ru/r/codeblock-015.json
Installs to: components/vibeui/codeblock-015.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
Several example files in one block: the tabs stand in a column on the left, like in an editor, and a radio group switches them without a single line of JavaScript.

Four example files with tabs in a left column: state on a radio group and :has(), layout driven by the block's own width. Zero dependencies, no client JS.

## 3. How to use it
import { Codeblock015 } from "@/components/vibeui/codeblock-015"

<Codeblock015
  root="src/app"
  activeIndex={0}
  group="catalog-files"
/>

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-codeblock-015-* palette — do not swap it for your theme tokens
- the radio group as the source of state: with useState the tabs stop working before hydration
- picking the panel by index rather than by file name: a rename otherwise blanks the panel
- hiding the input with clip-path rather than display:none — otherwise the tabs leave the tab order
- the layout rules on the inner shell: a container query never applies to the container itself
- the dark surface of the block — light monospaced text is unreadable without it

## 6. You may change
- the list of files through files
- the folder caption through root
- the initially open file through activeIndex
- the radio group name through group when a page holds several such blocks

## 7. Rules
- Six panels are supported: the selection selectors are spelled out in the CSS and stop there.
- Two blocks sharing the same group on one page form a single radio group and switch each other.
- There is deliberately no copy button — it would bring back the client JS this block exists to avoid.
- 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/codeblock-015.json
https://vibeui.ru/r/codeblock-015.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-codeblock-015-*. Серверный: состояние держит нативная радиогруппа, нужная панель выбирается селектором :has(input[value=...]:checked) по индексу файла, поэтому селекторы не зависят от имён и переживают любой список. Раскладка считается от собственной ширины: до 30rem вкладки лежат строкой сверху, дальше уезжают в колонку слева — правила стоят на внутреннем [data-part="shell"], потому что контейнерный запрос не действует на сам контейнер. Радиокнопки спрятаны clip-path, а не display:none, чтобы остаться в порядке обхода клавиатурой.

Component source

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

import type { CSSProperties } from "react"

export type Codeblock015Token = {
  text: string
  kind?: "keyword" | "string" | "comment" | "number" | "type"
}

export type Codeblock015File = {
  name: string
  lines: Codeblock015Token[][]
}

export type Codeblock015Props = {
  root?: string
  activeIndex?: number
  files?: Codeblock015File[]
  group?: string
  className?: string
  style?: CSSProperties
}

// Идея компонента: не один файл, а маленькое дерево проекта рядом с кодом.
// Вкладки стоят колонкой слева, как в редакторе, состояние держит радиогруппа,
// а панель выбирается через :has() по индексу — клиентского JS нет вообще.
const STYLES = `
:where([data-vibeui-block="codeblock-015"]){
--vibeui-codeblock-015-bg:oklch(0.19 0.02 260);
--vibeui-codeblock-015-rail:oklch(0.23 0.024 260);
--vibeui-codeblock-015-fg:oklch(0.93 0.008 260);
--vibeui-codeblock-015-muted:oklch(0.66 0.018 260);
--vibeui-codeblock-015-border:oklch(1 0 0 / 12%);
--vibeui-codeblock-015-accent:oklch(0.8 0.13 250);
--vibeui-codeblock-015-keyword:oklch(0.79 0.13 300);
--vibeui-codeblock-015-string:oklch(0.83 0.12 145);
--vibeui-codeblock-015-comment:oklch(0.6 0.02 260);
--vibeui-codeblock-015-number:oklch(0.84 0.12 72);
--vibeui-codeblock-015-type:oklch(0.83 0.11 230);
--vibeui-codeblock-015-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
--vibeui-codeblock-015-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
container-type:inline-size;
}
[data-vibeui-block="codeblock-015"]{
display:block;width:100%;max-width:38rem;box-sizing:border-box;margin:0;
font-family:var(--vibeui-codeblock-015-font);
}
[data-vibeui-block="codeblock-015"] [data-part="shell"]{
display:grid;grid-template-columns:1fr;overflow:hidden;
border:1px solid var(--vibeui-codeblock-015-border);border-radius:0.75rem;
background:var(--vibeui-codeblock-015-bg);color:var(--vibeui-codeblock-015-fg);
}
[data-vibeui-block="codeblock-015"] [data-part="rail"]{
display:flex;flex-wrap:wrap;gap:0.125rem;padding:0.375rem;
background:var(--vibeui-codeblock-015-rail);
border-bottom:1px solid var(--vibeui-codeblock-015-border);
}
[data-vibeui-block="codeblock-015"] [data-part="root"]{
flex:1 0 100%;padding:0.125rem 0.375rem 0.3125rem;
font-size:0.6875rem;letter-spacing:0.06em;text-transform:uppercase;
color:var(--vibeui-codeblock-015-muted);
}
[data-vibeui-block="codeblock-015"] label{
flex:1 1 auto;cursor:pointer;display:flex;align-items:center;gap:0.375rem;
padding:0.3125rem 0.5rem;border-radius:0.375rem;
font-family:var(--vibeui-codeblock-015-mono);font-size:0.75rem;
color:var(--vibeui-codeblock-015-muted);
transition:background-color .16s ease,color .16s ease;
}
[data-vibeui-block="codeblock-015"] label::before{
content:"";flex:none;width:0.375rem;height:0.375rem;border-radius:50%;
background:currentColor;opacity:.45;
}
[data-vibeui-block="codeblock-015"] input{
position:absolute;width:1px;height:1px;margin:-1px;padding:0;
border:0;clip-path:inset(50%);overflow:hidden;white-space:nowrap;
}
[data-vibeui-block="codeblock-015"] label:hover{color:var(--vibeui-codeblock-015-fg)}
[data-vibeui-block="codeblock-015"] label:has(input:checked){
background:oklch(1 0 0 / 10%);color:var(--vibeui-codeblock-015-accent);
}
[data-vibeui-block="codeblock-015"] label:has(input:checked)::before{opacity:1}
[data-vibeui-block="codeblock-015"] label:has(input:focus-visible){
outline:2px solid var(--vibeui-codeblock-015-accent);outline-offset:2px;
}
[data-vibeui-block="codeblock-015"] pre{
display:none;margin:0;padding:0.875rem;overflow-x:auto;
}
[data-vibeui-block="codeblock-015"] code{
display:block;min-width:max-content;
font-family:var(--vibeui-codeblock-015-mono);
font-size:0.8125rem;line-height:1.65;white-space:pre;
}
[data-vibeui-block="codeblock-015"] [data-token="keyword"]{color:var(--vibeui-codeblock-015-keyword)}
[data-vibeui-block="codeblock-015"] [data-token="string"]{color:var(--vibeui-codeblock-015-string)}
[data-vibeui-block="codeblock-015"] [data-token="comment"]{color:var(--vibeui-codeblock-015-comment);font-style:italic}
[data-vibeui-block="codeblock-015"] [data-token="number"]{color:var(--vibeui-codeblock-015-number)}
[data-vibeui-block="codeblock-015"] [data-token="type"]{color:var(--vibeui-codeblock-015-type)}
/* Панель выбирает отмеченная радиокнопка: значение — индекс файла, поэтому
   селекторы не зависят от имён и переживают любой список файлов. */
[data-vibeui-block="codeblock-015"] [data-part="shell"]:has(input[value="0"]:checked) pre[data-index="0"],
[data-vibeui-block="codeblock-015"] [data-part="shell"]:has(input[value="1"]:checked) pre[data-index="1"],
[data-vibeui-block="codeblock-015"] [data-part="shell"]:has(input[value="2"]:checked) pre[data-index="2"],
[data-vibeui-block="codeblock-015"] [data-part="shell"]:has(input[value="3"]:checked) pre[data-index="3"],
[data-vibeui-block="codeblock-015"] [data-part="shell"]:has(input[value="4"]:checked) pre[data-index="4"],
[data-vibeui-block="codeblock-015"] [data-part="shell"]:has(input[value="5"]:checked) pre[data-index="5"]{display:block}
@container (min-width: 30rem){
[data-vibeui-block="codeblock-015"] [data-part="shell"]{grid-template-columns:11rem 1fr}
[data-vibeui-block="codeblock-015"] [data-part="rail"]{
flex-direction:column;flex-wrap:nowrap;align-content:start;
border-bottom:0;border-right:1px solid var(--vibeui-codeblock-015-border);
}
[data-vibeui-block="codeblock-015"] label{flex:0 0 auto}
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="codeblock-015"] *{animation:none!important;transition:none!important}}
`

const FILES: Codeblock015File[] = [
  {
    name: "page.tsx",
    lines: [
      [
        { text: "export default function ", kind: "keyword" },
        { text: "Page", kind: "type" },
        { text: "() {" },
      ],
      [
        { text: "  return", kind: "keyword" },
        { text: " <" },
        { text: "Catalog", kind: "type" },
        { text: " />" },
      ],
      [{ text: "}" }],
    ],
  },
  {
    name: "layout.tsx",
    lines: [
      [{ text: "// общая оболочка каталога", kind: "comment" }],
      [
        { text: "export const ", kind: "keyword" },
        { text: "revalidate = " },
        { text: "3600", kind: "number" },
      ],
    ],
  },
  {
    name: "loading.tsx",
    lines: [
      [
        { text: "export default function ", kind: "keyword" },
        { text: "Loading", kind: "type" },
        { text: "() {" },
      ],
      [
        { text: "  return", kind: "keyword" },
        { text: " <" },
        { text: "Skeleton", kind: "type" },
        { text: " rows={" },
        { text: "6", kind: "number" },
        { text: "} />" },
      ],
      [{ text: "}" }],
    ],
  },
  {
    name: "error.tsx",
    lines: [
      [
        { text: '"use client"', kind: "string" },
        { text: " // ошибки ловим на клиенте", kind: "comment" },
      ],
      [
        { text: "export default function ", kind: "keyword" },
        { text: "Error", kind: "type" },
        { text: "() {" },
      ],
      [
        { text: "  return <p>" },
        { text: "Каталог не загрузился", kind: "string" },
        { text: "</p>" },
      ],
      [{ text: "}" }],
    ],
  },
]

/** Несколько файлов в одном блоке: вкладки колонкой, переключение без JS. */
export function Codeblock015({
  root = "src/app",
  activeIndex = 0,
  files = FILES,
  group = "vibeui-codeblock-015",
  className,
  style,
}: Codeblock015Props) {
  return (
    <>
      <style href="vibeui-codeblock-015" precedence="medium">
        {STYLES}
      </style>
      <figure
        data-vibeui-block="codeblock-015"
        className={className}
        style={style}
      >
        <div data-part="shell">
          <div data-part="rail" role="group" aria-label="Файлы примера">
            <span data-part="root">{root}</span>
            {files.slice(0, 6).map((file, index) => (
              <label key={file.name}>
                <input
                  type="radio"
                  name={group}
                  value={index}
                  defaultChecked={index === activeIndex}
                />
                {file.name}
              </label>
            ))}
          </div>
          {files.slice(0, 6).map((file, index) => (
            <pre key={file.name} data-index={index} aria-label={file.name}>
              <code>
                {file.lines.map((line, row) => (
                  <span key={row}>
                    {line.map((token, position) => (
                      <span key={position} data-token={token.kind}>
                        {token.text}
                      </span>
                    ))}
                    {row < file.lines.length - 1 ? "\n" : null}
                  </span>
                ))}
              </code>
            </pre>
          ))}
        </div>
      </figure>
    </>
  )
}