WelcomeTour

Онбординг-тур по интерфейсу: затемняет страницу, подсвечивает целевой элемент шага и показывает рядом подсказку с заголовком, описанием, индикатором прогресса и кнопками навигации. Шаги описываются пропом steps, запуск — через open / defaultOpen.

Когда использовать

  • Первое знакомство с разделом: показать, где что находится, сразу после входа или после релиза.
  • Анонс новой функции — короткий тур из одного-двух шагов вокруг новых элементов.
  • Обучающий сценарий, где важна последовательность: сначала фильтры, потом действие над выбранным.

Когда не нужен:

  • Подсказка к одному элементу:
    • Tooltip — короткое пояснение по наведению.
    • HotSpot — точка привлечения внимания без пошагового сценария.
  • Блокирующее подтверждение — Modal.
  • Справка, которую читают целиком, а не проходят по шагам — отдельная страница документации.

Анатомия

Тур состоит из трёх частей: полноэкранный оверлей, вырез вокруг целевого элемента шага (spotlight) и подсказка, прикреплённая к вырезу. Оверлей перехватывает клики по остальной странице; целевой элемент остаётся видимым в вырезе.

Слоты подсказки

Каждый элемент steps[i] собирается из:

  • title — заголовок шага.
  • subtitle — подзаголовок под заголовком.
  • content — тело шага.
  • target — целевой элемент: CSS-селектор, DOM-нода, ref или геттер.

Все слоты необязательные, кроме target. Индикатор прогресса появляется автоматически, когда шагов больше одного, и остаётся неинтерактивным — он показывает позицию в туре, навигация идёт кнопками.

showStepIndicator={false} убирает индикатор вместе с озвучкой позиции для скринридера. Это нужно туру, у которого длина неизвестна заранее: шаг с ненайденной целью тур пропускает на лету (адаптив скрыл блок, раздел недоступен по правам), а пересчитать общее число он не может — «2 из 5» тогда врёт. Когда все шаги гарантированно доступны, индикатор оставляют: он показывает, сколько ещё идти.

Placement (default bottom)

steps[i].placement задаёт положение подсказки относительно целевого элемента: top, bottom, left, right и их варианты -start / -end, а также auto (сторона подбирается автоматически) и center (подсказка по центру экрана — для шага без привязки к элементу). Если выбранной стороне не хватает места, подсказка разворачивается на противоположную.

Кнопки (default ['back', 'primary', 'skip'])

Проп buttons перечисляет, какие кнопки показывает подсказка:

  • back — «Назад»; на первом шаге скрывается автоматически.
  • primary — «Далее», на последнем шаге — кнопка завершения.
  • skip — кнопка закрытия в шапке подсказки, завершает тур досрочно.

Подписи берутся из locale пакета и переопределяются пропом labels (весь тур) либо steps[i].labels (один шаг). Escape завершает тур целиком, как у остальных оверлеев ДС; пока тур открыт, Tab ходит по кругу внутри подсказки.

Do / Don’t

  • ✅ Один тур на сценарий: 3–5 шагов вокруг ключевых элементов раздела.
  • ❌ Тур на 12 шагов через весь интерфейс — такой редко проходят до конца.
  • ✅ Запускать тур один раз и запоминать факт прохождения на стороне приложения (onOpenChange со статусом finished / skipped).
  • ❌ Показывать тур при каждом входе в раздел — он становится модальным препятствием.
  • ✅ Привязывать шаг к элементу, который уже отрисован и виден.
  • ❌ Указывать target на элемент за пределами вьюпорта или появляющийся асинхронно — шаг не найдёт цель.
  • ✅ Формулировать шаг одним действием: что это и зачем нажимать.
  • ❌ Складывать в content абзац справки — для этого есть документация раздела.
  • ✅ Скрывать индикатор (showStepIndicator={false}), если часть шагов может оказаться недоступной.
  • ❌ Показывать «2 из 5» туру, который на узком экране проходит за три шага.
  • ✅ Оставлять skip: пользователь должен уметь выйти в любой момент.
  • ❌ Делать тур безвыходным (buttons только с primary) в обязательном онбординге.

Установка

pnpm add @cloud-ru/ds-uikit-product-welcome-tour
import { TOUR_BUTTON, TOUR_PLACEMENT, WelcomeTour } from '@cloud-ru/ds-uikit-product-welcome-tour';

Примеры использования

Базовое использование

Базовое использованиеТур из двух шагов, целевые элементы задаются ref-ами.
МенюПоиск
tsx
import { Button } from '@cloud-ru/ds-button';
import { WelcomeTour } from '@cloud-ru/ds-uikit-product-welcome-tour';
import { useRef, useState } from 'react';

export function Basic() {
  const menuRef = useRef<HTMLSpanElement>(null);
  const searchRef = useRef<HTMLSpanElement>(null);
  const [open, setOpen] = useState(false);

  return (
    <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
      <span ref={menuRef}>Меню</span>
      <span ref={searchRef}>Поиск</span>

      <Button label='Запустить тур' appearance='neutral' view='outline' onClick={() => setOpen(true)} />

      <WelcomeTour
        open={open}
        onOpenChange={setOpen}
        steps={[
          {
            target: menuRef,
            title: 'Меню разделов',
            subtitle: 'Навигация по проекту',
            content: 'Отсюда открываются все разделы: ресурсы, биллинг и настройки.',
          },
          {
            target: searchRef,
            title: 'Поиск',
            content: 'Ищет по ресурсам проекта и открывает найденное в текущем разделе.',
          },
        ]}
      />
    </div>
  );
}

Управляемый режим

Управляемый режим`open` и `stepIndex` живут во внешнем состоянии — тур запускается с произвольного шага.
МенюПоискТекущий шаг: 1
tsx
import { Button } from '@cloud-ru/ds-button';
import { TourStep, WelcomeTour } from '@cloud-ru/ds-uikit-product-welcome-tour';
import { useRef, useState } from 'react';

export function Controlled() {
  const menuRef = useRef<HTMLSpanElement>(null);
  const searchRef = useRef<HTMLSpanElement>(null);
  const [open, setOpen] = useState(false);
  const [stepIndex, setStepIndex] = useState(0);

  const steps: TourStep[] = [
    { target: menuRef, title: 'Меню разделов', content: 'Первый шаг тура.' },
    { target: searchRef, title: 'Поиск', content: 'Второй шаг тура.' },
  ];

  const start = (index: number) => {
    setStepIndex(index);
    setOpen(true);
  };

  return (
    <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
      <span ref={menuRef}>Меню</span>
      <span ref={searchRef}>Поиск</span>

      <Button label='С первого шага' appearance='neutral' view='outline' onClick={() => start(0)} />
      <Button label='Со второго шага' appearance='neutral' view='outline' onClick={() => start(1)} />

      <span>Текущий шаг: {stepIndex + 1}</span>

      <WelcomeTour open={open} stepIndex={stepIndex} steps={steps} onOpenChange={setOpen} onStepChange={setStepIndex} />
    </div>
  );
}

Свои подписи кнопок

Свои подписи кнопок`labels` переопределяет дефолты из locale, `steps[i].labels` — подписи одного шага.
МенюБиллинг
tsx
import { Button } from '@cloud-ru/ds-button';
import { WelcomeTour } from '@cloud-ru/ds-uikit-product-welcome-tour';
import { useRef, useState } from 'react';

export function CustomLabels() {
  const menuRef = useRef<HTMLSpanElement>(null);
  const billingRef = useRef<HTMLSpanElement>(null);
  const [open, setOpen] = useState(false);

  return (
    <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
      <span ref={menuRef}>Меню</span>
      <span ref={billingRef}>Биллинг</span>

      <Button label='Запустить тур' appearance='neutral' view='outline' onClick={() => setOpen(true)} />

      <WelcomeTour
        open={open}
        onOpenChange={setOpen}
        // Подписи всего тура: переопределяют дефолты из locale.
        labels={{ next: 'Дальше', back: 'Назад', finish: 'Всё понятно' }}
        steps={[
          { target: menuRef, title: 'Меню разделов', content: 'Навигация по проекту.' },
          {
            target: billingRef,
            title: 'Биллинг',
            content: 'Расходы проекта и настройки оплаты.',
            // Подписи одного шага: переопределяют `labels` компонента.
            labels: { finish: 'Перейти в биллинг' },
          },
        ]}
      />
    </div>
  );
}

Тур без кнопки «Назад»

Тур без кнопки «Назад»`buttons` без `back` — линейный сценарий, только вперёд.
МенюПоиск
tsx
import { Button } from '@cloud-ru/ds-button';
import { TOUR_BUTTON, WelcomeTour } from '@cloud-ru/ds-uikit-product-welcome-tour';
import { useRef, useState } from 'react';

export function WithoutBackButton() {
  const menuRef = useRef<HTMLSpanElement>(null);
  const searchRef = useRef<HTMLSpanElement>(null);
  const [open, setOpen] = useState(false);

  return (
    <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
      <span ref={menuRef}>Меню</span>
      <span ref={searchRef}>Поиск</span>

      <Button label='Запустить тур' appearance='neutral' view='outline' onClick={() => setOpen(true)} />

      <WelcomeTour
        open={open}
        // Набор кнопок подсказки: без `back` тур идёт только вперёд.
        buttons={[TOUR_BUTTON.Primary, TOUR_BUTTON.Skip]}
        onOpenChange={setOpen}
        steps={[
          { target: menuRef, title: 'Меню разделов', content: 'Первый шаг.' },
          { target: searchRef, title: 'Поиск', content: 'Последний шаг — кнопки «Назад» нет.' },
        ]}
      />
    </div>
  );
}

Props

Types

PropsWelcomeTourProps
PropTypeDefaultRequiredDescription
buttonsTourButton[][TOUR_BUTTON.Back, TOUR_BUTTON.Primary, TOUR_BUTTON.Skip]noНабор кнопок подсказки.
defaultOpenbooleanfalsenoЗапущен ли тур изначально. Неуправляемый режим.
defaultStepIndexnumber0noИндекс шага, с которого начинается тур. Неуправляемый режим.
labelsPartial<TourLabels>—noПодписи кнопок. Переопределяют значения из locale.
onOpenChange((open: boolean, status: TourStatus) => void)—noКолбек смены состояния тура. Вторым аргументом приходит статус, с которым тур завершился.
onStepChange((index: number) => void)—noКолбек смены шага. В неуправляемом режиме сообщает об уже случившемся переходе — в том числе о показе первого шага при запуске тура. В управляемом (когда задан `stepIndex`) это запрос на переход: компонент сам шаг не меняет, новый индекс обязан применить потребитель — иначе тур остановится на текущем шаге.
openboolean—noЗапущен ли тур. Управляемый режим.
portalContainerHTMLElement | null—noКонтейнер для портала. По умолчанию — контейнер из `PortalContextProvider` либо `document.body`.
scrollOffsetnumber20noОтступ при скролле к целевому элементу, px.
showStepIndicatorbooleantruenoПоказывать ли индикатор прогресса. Он и так появляется только у тура длиннее одного шага; `false` убирает его вместе с озвучкой позиции для скринридера — это нужно там, где часть шагов может отвалиться на лету (цели нет на странице, шаг пропускается), и «2 из 5» окажется неправдой.
spotlightPaddingTourSpotlightPadding10noОтступ выреза от границ целевого элемента для всех шагов. Шаг переопределяет своим `spotlightPadding`.
stepIndexnumber—noИндекс текущего шага. Управляемый режим.
stepsTourStep[]—yesШаги тура.

Unions

Types

WelcomeTourProps

Unions

Адаптивность

Только desktop. Компонент не читает раскладку из @cloud-ru/ds-adaptive и отдельной mobile-поверхности не получит: пошаговый онбординг — сценарий десктопного интерфейса. Подсказка и полноэкранный оверлей одинаковы на всех разрешениях.

Что это значит на практике:

  • Ширина подсказки ограничена сверху и на узком экране занимает почти всю его ширину.
  • Оверлей и вырез вокруг целевого элемента считаются от вьюпорта и работают на любом размере, но подсказка рядом с мелким элементом может не поместиться на выбранной стороне — движок развернёт её на противоположную.
  • layoutPresets у компонента нет: пропы применяются одинаково на всех раскладках, AdaptiveProvider на тур не влияет.

Если раздел открывают преимущественно с телефона, тур там лучше не запускать: решение о показе принимает приложение, и оно же хранит состояние прохождения.

Storybook

Figma

Смотри также

  • HotSpot — точка привлечения внимания к элементу без пошагового сценария.
  • Popover — всплывающий слой рядом с триггером.
  • Tooltip — короткая подсказка по наведению.