Pricing

Inverted Pricing

Светлая секция тарифов из трёх планов: рекомендованный выделен инвертированной тёмной панелью, остальные остаются лёгкими. Самодостаточна: не зависит от темы и стиля проекта.

  • light
  • pricing
  • saas
  • plans
  • landing
  • section

Превью

1440pxHost theme

Использовать с AI

  1. 1. Скопируйте ссылку.
  2. 2. Напишите агенту своими словами и вставьте её в предложение.
  3. 3. Агент откроет ссылку и поставит компонент из registry.
Показать полную инструкцию

То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.

# Установка и размещение «pricing-001» (Inverted Pricing) из VibeUI

## 1. Сначала установка — не пропускай, не пересоздавай
Команда установки недоступна: URL реестра VibeUI не сконфигурирован.

Ставится в: components/vibeui/pricing-001.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.

Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.

## 2. Что это
Светлая секция тарифов из трёх планов: рекомендованный выделен инвертированной тёмной панелью, остальные остаются лёгкими. Самодостаточна: не зависит от темы и стиля проекта.

Светлая секция тарифов. Центрированная шапка: eyebrow, h2 в две части и описание. Ниже три плана в ряд: имя, описание, крупная цена с периодом, CTA-ссылка и список до четырёх пунктов с галочками. Рекомендованный план — единственная тяжёлая поверхность в блоке: инвертированная тёмная панель с акцентным бейджем, приподнятая относительно соседей. Под планами строка-сноска про условия. Ноль зависимостей, один файл, раскладка и типографика считаются от ширины блока через container-запросы и cqi.

## 3. Как использовать
Это целая секция страницы. Вставляй её одним куском — не вынимай из
неё фрагменты и не собирай заново из мелких компонентов.
Имя экспорта и пропсы смотри в установленном файле.

## 4. Куда поставить
Это полноширинная секция страницы. Поставь её прямым потомком
разметки страницы, в том порядке, который назвал пользователь. Не
вкладывай внутрь карточки, сайдбара, модального окна или другого
узкого контейнера: секция меряет собственную ширину и разложится
неправильно.

Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)

## 5. Сохранить как установлено
- иерархию: ровно один рекомендованный план выделен инвертированной тёмной панелью, остальные остаются светлыми и без теней — не выравнивай все три в одинаковые карточки
- инверсию как приём: тёмная панель это единственная тяжёлая поверхность секции, она и создаёт акцент; не заменяй её на рамку, тень или другой оттенок светлого
- приподнятость рекомендованного плана относительно соседей от 48rem ширины блока
- светлую палитру --vibeui-pricing-001-* — не заменять на токены темы проекта (bg-background, text-foreground, primary, border) и не переводить секцию целиком в тёмную
- структуру плана: имя, описание, цена с периодом, CTA, список пунктов — и порядок этих элементов
- CTA как обычные ссылки <a href>: это секция лендинга, а не checkout; не превращай их в кнопки оплаты и не добавляй форму
- h2 как заголовок секции — блок ставится после hero и не должен приносить второй h1 на страницу; имена планов остаются h3
- container-type: inline-size на корне блока и @container-правила в его <style> — блок меряет собственную ширину, а не ширину окна; не переписывай их на viewport-медиазапросы
- типографику в единицах cqi, clamp и tracking
- одноразовый стагер планов; в этом блоке нет бесконечных анимаций
- блок <style> внутри компонента — в нём палитра, container-правила, keyframes и prefers-reduced-motion
- семантику: планы и пункты как <ul>/<li>, иконки-галочки aria-hidden

## 6. Можно менять
- тексты eyebrow, title, titleAccent, description и footnote
- до трёх планов: name, description, price, period
- до четырёх пунктов внутри каждого плана
- подписи и href у CTA каждого плана
- какой план рекомендован — флаг featured у нужного плана
- акцентный цвет — проп accent (и accentForeground для контраста текста на акценте)
- внешние отступы через className

## 7. Правила
- Не добавляй биллинг: в блоке нет и не должно быть подписок, оплаты, авторизации, обращений к API и состояния. Цены — статический текст.
- Не добавляй переключатель monthly/yearly: в блоке его намеренно нет, период оплаты объясняет строка footnote. Нерабочий переключатель, который выглядит кликабельным, вводит пользователя в заблуждение.
- Не добавляй четвёртый план и не убирай рекомендованный: рендерятся первые три плана и первые четыре пункта в каждом.
- Не добавляй иконки из icon-библиотек: блок собран без зависимостей.
- Не добавляй motion-библиотеки: анимации уже на CSS.
- Не выноси CSS-переменные в globals.css: блок должен оставаться одним файлом.
- Если меняешь accent, проверь контраст текста на акцентном CTA и на бейдже: нужно не менее 4.5:1.
- Сохраняй семантику ссылок. Для внутренней навигации можно использовать компонент роутера проекта, если сохраняются визуальные стили, доступность и поведение href.

## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.

Для разработчика

Команда установки недоступна: переменная окруженияREGISTRY_BASE_URL

Блок самодостаточен: один файл, без зависимостей, всегда светлый независимо от темы проекта. Цвета живут в локальных переменных --vibeui-pricing-001-*; акцент меняется пропом accent. Раскладка переключается container-запросами от ширины самого блока. Это секция лендинга, а не checkout: цены — статический контент, CTA — обычные ссылки, никакой логики биллинга в блоке нет.

Исходник компонента

Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.

import type { CSSProperties } from "react"

type Pricing001Action = {
  label: string
  href: string
}

type Pricing001Plan = {
  name: string
  description: string
  price: string
  period?: string
  features: string[]
  action: Pricing001Action
  featured?: boolean
}

export type Pricing001Props = {
  eyebrow?: string
  title?: string
  titleAccent?: string
  description?: string
  plans?: Pricing001Plan[]
  footnote?: string
  accent?: string
  accentForeground?: string
  className?: string
}

// Весь CSS блока живёт здесь, а не в globals.css проекта: палитра, раскладка
// и keyframes. Переменные объявлены в :where() с нулевой специфичностью,
// поэтому пользователь переопределяет их чем угодно.
//
// Секция светлая, но рекомендованный план — инвертированная тёмная панель.
// Это единственная «тяжёлая» поверхность в блоке: она и создаёт иерархию,
// поэтому у остальных планов нет ни рамок-карточек, ни теней.
//
// container-type делает блок собственным query-контейнером: раскладка и размер
// шрифта считаются от ширины блока, а не от ширины окна. Поэтому три колонки
// видны и в миниатюре каталога, где блок рендерится в 1280px внутри узкого
// окна. Правила раскладки — плоским CSS, а не Tailwind-вариантами, чтобы блок
// не зависел от версии Tailwind в чужом проекте; специфичность (0,2,0) выше
// утилит.
const STYLES = `
:where([data-vibeui-block="pricing-001"]){
--vibeui-pricing-001-bg:oklch(0.985 0.002 285);
--vibeui-pricing-001-card:oklch(1 0 0);
--vibeui-pricing-001-ink:oklch(0.2 0.015 285);
--vibeui-pricing-001-muted:oklch(0.5 0.012 285);
--vibeui-pricing-001-border:oklch(0.9 0.006 285);
--vibeui-pricing-001-accent:oklch(0.55 0.19 295);
--vibeui-pricing-001-accent-fg:oklch(0.99 0.003 285);
--vibeui-pricing-001-featured-bg:oklch(0.2 0.015 285);
--vibeui-pricing-001-featured-fg:oklch(0.98 0.002 285);
--vibeui-pricing-001-featured-muted:oklch(0.74 0.01 285);
--vibeui-pricing-001-featured-border:oklch(1 0 0 / 14%);
--vibeui-pricing-001-ring:color-mix(in oklab, var(--vibeui-pricing-001-accent) 70%, transparent);
--vibeui-pricing-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="pricing-001"]{container-type:inline-size}
@container (min-width:48rem){
[data-vibeui-block="pricing-001"] [data-part="frame"]{min-height:660px;padding-left:2.5rem;padding-right:2.5rem}
[data-vibeui-block="pricing-001"] [data-part="plans"]{grid-template-columns:repeat(3,minmax(0,1fr))}
[data-vibeui-block="pricing-001"] [data-part="plan"][data-featured="true"]{margin-block:-1rem}
}
@container (min-width:80rem){
[data-vibeui-block="pricing-001"] [data-part="frame"]{min-height:720px;padding-left:4rem;padding-right:4rem;padding-top:4rem;padding-bottom:4rem}
[data-vibeui-block="pricing-001"] [data-part="inner"]{max-width:1120px;margin-inline:auto}
}
@keyframes vibeui-pricing-001-fade-up{from{opacity:0;transform:translate3d(0,12px,0)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="pricing-001"] *{animation:none!important;transition:none!important}}
`

const ENTER =
  "animate-[vibeui-pricing-001-fade-up_0.5s_cubic-bezier(0.16,1,0.3,1)_both]"

const ACTION_BASE =
  "inline-flex h-10 w-full items-center justify-center rounded-lg px-4 text-[0.875rem] font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--vibeui-pricing-001-ring)]"

const PLAN_DELAYS = [
  "[animation-delay:90ms]",
  "[animation-delay:150ms]",
  "[animation-delay:210ms]",
]

function cx(...classes: (string | false | undefined)[]) {
  return classes.filter(Boolean).join(" ")
}

function CheckIcon({ featured }: { featured: boolean }) {
  return (
    <svg
      aria-hidden="true"
      viewBox="0 0 16 16"
      fill="none"
      className={cx(
        "mt-0.5 size-4 shrink-0",
        featured
          ? "text-[var(--vibeui-pricing-001-featured-fg)]"
          : "text-[var(--vibeui-pricing-001-accent)]",
      )}
    >
      <path
        d="M3.5 8.5 6.5 11.5 12.5 5"
        stroke="currentColor"
        strokeWidth="1.75"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  )
}

/**
 * Единственная логика блока: какой план подсвечен. Первый с featured, иначе
 * средний. Никакого биллинга, тарифных расчётов и состояния — цены статичны.
 */
function resolveFeaturedIndex(plans: Pricing001Plan[]): number {
  const explicit = plans.findIndex((plan) => plan.featured)

  return explicit === -1 ? Math.floor((plans.length - 1) / 2) : explicit
}

export function Pricing001({
  eyebrow = "Pricing",
  title = "Priced for teams that",
  titleAccent = "ship weekly",
  description = "Every plan includes the full component library. You pay for seats and support, not for features.",
  plans = [
    {
      name: "Solo",
      description: "For one person shipping side projects.",
      price: "$0",
      period: "forever",
      features: ["1 project", "Community support", "MIT license"],
      action: { label: "Start for free", href: "#" },
    },
    {
      name: "Team",
      description: "For product teams with a shared design language.",
      price: "$29",
      period: "per seat / month",
      features: [
        "Unlimited projects",
        "Priority support",
        "Design tokens sync",
        "Private component registry",
      ],
      action: { label: "Start 14-day trial", href: "#" },
      featured: true,
    },
    {
      name: "Studio",
      description: "For agencies delivering to many clients.",
      price: "$79",
      period: "per seat / month",
      features: ["Everything in Team", "Client handoff", "SSO and audit log"],
      action: { label: "Talk to sales", href: "#" },
    },
  ],
  footnote = "Billed annually, switch anytime. 30-day refund, no questions asked.",
  accent,
  accentForeground,
  className,
}: Pricing001Props) {
  const style = {
    ...(accent ? { "--vibeui-pricing-001-accent": accent } : {}),
    ...(accentForeground
      ? { "--vibeui-pricing-001-accent-fg": accentForeground }
      : {}),
  } as CSSProperties

  const visiblePlans = plans.slice(0, 3)
  const featuredIndex = resolveFeaturedIndex(visiblePlans)

  return (
    <section
      data-vibeui-block="pricing-001"
      style={style}
      className={cx(
        "relative isolate overflow-hidden bg-[var(--vibeui-pricing-001-bg)] font-[family-name:var(--vibeui-pricing-001-font)] text-[var(--vibeui-pricing-001-ink)] antialiased",
        className,
      )}
    >
      <style href="vibeui-pricing-001" precedence="medium">
        {STYLES}
      </style>

      {/* Отступы и высота живут на внутреннем слое: container-запросы читают
          ширину секции, а сама секция своим контейнером быть не может. */}
      <div
        data-part="frame"
        className="flex min-h-[560px] items-center px-5 py-12"
      >
        <div data-part="inner" className="w-full">
          <header className={cx(ENTER, "mx-auto max-w-[40rem] text-center")}>
            {eyebrow ? (
              <p className="text-[0.75rem] font-medium tracking-[0.09em] text-[var(--vibeui-pricing-001-accent)] uppercase">
                {eyebrow}
              </p>
            ) : null}

            <h2 className="mt-4 text-[clamp(1.75rem,3cqi,2.25rem)] leading-[1.12] font-semibold tracking-tight text-balance break-words">
              {title}
              {titleAccent ? (
                <>
                  {" "}
                  <span className="text-[var(--vibeui-pricing-001-accent)]">
                    {titleAccent}
                  </span>
                </>
              ) : null}
            </h2>

            {description ? (
              <p className="mt-3 text-[clamp(0.9375rem,1.15cqi,1rem)] leading-relaxed text-pretty break-words text-[var(--vibeui-pricing-001-muted)]">
                {description}
              </p>
            ) : null}
          </header>

          <ul data-part="plans" className="mt-8 grid items-stretch gap-4">
            {visiblePlans.map((plan, index) => {
              const featured = index === featuredIndex

              return (
                <li
                  key={plan.name}
                  data-part="plan"
                  data-featured={featured ? "true" : undefined}
                  className={cx(
                    ENTER,
                    PLAN_DELAYS[index],
                    "flex flex-col rounded-xl border p-6",
                    featured
                      ? "border-[var(--vibeui-pricing-001-featured-border)] bg-[var(--vibeui-pricing-001-featured-bg)] text-[var(--vibeui-pricing-001-featured-fg)]"
                      : "border-[var(--vibeui-pricing-001-border)] bg-[var(--vibeui-pricing-001-card)]",
                  )}
                >
                  <div className="flex items-center gap-3">
                    <h3 className="text-[1rem] font-medium">{plan.name}</h3>
                    {featured ? (
                      <span className="rounded-full bg-[var(--vibeui-pricing-001-accent)] px-2 py-0.5 text-[0.6875rem] font-medium text-[var(--vibeui-pricing-001-accent-fg)]">
                        Recommended
                      </span>
                    ) : null}
                  </div>

                  <p
                    className={cx(
                      "mt-2 text-[0.8125rem] leading-relaxed text-pretty",
                      featured
                        ? "text-[var(--vibeui-pricing-001-featured-muted)]"
                        : "text-[var(--vibeui-pricing-001-muted)]",
                    )}
                  >
                    {plan.description}
                  </p>

                  <p className="mt-5 flex items-baseline gap-2">
                    <span className="text-[2.25rem] leading-none font-semibold tabular-nums">
                      {plan.price}
                    </span>
                    {plan.period ? (
                      <span
                        className={cx(
                          "text-[0.8125rem]",
                          featured
                            ? "text-[var(--vibeui-pricing-001-featured-muted)]"
                            : "text-[var(--vibeui-pricing-001-muted)]",
                        )}
                      >
                        {plan.period}
                      </span>
                    ) : null}
                  </p>

                  <a
                    href={plan.action.href}
                    className={cx(
                      ACTION_BASE,
                      "mt-5",
                      featured
                        ? "bg-[var(--vibeui-pricing-001-accent)] text-[var(--vibeui-pricing-001-accent-fg)] transition-[filter] duration-150 hover:brightness-110"
                        : "border border-[var(--vibeui-pricing-001-border)] transition-colors duration-150 hover:bg-[var(--vibeui-pricing-001-bg)]",
                    )}
                  >
                    {plan.action.label}
                  </a>

                  <ul className="mt-6 flex flex-col gap-2.5">
                    {plan.features.slice(0, 4).map((feature) => (
                      <li
                        key={feature}
                        className={cx(
                          "flex gap-2.5 text-[0.8125rem]",
                          featured
                            ? "text-[var(--vibeui-pricing-001-featured-muted)]"
                            : "text-[var(--vibeui-pricing-001-muted)]",
                        )}
                      >
                        <CheckIcon featured={featured} />
                        {feature}
                      </li>
                    ))}
                  </ul>
                </li>
              )
            })}
          </ul>

          {footnote ? (
            <p
              className={cx(
                ENTER,
                "mt-8 text-center text-[0.8125rem] text-[var(--vibeui-pricing-001-muted)] [animation-delay:270ms]",
              )}
            >
              {footnote}
            </p>
          ) : null}
        </div>
      </div>
    </section>
  )
}