Display

Stage Rail

A horizontal stage rail: the connector takes the colour of a completed stage, and on narrow widths the rail scrolls with scroll-snap instead of folding into a column.

  • timeline
  • steps
  • horizontal
  • progress

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

Deal stages

  1. 1Заявка3 марта
  2. 2Оценка6 марта
  3. 3Договорсегодня
  4. 4Работыс 20 марта
  5. 5Сдачаапрель
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 "timeline-007" (Stage Rail) 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/timeline-007.json

Registry item: https://vibeui.ru/r/timeline-007.json
Installs to: components/vibeui/timeline-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 horizontal stage rail: the connector takes the colour of a completed stage, and on narrow widths the rail scrolls with scroll-snap instead of folding into a column.

A horizontal stage rail with numbered nodes: the connector shows what is done and a narrow block scrolls. Zero dependencies, one file, no client JS.

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

<Timeline007
  title="Deal stages"
  stages={[
    { title: "Request", date: "March 3", state: "done" },
    { title: "Contract", date: "today", state: "current" },
    { title: "Handover", date: "April", state: "todo" },
  ]}
/>

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-timeline-007-* palette — do not swap it for your theme tokens
- the horizontal axis at every width: a path folded into a column stops reading as a path
- the connector built from the stage's pseudo-element — it takes the state colour and vanishes at the last node
- the scroll-snap scrolling instead of wrapping: the stages must stay in one row
- the ring and aria-current="step" on the current node: state is not conveyed by colour alone
- the block's own light surface: dark text disappears on a dark page

## 6. You may change
- the heading through title
- the stages through stages: title, date and state done/current/todo
- the accent colour through accent
- the minimum stage width in the flex:1 0 7rem rule

## 7. Rules
- More than six stages in a row read badly even with scrolling: split them by meaning.
- Stage captions are short: a long phrase wraps and breaks the vertical rhythm of the nodes.
- The node number is positional and unrelated to the data: numbering cannot be passed in props.
- 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/timeline-007.json
https://vibeui.ru/r/timeline-007.json

Компонент самодостаточен: один файл, без зависимостей, палитра в локальных переменных --vibeui-timeline-007-*. Серверный: хуков нет. Соединитель — псевдоэлемент каждого этапа, растянутый от центра узла до центра следующего через left:calc(50% + 1rem) и right:calc(-50% + 1rem); он красится состоянием своего этапа и убирается у последнего. Узкая ширина не переворачивает ленту в столбец: горизонтальный путь, свёрнутый в вертикальный, врёт про порядок, поэтому список прокручивается с scroll-snap-type:x proximity. Текущий узел помечен кольцом box-shadow и aria-current="step".

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 Timeline007Stage = {
  title: string
  date?: string
  state?: "done" | "current" | "todo"
}

export type Timeline007Props = Omit<
  ComponentPropsWithoutRef<"section">,
  "children"
> & {
  stages?: Timeline007Stage[]
  title?: string
  accent?: string
}

// Идея компонента: этапы разложены по горизонтали, потому что так читается
// путь, а не список. Соединитель — псевдоэлемент каждого этапа, дотянутый до
// следующего кружка: он красится в цвет пройденного отрезка и обрывается на
// последнем узле. На узкой ширине лента не ломается в столбец, а прокручивается
// со scroll-snap: горизонтальный путь, свёрнутый в вертикальный, врёт про порядок.
const STYLES = `
:where([data-vibeui-block="timeline-007"]){
--vibeui-timeline-007-bg:oklch(1 0 0);
--vibeui-timeline-007-fg:oklch(0.22 0.014 265);
--vibeui-timeline-007-muted:oklch(0.57 0.014 265);
--vibeui-timeline-007-border:oklch(0.91 0.006 265);
--vibeui-timeline-007-done:oklch(0.58 0.14 152);
--vibeui-timeline-007-accent:oklch(0.55 0.18 262);
--vibeui-timeline-007-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="timeline-007"]{
display:flex;flex-direction:column;gap:0.75rem;
width:100%;max-width:32rem;box-sizing:border-box;padding:0.9375rem;
background:var(--vibeui-timeline-007-bg);
border:1px solid var(--vibeui-timeline-007-border);border-radius:0.875rem;
font-family:var(--vibeui-timeline-007-font);color:var(--vibeui-timeline-007-fg);
}
[data-vibeui-block="timeline-007"] [data-part="title"]{margin:0;font-size:0.9375rem;font-weight:650}
/* На узкой ширине лента прокручивается, а не переворачивается в столбец. */
[data-vibeui-block="timeline-007"] ol{
display:flex;margin:0;padding:0 0 0.25rem;list-style:none;
overflow-x:auto;overscroll-behavior-x:contain;
scroll-snap-type:x proximity;scrollbar-width:thin;
}
[data-vibeui-block="timeline-007"] li{
position:relative;flex:1 0 7rem;scroll-snap-align:start;
display:flex;flex-direction:column;align-items:center;gap:0.375rem;
padding:0 0.25rem;text-align:center;
}
/* Соединитель тянется до следующего узла и красится состоянием этого этапа. */
[data-vibeui-block="timeline-007"] li::before{
content:"";position:absolute;top:0.6875rem;left:calc(50% + 1rem);right:calc(-50% + 1rem);
height:2px;border-radius:2px;background:var(--vibeui-timeline-007-border);
}
[data-vibeui-block="timeline-007"] li[data-state="done"]::before{background:var(--vibeui-timeline-007-done)}
[data-vibeui-block="timeline-007"] li:last-child::before{display:none}
[data-vibeui-block="timeline-007"] [data-part="node"]{
position:relative;z-index:1;box-sizing:border-box;
display:flex;align-items:center;justify-content:center;
width:1.375rem;height:1.375rem;border-radius:9999px;
border:2px solid var(--vibeui-timeline-007-border);
background:var(--vibeui-timeline-007-bg);
font-size:0.625rem;font-weight:700;color:var(--vibeui-timeline-007-muted);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="timeline-007"] li[data-state="done"] [data-part="node"]{
border-color:var(--vibeui-timeline-007-done);background:var(--vibeui-timeline-007-done);
color:oklch(1 0 0);
}
[data-vibeui-block="timeline-007"] li[data-state="current"] [data-part="node"]{
border-color:var(--vibeui-timeline-007-accent);color:var(--vibeui-timeline-007-accent);
box-shadow:0 0 0 4px color-mix(in oklab,var(--vibeui-timeline-007-accent) 15%,transparent);
}
[data-vibeui-block="timeline-007"] [data-part="stage"]{font-size:0.8125rem;font-weight:600;line-height:1.25}
[data-vibeui-block="timeline-007"] li[data-state="todo"] [data-part="stage"]{color:var(--vibeui-timeline-007-muted);font-weight:500}
[data-vibeui-block="timeline-007"] [data-part="date"]{font-size:0.6875rem;color:var(--vibeui-timeline-007-muted);font-variant-numeric:tabular-nums}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="timeline-007"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_STAGES: Timeline007Stage[] = [
  { title: "Заявка", date: "3 марта", state: "done" },
  { title: "Оценка", date: "6 марта", state: "done" },
  { title: "Договор", date: "сегодня", state: "current" },
  { title: "Работы", date: "с 20 марта", state: "todo" },
  { title: "Сдача", date: "апрель", state: "todo" },
]

/**
 * Горизонтальная лента этапов: соединитель красится пройденным, узкий блок прокручивается.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Timeline007({
  stages = DEFAULT_STAGES,
  title = "Этапы сделки",
  accent,
  className,
  style,
  ...props
}: Timeline007Props) {
  const palette = {
    ...(accent ? { "--vibeui-timeline-007-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-timeline-007" precedence="medium">
        {STYLES}
      </style>
      <section
        {...props}
        data-vibeui-block="timeline-007"
        className={className}
        style={palette}
      >
        <h3 data-part="title">{title}</h3>
        <ol>
          {stages.map((stage, index) => {
            const state = stage.state ?? "todo"
            return (
              <li key={stage.title} data-state={state}>
                <span
                  data-part="node"
                  aria-current={state === "current" ? "step" : undefined}
                >
                  {index + 1}
                </span>
                <span data-part="stage">{stage.title}</span>
                {stage.date ? <span data-part="date">{stage.date}</span> : null}
              </li>
            )
          })}
        </ol>
      </section>
    </>
  )
}