Tables

Comparison Table

A comparison table: the first column sticks while scrolling right, and the chosen option is highlighted through every row, not just the header.

  • comparison
  • table
  • pricing
  • features

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/table-002?lang=en

Что входитСтартКомандаСтудия
Проектов110Без ограничений
Свой доменнетдада
Совместная работанетдада
История публикаций7 дней90 днейНавсегда
ПоддержкаПочтаПочтаЧат за час
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 "table-002" (Comparison Table) 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/table-002.json

Registry item: https://vibeui.ru/r/table-002.json
Installs to: components/vibeui/table-002.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 comparison table: the first column sticks while scrolling right, and the chosen option is highlighted through every row, not just the header.

A comparison table: options across, features down. The first column sticks during horizontal scrolling so a row never loses its label, and the recommended option is highlighted in every row. A boolean becomes a check or a dash with a hidden word for screen readers. Zero dependencies, one file, its own palette.

## 3. How to use it
import { Table002 } from "@/components/vibeui/table-002"

<Table002
  options={["Start", "Team", "Studio"]}
  highlight={1}
  rows={[{ label: "Custom domain", values: [false, true, true] }]}
/>

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-table-002-* palette — do not swap it for your theme tokens (bg-muted, text-primary and the like)
- the sticky first column with its shadow border: scrolling right without it leaves rows unlabelled
- highlighting the column through every row rather than the header alone — otherwise the eye loses the chosen option mid-table
- the hidden word beside the check and the dash: a screen reader reads "✓" and "—" as symbols
- th with scope="row" on the feature labels and scope="col" on the options — that is what ties a cell to both headers
- horizontal scrolling in the wrapper: a five-column comparison does not fit a phone
- the <style> block inside the component — it holds the palette, the sticky column and the highlight

## 6. You may change
- options — the names being compared, rows — the features and values
- highlight — the index of the highlighted column; -1 removes it
- the values: a string shows as-is, a boolean becomes a check or a dash
- the accent through the accent prop — it colours the highlighted column
- width and outer spacing through className

## 7. Rules
- Do not compare more than four options: the columns fall below a readable width.
- Every row must describe the same thing across options. A feature only one option has reads as a benefit rather than a difference.
- There is deliberately no buy button: comparison answers "how do they differ", while the pricing block does the selling.
- 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/table-002.json
https://vibeui.ru/r/table-002.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-table-002-*. Первая колонка держится на position:sticky с левым краем и тенью-границей. Значение ячейки — либо строка, либо булево: булево превращается в галочку или прочерк, рядом с которыми лежит слово для скринридера. Подсветка колонки ставится data-атрибутом на ячейки всех строк, а не только шапки.

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 Table002Value = boolean | string

export type Table002Row = {
  label: string
  values: Table002Value[]
}

export type Table002Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  /** Названия сравниваемых вариантов. */
  options?: string[]
  rows?: Table002Row[]
  /** Номер выделенной колонки, считая с нуля. -1 — без выделения. */
  highlight?: number
  accent?: string
}

// Идея компонента: сравнение читают строками, а глазами держатся за колонку.
// Поэтому первая колонка липнет при горизонтальной прокрутке, а выбранный
// вариант подсвечен целиком, а не только в заголовке.
const STYLES = `
:where([data-vibeui-block="table-002"]){
--vibeui-table-002-fg:oklch(0.24 0.016 265);
--vibeui-table-002-muted:oklch(0.54 0.014 265);
--vibeui-table-002-bg:oklch(1 0 0);
--vibeui-table-002-border:oklch(0.91 0.006 265);
--vibeui-table-002-accent:oklch(0.55 0.2 262);
--vibeui-table-002-yes:oklch(0.58 0.15 152);
--vibeui-table-002-radius:0.75rem;
--vibeui-table-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="table-002"]{
width:100%;box-sizing:border-box;overflow-x:auto;
border:1px solid var(--vibeui-table-002-border);
border-radius:var(--vibeui-table-002-radius);
background:var(--vibeui-table-002-bg);color:var(--vibeui-table-002-fg);
font-family:var(--vibeui-table-002-font);
}
[data-vibeui-block="table-002"] table{width:100%;border-collapse:separate;border-spacing:0;font-size:0.875rem}
[data-vibeui-block="table-002"] th,
[data-vibeui-block="table-002"] td{
padding:0.6875rem 0.875rem;border-bottom:1px solid var(--vibeui-table-002-border);
text-align:center;white-space:nowrap;
}
[data-vibeui-block="table-002"] tr:last-child th,
[data-vibeui-block="table-002"] tr:last-child td{border-bottom:0}
[data-vibeui-block="table-002"] thead th{
font-size:0.8125rem;font-weight:600;
background:oklch(0.975 0.003 265);
}
/* Первая колонка липнет: при прокрутке вправо строка не теряет подпись. */
[data-vibeui-block="table-002"] [data-part="label"]{
position:sticky;left:0;z-index:1;
text-align:left;font-weight:500;white-space:normal;
background:var(--vibeui-table-002-bg);
box-shadow:1px 0 0 var(--vibeui-table-002-border);
}
[data-vibeui-block="table-002"] thead [data-part="label"]{background:oklch(0.975 0.003 265)}
/* Выделенный вариант подсвечен во всех строках, а не только в шапке. */
[data-vibeui-block="table-002"] [data-highlight="true"]{
background:color-mix(in oklab,var(--vibeui-table-002-accent) 7%,transparent);
}
[data-vibeui-block="table-002"] thead [data-highlight="true"]{
color:var(--vibeui-table-002-accent);
background:color-mix(in oklab,var(--vibeui-table-002-accent) 12%,transparent);
}
[data-vibeui-block="table-002"] [data-part="yes"]{color:var(--vibeui-table-002-yes);font-weight:600}
[data-vibeui-block="table-002"] [data-part="no"]{color:var(--vibeui-table-002-muted)}
/* Слово для скринридера: галочку и прочерк он прочитает как символы. */
[data-vibeui-block="table-002"] [data-part="sr"]{
position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="table-002"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_OPTIONS = ["Старт", "Команда", "Студия"]

const DEFAULT_ROWS: Table002Row[] = [
  { label: "Проектов", values: ["1", "10", "Без ограничений"] },
  { label: "Свой домен", values: [false, true, true] },
  { label: "Совместная работа", values: [false, true, true] },
  { label: "История публикаций", values: ["7 дней", "90 дней", "Навсегда"] },
  { label: "Поддержка", values: ["Почта", "Почта", "Чат за час"] },
]

/**
 * Таблица сравнения вариантов: липкая первая колонка, выделенный столбец.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Table002({
  options = DEFAULT_OPTIONS,
  rows = DEFAULT_ROWS,
  highlight = 1,
  accent,
  className,
  style,
  ...props
}: Table002Props) {
  const palette = {
    ...(accent ? { "--vibeui-table-002-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-table-002" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="table-002"
        className={className}
        style={palette}
      >
        <table>
          <thead>
            <tr>
              <th data-part="label" scope="col">
                Что входит
              </th>
              {options.map((option, index) => (
                <th
                  key={option}
                  scope="col"
                  data-highlight={index === highlight || undefined}
                >
                  {option}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {rows.map((row) => (
              <tr key={row.label}>
                <th data-part="label" scope="row">
                  {row.label}
                </th>
                {row.values.map((value, index) => (
                  <td
                    key={`${row.label}-${index}`}
                    data-highlight={index === highlight || undefined}
                  >
                    {typeof value === "boolean" ? (
                      <span data-part={value ? "yes" : "no"}>
                        <span aria-hidden="true">{value ? "✓" : "—"}</span>
                        <span data-part="sr">{value ? "да" : "нет"}</span>
                      </span>
                    ) : (
                      value
                    )}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  )
}