Skip to content

UI-кит ​

English version: ui.md

@checkout-kit/ui — это видимая половина чекаута: поля, куда покупатель вводит карту, экраны, через которые проходит платёж, и токены, благодаря которым всё это выглядит вашим продуктом, а не библиотекой.

Обычный CSS и обычный React. Ни фреймворка, ни CSS-in-JS, ни пакета иконок, ни единой зависимости во время работы.

Установка из npm ​

sh
npm install @checkout-kit/core @checkout-kit/ui react@^19 react-dom@^19

Пакет в npm · Все варианты установки

Импортируйте @checkout-kit/ui/styles.css один раз. Если используете CheckoutRoot из примера ниже, дополнительно установите @checkout-kit/react и @checkout-kit/runtime-browser.

tsx
import '@checkout-kit/ui/styles.css'

Дальше повесьте ck-root на обёртку — или возьмите CheckoutRoot, который заодно несёт тему и платформу:

tsx
import { CheckoutRoot } from '@checkout-kit/react'

;<CheckoutRoot theme="auto">…</CheckoutRoot>

Все стили ограничены этим элементом и лежит в каскадном слое checkout, так что ваш CSS выигрывает без борьбы за специфичность. Если вы тоже используете слои — скажите, где наш:

css
@layer theme, base, components, checkout, utilities;

С Tailwind это значит импортировать его по частям, чтобы кит оказался после preflight — который иначе сбросит нарисованные им кнопки — и до утилит:

css
@layer theme, base, components, checkout, utilities;
@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/preflight.css' layer(base);
@import '@checkout-kit/ui/styles.css';
@import 'tailwindcss/utilities.css' layer(utilities);

Посмотреть собранный чекаут ​

Откройте живую галерею. Первый пример объединяет заказ, контактные данные, сохранённую/новую карту и банковский перевод. Preview state переключает ожидание, успех и отказ; Frame width показывает контейнеры 390px и 320px. Галерея не создаёт платёж и не сохраняет карту.

Собранный чекаут в светлой теме с компактной плотностью

Базовый стиль использует нейтральные поверхности, синий акцент, круглые кнопки и текст 16px в полях. Контролы сохраняют нативную семантику формы и управление с клавиатуры. Каталог ниже показывает отдельные компоненты с теми же темой, платформой, плотностью и цветом.

Темизация ​

УровеньПримерыЧто меняет
Примитивы--ck-p-accent-500Брендовый цвет, от которого вычисляются интерактивные оттенки.
Семантика--ck-accent, --ck-control-border, --ck-textОбщую роль в нескольких компонентах.
Компонент--ck-button-radius, --ck-input-heightГеометрию конкретного контрола.

Двигаете примитив — двигается всё под ним. Задаёте семантический токен — меняется только он.

Три яруса переменных. Обычно вы трогаете один.

Перебрендировать всё — сдвиньте акцент. Hover, нажатие, подложка и кольцо фокуса выводятся из него, поэтому едут вместе и не расходятся:

css
.ck-root {
  --ck-p-accent-500: #047857;
}

Поменять одну вещь — задайте её семантический токен:

css
.ck-root {
  --ck-radius: 8px;
  --ck-control-height: 3rem;
  --ck-font: 'Inter', system-ui, sans-serif;
}

Семантику читают сами правила: --ck-accent, --ck-surface{,-raised,-sunken}, --ck-border{,-strong}, --ck-text{,-muted,-subtle}, --ck-danger, --ck-success, плюс шкалы отступов (--ck-space-1…12), кеглей (--ck-font-size-xs…2xl), радиусов, высот контролов, фокуса и анимации. Ярус примитивов под ней (--ck-p-*) — это сырые рампы.

Один токен стоит знать по имени. --ck-scrim — то, во что затухает липкая панель действий. Кит не владеет вашей страницей и не знает её настоящего фона; по умолчанию берётся близкое к теме значение, а если ваш фон другой — переопределите:

css
.ck-root {
  --ck-scrim: #fff;
}

Выбирайте акцент с читаемым текстом во всех состояниях: контраст автоматически не вычисляется. Для светлого акцента задайте тёмный --ck-accent-contrast или accentContrast. Вариант Ink в галерее использует тёмную кнопку в светлой теме и светлую в тёмной. Бордер редактируемых полей задаётся отдельно через --ck-control-border / controlBorder.

Из JavaScript, если бренд живёт в пайплайне токенов, а не в стайлшите. appearanceToStyle() возвращает те же самые custom properties, которые вы написали бы руками, — это типизированная дверь к ним, а не вторая система темизации:

tsx
import { appearanceToStyle } from '@checkout-kit/ui'

;<CheckoutRoot theme="auto" style={appearanceToStyle({ accent: '#047857', radius: 8 })}>

Числа трактуются как пиксели, строки — как написано. Всё, чего нет в типе, по-прежнему задаётся одной строкой CSS на .ck-root.

Плотность. data-ck-density="compact" ужимает отступы и высоты контролов — для чекаута, встроенного в чужую страницу или живущего в WebView. Ставится через <CheckoutRoot density="compact">. Минимальная цель нажатия при этом намеренно не двигается: плотная раскладка — это выбор дизайна, а цель, в которую не попасть пальцем, — нет.

Светлая и тёмная. По умолчанию тёмная. data-ck-theme="light" — светлая, data-ck-theme="auto" — как в системе. Переопределяется только семантика, поэтому светлая тема — это тот же бренд, а не второй.

html
<div class="ck-root" data-ck-theme="auto">…</div>

Анимация, контраст и тач сделаны теми же токенами: один блок prefers-reduced-motion обнуляет все длительности, потому что ни одно правило не хардкодит их; prefers-contrast: more усиливает границы и кольцо фокуса; pointer: coarse поднимает минимальную цель до 48px.

Конвенции платформы. iPhone и Android расходятся в шрифте, радиусе скругления и в том, как выглядит нажатие, а из CSS не видно, что именно под ним. CheckoutRoot это определяет; без React вызывайте applyPlatform(root) из @checkout-kit/runtime-browser один раз на старте. platform="ios" фиксирует значение, а без атрибута всё падает в десктопные значения.

Всё остальное — тач, hover, цветовая схема, контраст, анимация — остаётся media-запросами, потому что это браузер знает сам.

Раскладка меряется по контейнеру, а не по окну. .ck-root — контейнер, и все решения о раскладке сделаны container-запросами. Один и тот же чекаут рисуется на всю страницу, в 360px WebView и внутри iframe мерчанта неизвестной ширины — вьюпорт в двух случаях из трёх врёт.

Компоненты ​

Всё это можно посмотреть на странице /gallery задеплоенного демо: каждый компонент на одной странице и переключатели темы, платформы, плотности и акцента.

Где основные из них стоят на экране чекаута:

Часть экранаИз чего собрать
Заказ и суммаOrderSummary, Money, Panel, PromoCodeInput
Способ оплатыPaymentMethodSelector, SavedInstrumentList, OptionCardGroup
Данные покупателяField, InputGroup, Input, ContactFields, AddressFields
Сбор данных / подтверждениеПоля SDK провайдера внутри ActionFrame
Отправка и результатPaymentButton, PaymentStatus, экраны результата и Receipt

Дальше платёж заканчивается на одном из экранов состояния — ProcessingState, AuthenticationState, SuccessState, FailureState, — которые заменяют форму, а не живут внутри неё.

Форма. Главный — Field. Он даёт контролу id, направляет на него label и называет подсказку и ошибку в aria-describedby. Через него проходит каждый инпут кита — поэтому ни одному экрану не нужно об этом помнить.

tsx
<Field label="Email" hint="Куда придёт чек" error={errors.email?.message} required>
  {(control) => <Input {...control} type="email" autoComplete="email" />}
</Field>

FieldGroup — это <fieldset> для полей, которые вместе задают один вопрос, например адрес плательщика, а FieldRow ставит два коротких поля в ряд, когда для двух хватает места, — и выравнивает их контролы, даже если подсказка есть только у одного.

Кроме Input есть Select, Textarea и Checkbox. Checkbox не проходит через Field: его подпись стоит рядом с боксом, а не над ним, поэтому у него свои label, description и error. Им соглашаются с условиями и сохраняют карту.

Поля с иконками и действиями. InputGroup рисует общую поверхность вокруг поля, декоративной иконки слева и суффикса или действия справа. Фокус и ошибка берутся у самого поля. Передавайте props из Field и ref формы в Input, чтобы связи label/ошибка сохранились. IconButton требует локализованный label; сама иконка декоративная.

tsx
<Field label="Email для чека" error={emailError}>
  {(control) => (
    <InputGroup
      leading={<MailIcon />}
      trailing={
        <IconButton label="Очистить email" onClick={clearEmail}>
          <CloseIcon />
        </IconButton>
      }
    >
      <Input {...control} type="email" ref={emailRef} autoComplete="email" />
    </InputGroup>
  )}
</Field>

Если кнопка очистки после нажатия исчезает, верните фокус в поле из обработчика. Отключайте действия справа вместе с полем, когда этого требует форма. Контент слева оставляйте декоративным.

Адрес и контакты. AddressFields и ContactFields существуют ради одного — autocomplete. Каждый токен здесь браузер понимает, а один неверный выключает автозаполнение всей формы. Список стран ваш: кит не возит данных. autoCompleteSection разводит платёжный адрес и адрес доставки по разным секциям.

Деньги. Суммы ходят в минорных единицах. formatMoney из @checkout-kit/core превращает их в то, что читает покупатель, спрашивая у Intl, сколько у валюты минорных единиц, вместо того чтобы возить таблицу. Именно на этом ошибаются самописные форматтеры: у JPY их нет, у KWD их три. <Money amount={1999} currency="USD" /> рисует это табличными цифрами.

OrderSummary собирает строки вместе. Арифметики он не делает: подытоги, налоги и скидки приходят посчитанными, потому что где округляется — дело вашей налоговой логики.

Остальное. PromoCodeInput (инпут с кнопкой применения; не вложенная <form>), SavedInstrumentList (просроченная карта показывается неактивной, а не прячется — тот, кто её ищет, должен увидеть почему), ExpressCheckout (ряд-слот и разделитель: каждый кошелёк диктует свою кнопку, поэтому вы приносите ту, что рисует его SDK), TrustStrip, Steps, Skeleton, SkeletonText, Countdown (для action.expiresAt: молчит, пока идёт, объявляется, когда истёк), CopyButton, Disclosure, Divider, Panel и Receipt.

Dialog — настоящий <dialog>, открытый через showModal(). Поэтому в ките нет кода ловушки фокуса: фокус, Tab, инертность страницы, Escape и возврат фокуса — всё браузерное. variant="sheet" закрепляет его у нижнего края и ограничивает ширину на десктопе. Видимая кнопка закрытия переводится через closeLabel; description связан с aria-describedby. dismissible={false} скрывает кнопку и запрещает закрытие по Escape или фону. Для такого шага задайте явное действие завершения.

ValidationSummary — вторая половина вежливых ошибок Field. Раз они не перебивают, длинной форме нечем сказать, сколько всего проблем при отправке. Этот блок забирает фокус, считает их и связывает каждую с её полем.

Ввод карты. CardNumberInput, ExpiryInput, CvcInput, CardholderInput, разложенные CardFields. Они форматируют по мере ввода, не выбрасывая каретку в конец, группируют цифры так, как их печатает бренд (4-6-5, как только появился префикс Amex), и берут длины из тех же правил, что и валидатор, — маска и сообщение разойтись не могут. Бренд показывается текстовым бейджем: логотипы схем — товарные знаки, свои передавайте через icons.

Выбор. OptionCardGroup и OptionCard — настоящие радиокнопки, скрытые и стилизованные через :has(:checked). Стрелки, семантика формы и озвучка достаются от браузера. PaymentMethodSelector — то же самое с платёжными метаданными, и методы приходят от вас: у кита своего списка нет.

tsx
<PaymentMethodSelector
  methods={[
    { id: 'card', label: 'Карта', description: 'Visa, Mastercard, Amex' },
    { id: 'transfer', label: 'Банковский перевод', badge: 'Мгновенно' },
  ]}
  value={method}
  onChange={setMethod}
/>

Платёж. PaymentStatus — единственный live-регион на странице: озвучивается то, что происходит с деньгами, а ошибки полей остаются вежливыми и его не заглушают. PaymentButton показывает спиннер и перестаёт реагировать, пока идёт платёж, но сохраняет подпись и фокус. Он также предотвращает нативный submit во время работы; Button учитывает заданный хостом aria-disabled. Подпись, меняющаяся на ходу, уводит цель из-под курсора, а disabled сбрасывает фокус в начало страницы.

ProcessingState, AuthenticationState, SuccessState и FailureState — экраны, на которых платёж заканчивается. Каждый начинается с заголовка. SuccessState и FailureState забирают фокус при появлении; фокус ожидания и аутентификации задаёт хост. В статическом каталоге передавайте результатам autoFocus={false}, а PaymentStatus — announce={false}. Настоящий результат сохраняет поведение по умолчанию. Отмена использует нейтральные иконку и цвет.

CheckoutLayout — это колонка с необязательным aside под сумму заказа: на телефоне над формой, от 48rem — липким блоком сбоку. ck-panel — опциональная приподнятая поверхность для того, что в неё положат.

ActionFrame — место, где рисует провайдер. Три варианта, потому что фрейм с полями, страница банка и QR-код хотят три разных размера:

tsx
<ActionFrame variant="challenge">
  <PaymentActionHost className="ck-action-host" />
</ActionFrame>

Доступность и граница ответственности ​

Набор даёт: label и ошибку, связанные с каждым полем; один alert и один status вместо россыпи; видимое кольцо фокуса на каждом контроле; настоящий таб-паттерн со стрелками; радиогруппы, достижимые с клавиатуры; цели 44px по умолчанию (48px для touch, на десктопе допускаются малые действия 36px); экраны результата, забирающие фокус. Компактная плотность сохраняет размер цели платформы.

За вами остаётся: куда уходит фокус при смене ваших страниц, и любые объявления, относящиеся к вашей вёрстке, а не к платежу. Кит не владеет вашей страницей и сделать это за вас не может.

Перевод ​

Любую строку, которую рисует кит, можно заменить. Библиотеки i18n внутри нет и мнения о том, какую брать, — тоже.

tsx
<PaymentStatus state={state} messages={{ processing: t('checkout.processing') }} />
<PaymentButton state={state}>{t('checkout.pay')}</PaymentButton>
<SuccessState heading={t('checkout.paid')} />

Раннеры в @checkout-kit/runtime-browser принимают строки так же — см. createBrowserRuntime({ redirect: { frameTitle }, display: { text } }).

Чего переводить не надо — PaymentError.message: это слова эмитента, и именно их покупатель пересказывает своему банку. Нужна своя формулировка — ветвитесь по PaymentError.code, а на незнакомый код показывайте сообщение как есть.

Проверить свою композицию ​

В исходниках запустите npm run test:ui. Набор проверяет десктопный Chromium, Chromium в размере Android, WebKit в размере iPhone и viewport 320px. Axe проверяет правила WCAG A/AA в двух темах и пяти акцентах; браузерные сценарии проверяют клавиатуру, радио и вкладки, закрытие sheet с возвратом фокуса, переполнение узких контейнеров, цели нажатия, RTL и результаты при reduced motion. CI прикладывает скриншоты и отчёт Playwright. Автоматические проверки покрывают обнаруживаемые правила; финальные тексты, фреймы провайдера и сценарий скринридера проверяйте в своём приложении.

Apache-2.0 · Headless-движок, адаптеры провайдеров и запускаемые примеры.