# @cloud-ru/ds-bottom-sheet > Mobile-first overlay-контейнер с drag-handle и swipe-down для диалогов, выпадающих списков и фильтров. Поддерживает snap-points (раскрытие на половину → full). Docs: /snack-v2/components/bottom-sheet/ ## Установка ```sh pnpm add @cloud-ru/ds-bottom-sheet ``` ## Когда использовать - Полу-полно-экранный диалог на мобильном устройстве. - Action-sheet («Выбрать действие»: фото, удалить, отменить). - Multi-step flow с back-кнопкой в шапке. - Контейнер для выпадающего списка / фильтров на мобильном. ## API ### BottomSheet | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actionButton` | `ReactNode` | — | no | Action-кнопка справа в шапке (любой ReactNode — обычно `Button view='function'`). | | `additionalButton` | `BottomSheetActionButton` | — | no | Дополнительная (третья) кнопка — объект пропсов `Button` (по умолчанию `view='simple'`, `appearance='neutral'`). | | `approveButton` | `BottomSheetActionButton` | — | no | Основная кнопка действия — объект пропсов `Button` (по умолчанию `view='filled'`, `appearance='primary'`). Ширина зависит от `footerActionsOrientation` и числа кнопок. | | `bodyPadding` | `boolean` | `true` | no | Горизонтальные паддинги body. При `false` контент идёт во всю ширину (edge-to-edge) — для карт, изображений, списков без отступов. Соответствует Figma-оси `padding=false`. | | `cancelButton` | `BottomSheetActionButton` | — | no | Кнопка отмены — объект пропсов `Button` (по умолчанию `view='outline'`, `appearance='neutral'`). | | `className` | `string` | — | no | CSS-класс самого sheet-контейнера. | | `closeOnPopstate` | `boolean` | `true` | no | Закрывать sheet при `popstate` (browser-back на mobile). | | `container` | `string \| HTMLElement` | — | no | Контейнер для портала. По дефолту — `body` либо контекст-провайдер `@cloud-ru/ds-portal-context`. | | `content` | `ReactNode` | — | no | Основное содержимое (рендерится в `BottomSheetCustom.Body`). | | `data-test-id` | `string` | — | no | | | `defaultSnapIndex` | `number` | `0` | no | Индекс snap'а, на котором sheet открывается по дефолту. Игнорируется при controlled `snapIndex`. | | `footer` | `ReactNode` | — | no | Произвольный футер. Если задан — имеет приоритет над `approveButton` / `cancelButton` / `additionalButton`. | | `footerActionsOrientation` | `horizontal \| vertical` | `'horizontal'` | no | Ориентация кнопок футера, собранных из `approveButton` / `cancelButton` / `additionalButton`. Применяется **только при ровно двух** кнопках (canonical cancel/confirm): - `'horizontal'` — кнопки в ряд через space-between: secondary слева, primary справа, ширина по контенту. Точное соответствие Figma `bottomBar.buttonGroup`. - `'vertical'` — кнопки в столбик, full-width (primary сверху). Одна кнопка всегда рендерится full-width (одиночный CTA); три кнопки не помещаются в ряд на mobile-вьюпорте и всегда идут в столбик — для них значение игнорируется. Игнорируется при заданном `footer` (произвольная разметка футера). | | `footerTestIds` | `{ approve?: string; cancel?: string; additional?: string \| undefined; } \| undefined` | — | no | Переопределение `data-test-id` собранных слотов футера (approve/cancel/additional). По умолчанию — собственные id `BottomSheet`. Адаптивные `Modal`/`Drawer` передают сюда свои `TEST_IDS.footer*`, чтобы футер метился одинаково на desktop-поверхности и в mobile-sheet'е. | | `lockScroll` | `boolean` | `true` | no | Блокировать ли скролл фона на время открытия (`react-remove-scroll`). При `false` страница под sheet'ом остаётся прокручиваемой — для non-modal сценариев (sheet поверх контента, с которым продолжают взаимодействовать). Обычно используется вместе с `showBackdrop={false}`. | | `media` | `ReactNode \| PopupMediaProps` | — | no | Media-блок над шапкой: изображение / иконка либо произвольный `ReactNode`. | | `onBackButtonClick` | `(() => void)` | — | no | Callback клика на back-кнопку (слева в шапке). Наличие callback'а рендерит ArrowLeft-кнопку. | | `onClose` | `() => void` | — | yes | Колбэк закрытия (вызывается при click outside, Esc, swipe-down, browser-back). | | `onSnapIndexChange` | `((snapIndex: number) => void)` | — | no | Callback изменения активного snap'а (пересечение swipe-границы или click по UI). Не вызывается при программной смене controlled `snapIndex`. | | `open` | `boolean` | — | yes | Управление состоянием показан / не показан. | | `rootClassName` | `string` | — | no | CSS-класс корневого элемента portal'а. | | `safeArea` | `boolean` | `true` | no | Резервировать ли место под iOS notch / home-indicator и Android nav-bar. Реализовано паддингом на `.content` через `env(safe-area-inset-*)`: на устройстве без выреза/индикатора (и на desktop) inset = 0, поэтому никакого «лишнего» отступа не появляется; на notched-устройстве — ровно нужный. Верхний отступ добавляется только когда sheet раскрыт на полный вьюпорт (его верх под notch). | | `showBackdrop` | `boolean` | `true` | no | Отображение тёмной подложки за sheet'ом. При `false` фон не затемняется и click-outside не закрывает sheet (нет backdrop-узла, по которому ловится клик). | | `slotAfterTitle` | `ReactNode` | — | no | Slot справа от title (внутри той же строки) — типично `QuestionTooltip`, status badge. | | `slotSecondTitle` | `ReactNode` | — | no | Slot под подзаголовком — типично `SearchBar`, `SegmentControl`, `Filter`. | | `snapIndex` | `number` | — | no | Controlled-индекс активного snap'а. Если задан, sheet всегда находится на этом snap'е; swipe-up/down вызывают `onSnapIndexChange`, но не меняют позицию сами — consumer должен передать новое значение. | | `snapPoints` | `SnapPoint[]` | — | no | Массив фиксированных позиций sheet'а от меньшей к большей. По дефолту `undefined` — sheet `height: auto` с одним snap'ом по высоте контента. Пример: `[0.5, 1]` — sheet открывается на половину экрана, drag вверх раскрывает до full-viewport; drag вниз ниже `0.5` ведёт к закрытию. Контракт массива (движок не сортирует и не дедуплицирует — порядок и различимость на стороне потребителя): - строго по возрастанию: индекс `0` — самая компактная позиция, последний — top / expanded; - значения должны резолвиться в различные высоты (`['50%', 0.5]` на типичном вьюпорте дадут одну высоту → дубль-индекс будет недостижим свайпом); - `'fit-content'` имеет смысл только как ЕДИНСТВЕННЫЙ snap (без `snapPoints`); внутри массива фиксированных позиций его «контентная» высота не определена. | | `subtitle` | `ReactNode` | — | no | Текстовая строка-подзаголовок под title. | | `swipeEnabled` | `boolean` | `true` | no | Включает swipe-down для закрытия / swipe-up для раскрытия на следующий snap-point. При `swipeEnabled=false` snap-point по-прежнему можно переключить через controlled `snapIndex` prop'ом. | | `title` | `ReactNode` | — | no | Заголовок в шапке. | | `withDividers` | `boolean` | `true` | no | Тонкие линии между topBar↔body и body↔footer: разграничивают закреплённые шапку и подвал от прокручиваемого под ними содержимого. Передайте `false`, чтобы убрать обе линии. | #### Related types - `BottomSheetActionButton` (alias) - `FooterActionsOrientation` = `horizontal | vertical` - `MediaKind` = `icon | image` - `PopupMediaProps` (interface) - `SnapPoint` (alias) ### BottomSheetCustom | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `children` | `string \| number \| boolean \| ReactElement> \| Iterable \| ReactPortal \| null \| undefined` | — | no | | | `className` | `string` | — | no | CSS-класс самого sheet-контейнера. | | `closeOnPopstate` | `boolean` | `true` | no | Закрывать sheet при `popstate` (browser-back на mobile). | | `container` | `string \| HTMLElement` | — | no | Контейнер для портала. По дефолту — `body` либо контекст-провайдер `@cloud-ru/ds-portal-context`. | | `data-test-id` | `string` | — | no | | | `defaultSnapIndex` | `number` | `0` | no | Индекс snap'а, на котором sheet открывается по дефолту. Игнорируется при controlled `snapIndex`. | | `disableMotions` | `boolean` | `false` | no | Отключить анимации открытия / закрытия и перехода между snap-точками. | | `lockScroll` | `boolean` | `true` | no | Блокировать ли скролл фона на время открытия (`react-remove-scroll`). При `false` страница под sheet'ом остаётся прокручиваемой — для non-modal сценариев (sheet поверх контента, с которым продолжают взаимодействовать). Обычно используется вместе с `showBackdrop={false}`. | | `onClose` | `() => void` | — | yes | Колбэк закрытия (вызывается при click outside, Esc, swipe-down, browser-back). | | `onSnapIndexChange` | `((snapIndex: number) => void)` | — | no | Callback изменения активного snap'а (пересечение swipe-границы или click по UI). Не вызывается при программной смене controlled `snapIndex`. | | `open` | `boolean` | — | yes | Управление состоянием показан / не показан. | | `rootClassName` | `string` | — | no | CSS-класс корневого элемента portal'а. | | `safeArea` | `boolean` | `true` | no | Резервировать ли место под iOS notch / home-indicator и Android nav-bar. Реализовано паддингом на `.content` через `env(safe-area-inset-*)`: на устройстве без выреза/индикатора (и на desktop) inset = 0, поэтому никакого «лишнего» отступа не появляется; на notched-устройстве — ровно нужный. Верхний отступ добавляется только когда sheet раскрыт на полный вьюпорт (его верх под notch). | | `showBackdrop` | `boolean` | `true` | no | Отображение тёмной подложки за sheet'ом. При `false` фон не затемняется и click-outside не закрывает sheet (нет backdrop-узла, по которому ловится клик). | | `snapIndex` | `number` | — | no | Controlled-индекс активного snap'а. Если задан, sheet всегда находится на этом snap'е; swipe-up/down вызывают `onSnapIndexChange`, но не меняют позицию сами — consumer должен передать новое значение. | | `snapPoints` | `SnapPoint[]` | — | no | Массив фиксированных позиций sheet'а от меньшей к большей. По дефолту `undefined` — sheet `height: auto` с одним snap'ом по высоте контента. Пример: `[0.5, 1]` — sheet открывается на половину экрана, drag вверх раскрывает до full-viewport; drag вниз ниже `0.5` ведёт к закрытию. Контракт массива (движок не сортирует и не дедуплицирует — порядок и различимость на стороне потребителя): - строго по возрастанию: индекс `0` — самая компактная позиция, последний — top / expanded; - значения должны резолвиться в различные высоты (`['50%', 0.5]` на типичном вьюпорте дадут одну высоту → дубль-индекс будет недостижим свайпом); - `'fit-content'` имеет смысл только как ЕДИНСТВЕННЫЙ snap (без `snapPoints`); внутри массива фиксированных позиций его «контентная» высота не определена. | | `swipeEnabled` | `boolean` | `true` | no | Включает swipe-down для закрытия / swipe-up для раскрытия на следующий snap-point. При `swipeEnabled=false` snap-point по-прежнему можно переключить через controlled `snapIndex` prop'ом. | #### Related types - `SnapPoint` (alias) ### Handle _Нет публичных пропсов._ ## Примеры ### Basic ```tsx import { BottomSheet } from '@cloud-ru/ds-bottom-sheet'; import { Button } from '@cloud-ru/ds-button'; import { useState } from 'react'; import { MobilePreview } from '../MobilePreview'; export function Basic() { const [open, setOpen] = useState(false); return ( ))} } >