Charts

Critical Path

A plan where the critical path is set apart by weight: zero-slack tasks get a fill and a heavy border, while the rest show a dashed tail of allowed slippage.

  • gantt
  • critical-path
  • slack
  • schedule

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/gantt-007?lang=en

Critical path

24 дней, без запаса 6 из 8

  1. БрифКП2 дн
  2. ИсследованиеКП5 дн
  3. Тексты4 дн
  4. МакетыКП6 дн
  5. ВёрсткаКП7 дн
  6. Фотосъёмка3 дн
  7. ПроверкаКП3 дн
  8. ЗапускКП1 дн
Сроки и запас таблицей
ЗадачаНачалоКонецЗапас, дн
Бриф (критическая)2026-05-042026-05-050
Исследование (критическая)2026-05-062026-05-100
Тексты2026-05-062026-05-097
Макеты (критическая)2026-05-112026-05-160
Вёрстка (критическая)2026-05-172026-05-230
Фотосъёмка2026-05-062026-05-0815
Проверка (критическая)2026-05-242026-05-260
Запуск (критическая)2026-05-272026-05-270
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 "gantt-007" (Critical Path) 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/gantt-007.json

Registry item: https://vibeui.ru/r/gantt-007.json
Installs to: components/vibeui/gantt-007.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 plan where the critical path is set apart by weight: zero-slack tasks get a fill and a heavy border, while the rest show a dashed tail of allowed slippage.

The critical path is computed from the links at render time: early and late dates, slack, and emphasis by weight. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Gantt007 } from "@/components/vibeui/gantt-007"

<Gantt007
  startDate="2026-05-04"
  tasks={[
    { id: "design", title: "Design", days: 6, after: ["research"] },
    { id: "build", title: "Build", days: 7, after: ["design"] },
  ]}
/>

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-gantt-007-* palette — do not swap it for your theme tokens
- the emphasis by weight rather than colour alone: fill, border and bold text survive print and colour blindness
- the «CP» tag as text — criticality cannot be stated by a hue
- the dashed slack tail: it answers how far a task may slip
- the scroll container with overflow-x and tabindex=0: a long plan is wider than the screen
- the dates and slack table in <details>: a chart needs a text alternative

## 6. You may change
- the tasks array together with its after links
- the plan start through startDate
- the caption through heading
- the accent through accent and the colour of non-critical bars

## 7. Rules
- The array has to be topologically ordered: a task comes after all of its predecessors.
- Cycles in the links are not checked — looped data yields meaningless dates.
- Plain consecutive calendar days are used: weekends and people's load are ignored.
- 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/gantt-007.json
https://vibeui.ru/r/gantt-007.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра --vibeui-gantt-007-*. Серверный: хуков нет. Функция schedule() делает прямой проход по связям after (ранние сроки) и обратный (поздние сроки), запас считается их разностью, нулевой запас означает критическую задачу. Раскладка идёт не по колонкам грида, а в процентах от длины проекта на flex-дорожках, поэтому масштаб не зависит от числа дней. Прокрутка вбок в контейнере с tabindex=0, сроки и запас продублированы таблицей в <details>.

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 Gantt007Task = {
  id: string
  title: string
  days: number
  /** id задач, после которых эта может начаться. */
  after?: string[]
}

export type Gantt007Props = Omit<
  ComponentPropsWithoutRef<"section">,
  "children" | "title"
> & {
  heading?: string
  startDate?: string
  tasks?: Gantt007Task[]
  accent?: string
}

// Идея компонента: критический путь — это не цвет, а начертание. Задачи без
// запаса получают сплошную заливку, жирную рамку и подпись «КП», остальные —
// тонкий контур и полупрозрачный «хвост» запаса: сразу видно, где план
// сдвинется от любой задержки, а где есть люфт. Сроки и запас считаются из
// связей прямо при рендере — прямого и обратного прохода хватает, состояния
// нет, компонент остаётся серверным.
const STYLES = `
:where([data-vibeui-block="gantt-007"]){
--vibeui-gantt-007-bg:oklch(1 0 0);
--vibeui-gantt-007-fg:oklch(0.22 0.014 265);
--vibeui-gantt-007-muted:oklch(0.6 0.014 265);
--vibeui-gantt-007-border:oklch(0.91 0.006 265);
--vibeui-gantt-007-line:oklch(0.955 0.004 265);
--vibeui-gantt-007-accent:oklch(0.52 0.19 25);
--vibeui-gantt-007-calm:oklch(0.55 0.03 265);
--vibeui-gantt-007-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="gantt-007"]{
width:100%;box-sizing:border-box;padding:1rem;
background:var(--vibeui-gantt-007-bg);
border:1px solid var(--vibeui-gantt-007-border);border-radius:1rem;
color:var(--vibeui-gantt-007-fg);font-family:var(--vibeui-gantt-007-font);
}
[data-vibeui-block="gantt-007"] *{box-sizing:border-box}
[data-vibeui-block="gantt-007"] ol,
[data-vibeui-block="gantt-007"] ul{margin:0;padding:0;list-style:none}
[data-vibeui-block="gantt-007"] [data-part="head"]{
display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;
gap:0.5rem;margin:0 0 0.75rem;
}
[data-vibeui-block="gantt-007"] [data-part="heading"]{
margin:0;font-size:0.9375rem;font-weight:700;letter-spacing:-0.01em;
}
[data-vibeui-block="gantt-007"] [data-part="hint"]{
margin:0;font-size:0.75rem;color:var(--vibeui-gantt-007-muted);
}
[data-vibeui-block="gantt-007"] [data-part="scroll"]{
overflow-x:auto;border:1px solid var(--vibeui-gantt-007-line);border-radius:0.625rem;
padding:0.5rem 0.625rem 0.625rem;
}
[data-vibeui-block="gantt-007"] [data-part="scroll"]:focus-visible{
outline:2px solid var(--vibeui-gantt-007-accent);outline-offset:2px;
}
[data-vibeui-block="gantt-007"] [data-part="shell"]{min-width:30rem}
[data-vibeui-block="gantt-007"] [data-part="row"]{
display:flex;align-items:center;gap:0.625rem;padding:0.1875rem 0;
}
[data-vibeui-block="gantt-007"] [data-part="label"]{
flex:none;width:9rem;font-size:0.75rem;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* Дорожка занимает всю оставшуюся ширину: полоса ставится в процентах
   от длины проекта, поэтому сетка колонок вообще не нужна. */
[data-vibeui-block="gantt-007"] [data-part="track"]{
position:relative;flex:1;height:1.25rem;
background:repeating-linear-gradient(
to right,
var(--vibeui-gantt-007-line) 0 1px,
transparent 1px calc(100% / var(--vibeui-gantt-007-ticks,10)));
}
[data-vibeui-block="gantt-007"] [data-part="bar"]{
position:absolute;top:0.125rem;bottom:0.125rem;
display:flex;align-items:center;padding:0 0.375rem;border-radius:0.25rem;
font-size:0.5625rem;line-height:1;white-space:nowrap;overflow:hidden;
font-variant-numeric:tabular-nums;
border:1px solid var(--vibeui-gantt-007-calm);
color:var(--vibeui-gantt-007-fg);
background:color-mix(in oklab,var(--vibeui-gantt-007-calm) 10%,var(--vibeui-gantt-007-bg));
}
/* Критическая задача отличается начертанием, а не оттенком: заливка,
   двойная рамка и жирный текст переживают и печать, и дальтонизм. */
[data-vibeui-block="gantt-007"] [data-part="bar"][data-critical="true"]{
border:2px solid var(--vibeui-gantt-007-accent);
background:color-mix(in oklab,var(--vibeui-gantt-007-accent) 22%,var(--vibeui-gantt-007-bg));
font-weight:750;
box-shadow:inset 0 0 0 1px var(--vibeui-gantt-007-bg);
}
[data-vibeui-block="gantt-007"] [data-part="row"][data-critical="true"] [data-part="label"]{
font-weight:700;
}
[data-vibeui-block="gantt-007"] [data-part="row"][data-critical="false"] [data-part="label"]{
color:var(--vibeui-gantt-007-muted);
}
/* Хвост запаса: докуда задачу можно сдвинуть, ничего не сломав. */
[data-vibeui-block="gantt-007"] [data-part="slack"]{
position:absolute;top:0.4375rem;bottom:0.4375rem;
border-top:1px dashed var(--vibeui-gantt-007-calm);
border-bottom:1px dashed var(--vibeui-gantt-007-calm);
}
[data-vibeui-block="gantt-007"] [data-part="scale"]{
display:flex;margin-left:9.625rem;
}
[data-vibeui-block="gantt-007"] [data-part="scale"] span{
flex:1;font-size:0.5625rem;color:var(--vibeui-gantt-007-muted);
font-variant-numeric:tabular-nums;
border-left:1px solid var(--vibeui-gantt-007-line);padding-left:0.1875rem;
}
[data-vibeui-block="gantt-007"] [data-part="tag"]{
margin-left:0.375rem;padding:0 0.1875rem;border-radius:0.1875rem;
background:var(--vibeui-gantt-007-accent);color:var(--vibeui-gantt-007-bg);
font-size:0.5rem;font-weight:700;letter-spacing:0.04em;
}
[data-vibeui-block="gantt-007"] [data-part="table"]{margin:0.75rem 0 0}
[data-vibeui-block="gantt-007"] summary{
cursor:pointer;font-size:0.75rem;font-weight:600;color:var(--vibeui-gantt-007-accent);
}
[data-vibeui-block="gantt-007"] summary:focus-visible{
outline:2px solid var(--vibeui-gantt-007-accent);outline-offset:2px;border-radius:0.25rem;
}
[data-vibeui-block="gantt-007"] table{
width:100%;margin-top:0.5rem;border-collapse:collapse;font-size:0.6875rem;
}
[data-vibeui-block="gantt-007"] th,
[data-vibeui-block="gantt-007"] td{
padding:0.25rem 0.375rem;text-align:left;
border-bottom:1px solid var(--vibeui-gantt-007-line);
}
[data-vibeui-block="gantt-007"] thead th{color:var(--vibeui-gantt-007-muted);font-weight:600}
[data-vibeui-block="gantt-007"] td{font-variant-numeric:tabular-nums}
[data-vibeui-block="gantt-007"] tr[data-critical="true"] th{font-weight:750}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="gantt-007"] *{animation:none!important;transition:none!important}}
`

const DAY = 86400000

const DEFAULT_TASKS: Gantt007Task[] = [
  { id: "brief", title: "Бриф", days: 2 },
  { id: "research", title: "Исследование", days: 5, after: ["brief"] },
  { id: "content", title: "Тексты", days: 4, after: ["brief"] },
  { id: "design", title: "Макеты", days: 6, after: ["research"] },
  { id: "build", title: "Вёрстка", days: 7, after: ["design", "content"] },
  { id: "photo", title: "Фотосъёмка", days: 3, after: ["brief"] },
  { id: "qa", title: "Проверка", days: 3, after: ["build", "photo"] },
  { id: "launch", title: "Запуск", days: 1, after: ["qa"] },
]

/**
 * Прямой и обратный проход по связям: ранние сроки, поздние сроки и запас.
 * Задача без запаса лежит на критическом пути.
 */
function schedule(tasks: Gantt007Task[]) {
  const earliest = new Map<string, number>()

  for (const task of tasks) {
    const start = (task.after ?? []).reduce(
      (max, id) => Math.max(max, earliest.get(id) ?? 0),
      0,
    )
    earliest.set(task.id, start + task.days)
  }

  const total = Math.max(...earliest.values())
  const latest = new Map<string, number>()

  for (const task of [...tasks].reverse()) {
    const finish = tasks
      .filter((other) => other.after?.includes(task.id))
      .reduce(
        (min, other) =>
          Math.min(min, (latest.get(other.id) ?? total) - other.days),
        total,
      )
    latest.set(task.id, finish)
  }

  return {
    total,
    rows: tasks.map((task) => {
      const finish = earliest.get(task.id) ?? task.days
      const start = finish - task.days
      const slack = (latest.get(task.id) ?? total) - finish

      return { task, start, finish, slack, critical: slack === 0 }
    }),
  }
}

/**
 * План с выделенным критическим путём: срок, запас и критичность считаются
 * из связей при рендере. Один файл, ноль зависимостей, клиентского JS нет.
 */
export function Gantt007({
  heading = "Критический путь",
  startDate = "2026-05-04",
  tasks = DEFAULT_TASKS,
  accent,
  className,
  style,
  ...props
}: Gantt007Props) {
  const origin = new Date(`${startDate}T00:00:00Z`).getTime()
  const { total, rows } = schedule(tasks)
  const ticks = Math.min(10, total)
  const step = Math.ceil(total / ticks)
  const critical = rows.filter((row) => row.critical).length

  const dateText = (offset: number) =>
    new Date(origin + offset * DAY).toISOString().slice(0, 10)

  const palette = {
    "--vibeui-gantt-007-ticks": ticks,
    ...(accent ? { "--vibeui-gantt-007-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-gantt-007" precedence="medium">
        {STYLES}
      </style>
      <section
        {...props}
        data-vibeui-block="gantt-007"
        aria-label={heading}
        className={className}
        style={palette}
      >
        <header data-part="head">
          <h3 data-part="heading">{heading}</h3>
          <p data-part="hint">
            {total} дней, без запаса {critical} из {rows.length}
          </p>
        </header>

        <div
          data-part="scroll"
          tabIndex={0}
          role="group"
          aria-label={`${heading}: диаграмма, прокручивается вбок`}
        >
          <div data-part="shell">
            <ol>
              {rows.map((row) => (
                <li
                  key={row.task.id}
                  data-part="row"
                  data-critical={String(row.critical)}
                >
                  <span data-part="label">
                    {row.task.title}
                    {row.critical ? <b data-part="tag">КП</b> : null}
                  </span>

                  <span data-part="track">
                    {row.slack > 0 ? (
                      <i
                        data-part="slack"
                        aria-hidden="true"
                        style={{
                          left: `${(row.finish / total) * 100}%`,
                          width: `${(row.slack / total) * 100}%`,
                        }}
                      />
                    ) : null}

                    <b
                      data-part="bar"
                      data-critical={String(row.critical)}
                      style={{
                        left: `${(row.start / total) * 100}%`,
                        width: `${(row.task.days / total) * 100}%`,
                      }}
                    >
                      {row.task.days} дн
                    </b>
                  </span>
                </li>
              ))}
            </ol>

            <div data-part="scale" aria-hidden="true">
              {Array.from({ length: ticks }, (_, index) => (
                <span key={index}>{index * step}</span>
              ))}
            </div>
          </div>
        </div>

        <details data-part="table">
          <summary>Сроки и запас таблицей</summary>
          <table>
            <thead>
              <tr>
                <th scope="col">Задача</th>
                <th scope="col">Начало</th>
                <th scope="col">Конец</th>
                <th scope="col">Запас, дн</th>
              </tr>
            </thead>
            <tbody>
              {rows.map((row) => (
                <tr key={row.task.id} data-critical={String(row.critical)}>
                  <th scope="row">
                    {row.task.title}
                    {row.critical ? " (критическая)" : ""}
                  </th>
                  <td>{dateText(row.start)}</td>
                  <td>{dateText(row.finish - 1)}</td>
                  <td>{row.slack}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </details>
      </section>
    </>
  )
}