# @cloud-ru/ds-drawer > Пакет выезжающих панелей — компоненты Drawer и DrawerCustom с едиными токенами позиции и ширины. Docs: /snack-v2/components/drawer/ ## Установка ```sh pnpm add @cloud-ru/ds-drawer ``` ## API ### DesktopDrawer | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `additionalButton` | `BottomSheetActionButton` | — | no | Дополнительная (третья) кнопка — пропсы `Button` (дефолт `view='simple'`, `appearance='neutral'`). | | `approveButton` | `BottomSheetActionButton` | — | no | Основная кнопка действия — пропсы `Button` (дефолт `view='filled'`, `appearance='primary'`). | | `cancelButton` | `BottomSheetActionButton` | — | no | Кнопка отмены — объект пропсов `Button` (по умолчанию `view='outline'`, `appearance='neutral'`). | | `className` | `string` | — | no | CSS-класс для элемента с контентом CSS-класс | | `closeOnPopstate` | `boolean` | — | no | Закрывать дровер при перемещении по истории браузера | | `container` | `string \| HTMLElement` | — | no | Контейнер в котором будет рендерится Drawer. По-умолчанию - body | | `content` | `ReactNode` | — | no | Содержимое body (альтернатива `children`). | | `data-test-id` | `string` | — | no | | | `disableMotions` | `boolean` | `false` | no | Отключить анимации | | `footer` | `(ReactElement> & (string \| number \| boolean \| ReactElement> \| Iterable<...> \| ReactPortal \| null))` | — | no | Футер Произвольный футер. Приоритетнее `approveButton` / `cancelButton` / `additionalButton`. | | `footerActionsOrientation` | `horizontal \| vertical` | `'horizontal'` | no | Ориентация кнопок футера. Применяется только при двух кнопках; игнорируется при заданном `footer`. | | `heightAuto` | `boolean` | `false` | no | Высота панели по контенту (только при `position: "top" \| "bottom"`). | | `media` | `ReactNode` | — | no | Медиа-контент | | `nestedDrawer` | `ReactElement>` | — | no | Вложенный Drawer | | `onBackButtonClick` | `(() => void)` | — | no | Callback клика на back-кнопку (слева в шапке). Наличие callback'а авто-рендерит `Button view='function' icon={}`. | | `onClose` | `() => void` | — | yes | Колбэк закрытия | | `onSnapIndexChange` | `((snapIndex: number) => void)` | — | no | Callback изменения активного snap'а (пересечение swipe-границы или click по UI). Не вызывается при программной смене controlled `snapIndex`. | | `open` | `boolean` | — | yes | Управление состоянием показан/не показан. | | `position` | `bottom \| left \| right \| top` | — | yes | Расположение | | `rootClassName` | `string` | — | no | CSS-класс для корневого элемента | | `safeArea` | `boolean` | `true` | no | Резервировать ли место под iOS notch / home-indicator и Android nav-bar. Реализовано паддингом на `.content` через `env(safe-area-inset-*)`: на устройстве без выреза/индикатора (и на desktop) inset = 0, поэтому никакого «лишнего» отступа не появляется; на notched-устройстве — ровно нужный. Верхний отступ добавляется только когда sheet раскрыт на полный вьюпорт (его верх под notch). | | `showBlackout` | `boolean` | `true` | no | Отображение темной подложки | | `showButtonClosed` | `boolean` | `true` | no | Отображение кнопки закрытия в шапке дровера | | `slotAfterTitle` | `ReactNode` | — | no | Slot справа от title (например, `QuestionTooltip` из `@cloud-ru/ds-tooltip`). | | `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 | Заголовок. Типографика зависит от поверхности: `title-l` на sheet, `headline-s` на window (modal/drawer). | | `width` | `string \| number` | `'s'` | no | Ширина (только при position: "left" \| "right") | ### DialogBody | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `bodyPadding` | `boolean` | `true` | no | Горизонтальные паддинги body. При `false` контент идёт во всю ширину sheet'а (edge-to-edge) — для карт, изображений, списков без отступов. Соответствует Figma-оси `padding=false`. | | `className` | `string` | — | no | CSS-класс контейнера body. | | `content` | `ReactNode` | — | no | Содержимое body (альтернатива `children`). | | `data-test-id` | `string` | — | no | | ### DialogFooter | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `className` | `string` | — | no | CSS-класс контейнера footer'а. | | `data-test-id` | `string` | — | no | | ### DialogHeader | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `actionButton` | `ReactNode` | — | no | Slot справа от headline-строки (любой `ReactNode`, обычно `Button` с иконкой). | | `className` | `string` | — | no | CSS-класс контейнера header'а. | | `data-test-id` | `string` | — | no | | | `onBackButtonClick` | `(() => void)` | — | no | Callback клика на back-кнопку (слева в шапке). Наличие callback'а авто-рендерит `Button view='function' icon={}`. | | `slotAfterTitle` | `ReactNode` | — | no | Slot справа от title (например, `QuestionTooltip` из `@cloud-ru/ds-tooltip`). | | `slotSecondTitle` | `ReactNode` | — | no | Slot под подзаголовком (Figma `secondWrapper`) — типично `SearchBar`, `SegmentControl` или `Filter`. Есть только в мастере `bottomSheet`, поэтому рендерится **только** на sheet-поверхности. | | `subtitle` | `ReactNode` | — | no | Текстовая строка-подзаголовок под title (Figma `subtitleWrapper`). Рендерится на всех поверхностях. | | `testIds` | `PopupHeaderTestIds` | — | no | Переопределение `data-test-id` слотов шапки. Каждый пропущенный ключ берётся из `TEST_IDS`. Потребитель-обёртка (drawer/modal) прокидывает сюда свои id, чтобы сохранить публичный контракт. | | `title` | `ReactNode` | — | no | Заголовок. Типографика зависит от поверхности: `title-l` на sheet, `headline-s` на window (modal/drawer). | | `titleId` | `string` | — | no | `id` заголовка — для связи с `aria-labelledby` dialog'а (accessible name). | | `truncate` | `{ title?: number; subtitle?: number; } \| undefined` | — | no | Усечение строковых `title`/`subtitle` через `TruncateString` (число строк). Применяется только когда задано — по умолчанию текст не усекается. Актуально для window-поверхности (modal/drawer), где длинный заголовок иначе переносится на несколько строк. | ### Drawer | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `additionalButton` | `BottomSheetActionButton` | — | no | Дополнительная (третья) кнопка — пропсы `Button` (дефолт `view='simple'`, `appearance='neutral'`). | | `approveButton` | `BottomSheetActionButton` | — | no | Основная кнопка действия — пропсы `Button` (дефолт `view='filled'`, `appearance='primary'`). | | `cancelButton` | `BottomSheetActionButton` | — | no | Кнопка отмены — объект пропсов `Button` (по умолчанию `view='outline'`, `appearance='neutral'`). | | `children` | `string \| number \| boolean \| ReactElement> \| Iterable \| ReactPortal \| null \| undefined` | — | no | | | `className` | `string` | — | no | CSS-класс для элемента с контентом CSS-класс | | `closeOnPopstate` | `boolean` | — | no | Закрывать дровер при перемещении по истории браузера | | `container` | `string \| HTMLElement` | — | no | Контейнер в котором будет рендерится Drawer. По-умолчанию - body | | `content` | `ReactNode` | — | no | Содержимое body (альтернатива `children`). | | `data-test-id` | `string` | — | no | | | `disableMotions` | `boolean` | `false` | no | Отключить анимации | | `footer` | `(ReactElement> & (string \| number \| boolean \| ReactElement> \| Iterable<...> \| ReactPortal \| null))` | — | no | Футер Произвольный футер. Приоритетнее `approveButton` / `cancelButton` / `additionalButton`. | | `footerActionsOrientation` | `horizontal \| vertical` | `'horizontal'` | no | Ориентация кнопок футера. Применяется только при двух кнопках; игнорируется при заданном `footer`. | | `heightAuto` | `boolean` | `false` | no | Высота панели по контенту (только при `position: "top" \| "bottom"`). | | `media` | `ReactNode` | — | no | Медиа-контент | | `nestedDrawer` | `ReactElement>` | — | no | Вложенный Drawer | | `onBackButtonClick` | `(() => void)` | — | no | Callback клика на back-кнопку (слева в шапке). Наличие callback'а авто-рендерит `Button view='function' icon={}`. | | `onClose` | `() => void` | — | yes | Колбэк закрытия | | `onSnapIndexChange` | `((snapIndex: number) => void)` | — | no | Callback изменения активного snap'а (пересечение swipe-границы или click по UI). Не вызывается при программной смене controlled `snapIndex`. | | `open` | `boolean` | — | yes | Управление состоянием показан/не показан. | | `position` | `bottom \| left \| right \| top` | — | yes | Расположение | | `rootClassName` | `string` | — | no | CSS-класс для корневого элемента | | `safeArea` | `boolean` | `true` | no | Резервировать ли место под iOS notch / home-indicator и Android nav-bar. Реализовано паддингом на `.content` через `env(safe-area-inset-*)`: на устройстве без выреза/индикатора (и на desktop) inset = 0, поэтому никакого «лишнего» отступа не появляется; на notched-устройстве — ровно нужный. Верхний отступ добавляется только когда sheet раскрыт на полный вьюпорт (его верх под notch). | | `showBlackout` | `boolean` | `true` | no | Отображение темной подложки | | `showButtonClosed` | `boolean` | `true` | no | Отображение кнопки закрытия в шапке дровера | | `slotAfterTitle` | `ReactNode` | — | no | Slot справа от title (например, `QuestionTooltip` из `@cloud-ru/ds-tooltip`). | | `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 | Заголовок. Типографика зависит от поверхности: `title-l` на sheet, `headline-s` на window (modal/drawer). | | `width` | `string \| number` | `'s'` | no | Ширина (только при position: "left" \| "right") | #### Related types - `BottomSheetActionButton` (alias) - `DrawerProps` (interface) - `FooterActionsOrientation` = `horizontal | vertical` - `Position` = `bottom | left | right | top` - `SnapPoint` (alias) - `Width` = `l | m | s` ### DrawerBody | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `bodyPadding` | `boolean` | `true` | no | Горизонтальные паддинги body. При `false` контент идёт во всю ширину sheet'а (edge-to-edge) — для карт, изображений, списков без отступов. Соответствует Figma-оси `padding=false`. | | `className` | `string` | — | no | CSS-класс контейнера body. | | `content` | `ReactNode` | — | no | Содержимое body (альтернатива `children`). | | `data-test-id` | `string` | — | no | | ### DrawerCustom | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `children` | `string \| number \| boolean \| ReactElement> \| Iterable \| ReactPortal \| null \| undefined` | — | no | | | `className` | `string` | — | no | CSS-класс для элемента с контентом | | `closeOnPopstate` | `boolean` | — | no | Закрывать дровер при перемещении по истории браузера | | `container` | `string \| HTMLElement` | — | no | Контейнер в котором будет рендерится Drawer. По-умолчанию - body | | `data-test-id` | `string` | — | no | | | `disableMotions` | `boolean` | `false` | no | Отключить анимации | | `footer` | `ReactElement>` | — | no | Футер | | `heightAuto` | `boolean` | `false` | no | Высота панели по контенту (только при `position: "top" \| "bottom"`). | | `nestedDrawer` | `ReactElement, string \| JSXElementConstructor>` | — | no | Вложенный Drawer | | `onClose` | `() => void` | — | yes | Колбэк закрытия | | `open` | `boolean` | — | yes | Управление состоянием показан/не показан. | | `position` | `bottom \| left \| right \| top` | — | yes | Расположение | | `push` | `boolean \| PushConfig` | — | no | Смещение при открытии "вложенного" компонента | | `resizable` | `{ min: number; max?: number; default?: number; onResize?: ((width: number) => void) \| undefined; onResizeEnd?: ((width: number) => void) \| undefined; draggerTooltip?: string \| undefined; } \| undefined` | `'s'` | no | Ширина (только при position: "left" \| "right") | | `rootClassName` | `string` | — | no | CSS-класс для корневого элемента | | `safeArea` | `boolean` | `true` | no | Резервировать ли место под iOS notch / home-indicator и Android nav-bar. Реализовано паддингом на `.content` через `env(safe-area-inset-*)`: на устройстве без выреза/индикатора (и на desktop) inset = 0, поэтому никакого «лишнего» отступа не появляется; на notched-устройстве — ровно нужный. Верхний отступ добавляется только когда sheet раскрыт на полный вьюпорт (его верх под notch). | | `showBlackout` | `boolean` | `true` | no | Отображение темной подложки | | `showButtonClosed` | `boolean` | `true` | no | Отображение кнопки закрытия в шапке дровера | | `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'ом. | | `width` | `string \| number` | `'s'` | no | Ширина (только при position: "left" \| "right") | #### Related types - `DrawerCustomProps` (interface) - `Position` = `bottom | left | right | top` - `SnapPoint` (alias) - `Width` = `l | m | s` ### MobileDrawer | Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | `additionalButton` | `BottomSheetActionButton` | — | no | Дополнительная (третья) кнопка — пропсы `Button` (дефолт `view='simple'`, `appearance='neutral'`). | | `approveButton` | `BottomSheetActionButton` | — | no | Основная кнопка действия — пропсы `Button` (дефолт `view='filled'`, `appearance='primary'`). | | `cancelButton` | `BottomSheetActionButton` | — | no | Кнопка отмены — объект пропсов `Button` (по умолчанию `view='outline'`, `appearance='neutral'`). | | `className` | `string` | — | no | CSS-класс для элемента с контентом CSS-класс | | `closeOnPopstate` | `boolean` | — | no | Закрывать дровер при перемещении по истории браузера | | `container` | `string \| HTMLElement` | — | no | Контейнер в котором будет рендерится Drawer. По-умолчанию - body | | `content` | `ReactNode` | — | no | Содержимое body (альтернатива `children`). | | `data-test-id` | `string` | — | no | | | `disableMotions` | `boolean` | `false` | no | Отключить анимации | | `footer` | `(ReactElement> & (string \| number \| boolean \| ReactElement> \| Iterable<...> \| ReactPortal \| null))` | — | no | Футер Произвольный футер. Приоритетнее `approveButton` / `cancelButton` / `additionalButton`. | | `footerActionsOrientation` | `horizontal \| vertical` | `'horizontal'` | no | Ориентация кнопок футера. Применяется только при двух кнопках; игнорируется при заданном `footer`. | | `heightAuto` | `boolean` | `false` | no | Высота панели по контенту (только при `position: "top" \| "bottom"`). | | `media` | `ReactNode` | — | no | Медиа-контент | | `nestedDrawer` | `ReactElement>` | — | no | Вложенный Drawer | | `onBackButtonClick` | `(() => void)` | — | no | Callback клика на back-кнопку (слева в шапке). Наличие callback'а авто-рендерит `Button view='function' icon={}`. | | `onClose` | `() => void` | — | yes | Колбэк закрытия | | `onSnapIndexChange` | `((snapIndex: number) => void)` | — | no | Callback изменения активного snap'а (пересечение swipe-границы или click по UI). Не вызывается при программной смене controlled `snapIndex`. | | `open` | `boolean` | — | yes | Управление состоянием показан/не показан. | | `position` | `bottom \| left \| right \| top` | — | yes | Расположение | | `rootClassName` | `string` | — | no | CSS-класс для корневого элемента | | `safeArea` | `boolean` | `true` | no | Резервировать ли место под iOS notch / home-indicator и Android nav-bar. Реализовано паддингом на `.content` через `env(safe-area-inset-*)`: на устройстве без выреза/индикатора (и на desktop) inset = 0, поэтому никакого «лишнего» отступа не появляется; на notched-устройстве — ровно нужный. Верхний отступ добавляется только когда sheet раскрыт на полный вьюпорт (его верх под notch). | | `showBlackout` | `boolean` | `true` | no | Отображение темной подложки | | `showButtonClosed` | `boolean` | `true` | no | Отображение кнопки закрытия в шапке дровера | | `slotAfterTitle` | `ReactNode` | — | no | Slot справа от title (например, `QuestionTooltip` из `@cloud-ru/ds-tooltip`). | | `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 | Заголовок. Типографика зависит от поверхности: `title-l` на sheet, `headline-s` на window (modal/drawer). | | `width` | `string \| number` | `'s'` | no | Ширина (только при position: "left" \| "right") | ## Примеры ### Basic ```tsx import { Button, ButtonGroup } from '@cloud-ru/ds-button'; import { Drawer } from '@cloud-ru/ds-drawer'; import { useState } from 'react'; export function Basic() { const [open, setOpen] = useState(false); const close = () => setOpen(false); return ( <>