---
url: https://nsh.dev/ru/elements/scrollbar
description: "Компонент Scrollbar: полоса прокрутки собственного вида для каждого scroll view и списка (тема, заданная один раз) или для одного из них, размещаемая там, где нужно приложению; ползунок следует за содержимым, а перетаскивание и нажатия прокручивают его; полоса отображается во время прокрутки, при наведении или всегда, плавно появляясь и исчезая с таймингом, заданным приложением."
demo: https://nsh.dev/play/index.html#el-scrollbar
---

# Scrollbar

Scrollbar даёт области прокрутки или списку полосу в собственном стиле приложения: тонкую «таблетку», которая расширяется под указателем, дорожку фирменного тона, полосу рядом с фиксированным заголовком или вдоль двух панелей. Если задать её один раз как тему приложения, каждая область прокрутки и каждый список рисуют такую полосу вместо индикатора платформы ([Встроенные полосы](#built-in-bars)). Если приложение размещает полосу само, она указывает, какой областью прокрутки управляет, и находится там, где её поставит приложение. Полоса сама отвечает за своё поведение: длина и положение ползунка следуют за содержимым, областью просмотра и смещением (в том числе пока список растёт или окно меняет размер). Перетаскивание ползунка прокручивает содержимое, а нажатие на дорожку переносит середину ползунка в это место. Полоса показывается, пока содержимое прокручивается, пока над ней находится указатель или всегда, а плавное появление и исчезновение выполняются нативно. Внешний вид и тайминги задаёт приложение: у `Scrollbar` нет собственных цветов, размеров и радиусов, а её затухания не занимают времени, пока приложение его не задаст.

```rust
use nsh_base::{ScrollbarEntrance, ScrollbarMode, ScrollbarMotion, scrollbar, theme};
use nsh_core::prelude::*;

fn inbox(rows: impl IntoElement) -> impl IntoElement {
    let list = element_ref();
    // In over 300 ms and out over 500 ms, 2 s after the last scroll; pointing
    // at the hidden thumb slides it in from the edge.
    let mut motion = ScrollbarMotion::default();
    motion.enter = 300.0;
    motion.exit = 500.0;
    motion.thumb_hover_entrance = ScrollbarEntrance::SlideAndFade;
    view()
        .relative()
        .flex_1()
        // The scroll view's own indicator gives way to the app's bar.
        .child(scroll().element_ref(&list).flex_1().scrollbar(false).child(rows))
        .child(
            scrollbar()
                .scroll(list)
                .mode(ScrollbarMode::Hover)
                .motion(motion)
                .accessibility_label("Messages")
                // The track over the right edge; the thumb right-aligned in it.
                .absolute()
                .top_0()
                .bottom_0()
                .right_0()
                .w(px(16.0))
                .items_end()
                .p(px(4.0))
                .thumb_style(|s| {
                    s.w(px(6.0))
                        .rounded_full()
                        .bg(move || theme().muted_foreground)
                        .opacity(0.6)
                        .group_hover(|s| s.w(px(8.0)).opacity(0.9))
                }),
        )
}
```

```tsx
import { mount, Scroll, Text, View, type YNode } from "nsh";
import { Scrollbar } from "nsh/base";

function App() {
  let list: YNode | undefined;
  return (
    <View class="relative flex-1">
      <Scroll ref={list} scrollbar={false} class="flex-1">
        {Array.from({ length: 60 }, (_, i) => (
          <Text class="px-4 py-3">{`Message ${i + 1}`}</Text>
        ))}
      </Scroll>
      <Scrollbar
        scroll={list}
        mode="hover"
        motion={{ enter: 300, exit: 500, thumbHoverEntrance: "slideAndFade" }}
        accessibilityLabel="Messages"
        class="absolute inset-y-0 right-0 w-4 items-end p-1"
        thumbClass="w-1.5 rounded-full bg-muted-foreground opacity-60 transition-all duration-300 hover:w-2 hover:opacity-90 active:w-2 active:opacity-100"
      />
    </View>
  );
}

mount(App);
```

`Scrollbar` поставляется из [`nsh/base`](https://nsh.dev/translations/ru/modules/nsh-base.md), крейта `nsh-base` для Rust. В Rust `scrollbar()` создаёт полосу с сеттером для каждого свойства (`.scroll(..)`, `.thumb_style(..)`) и типизированными методами стиля для самой полосы; её `scroll` принимает `ElementRef` (или `ScrollbarTarget`).

<a id="built-in-bars"></a>

## Встроенные полосы

Каждая область прокрутки и каждый список могут рисовать `Scrollbar` вместо индикатора платформы, и размещать полосу не нужно: `Scrollbar.setTheme` один раз, там, где запускается приложение, задаёт внешний вид полосы каждого представления, у которого `scrollbar` равно `true`. Каждое представление получает собственную полосу поверх своей области просмотра; она остаётся на месте, пока содержимое прокручивается (в основном потоке, как и закреплённые представления), и рисуется над содержимым. Она следует за содержимым, прокручивается и затухает так же, как размещённая `Scrollbar`.

```rust
use nsh_base::{ScrollbarLook, ScrollbarMode, ScrollbarTheme, set_scrollbar_theme};

fn use_app_scrollbars() {
    let mut vertical = ScrollbarLook::default();
    vertical.class = Some("absolute inset-y-0 right-0 w-4 items-end p-1".into());
    vertical.thumb_class = Some("w-1.5 rounded-full bg-muted-foreground opacity-60 hover:w-2".into());
    let mut horizontal = ScrollbarLook::default();
    horizontal.class = Some("absolute inset-x-0 bottom-0 h-4 items-end p-1".into());
    horizontal.thumb_class = Some("h-1.5 rounded-full bg-muted-foreground opacity-60 hover:h-2".into());
    let mut theme = ScrollbarTheme::default();
    theme.mode = ScrollbarMode::Hover;
    theme.motion.enter = 300.0;
    theme.motion.exit = 500.0;
    theme.vertical = Some(vertical);
    theme.horizontal = Some(horizontal);
    set_scrollbar_theme(Some(theme));
}
```

```tsx
import { Scrollbar } from "nsh/base";

Scrollbar.setTheme({
  mode: "hover",
  motion: { enter: 300, exit: 500 },
  vertical: {
    class: "absolute inset-y-0 right-0 w-4 items-end p-1",
    thumbClass: "w-1.5 rounded-full bg-muted-foreground opacity-60 transition-all duration-300 hover:w-2",
  },
  horizontal: {
    class: "absolute inset-x-0 bottom-0 h-4 items-end p-1",
    thumbClass: "h-1.5 rounded-full bg-muted-foreground opacity-60 transition-all duration-300 hover:h-2",
  },
});
```

`ScrollbarTheme` содержит свойства полосы, одинаковые для всех представлений, и внешний вид каждой оси:

| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
| `mode` | `"scrolling" \| "hover" \| "always"` | `"scrolling"` | Когда полосы показываются: [Когда она показывается](#when-it-shows). |
| `motion` | `ScrollbarMotion` | мгновенно, `idle` `2000` | Как они появляются и скрываются: [Анимация](#motion). |
| `minThumbLength` | `number` | `48` | Минимальная длина ползунка. |
| `vertical`, `horizontal` | `ScrollbarLook` | нет | Внешний вид полос вертикальных и горизонтальных представлений: `class` и `style` для дорожки, размещённой в области просмотра представления; `thumbClass` и `thumbStyle`; `accessibilityLabel`. Представление, у оси которого нет внешнего вида, сохраняет индикатор платформы. |

Более поздний `setTheme` меняет стиль полос, а `Scrollbar.setTheme(null)` возвращает представлениям индикатор платформы. Одно представление может получить собственную тему через свойство `scrollbar` (`<Scroll scrollbar={{ mode: "always", vertical: { … } }}>`), независимо от того, задало ли приложение общую тему; `scrollbar={false}` оставляет представление без полосы. Список, размещённый в области просмотра родителя (`viewport="parent"`), прокручивается вместе с этим представлением и его полосой. Многострочный `Input` тоже получает полосу своего вертикального вида поверх текста, который она прокручивает так же, как собственный индикатор текстового представления.

<a id="the-scroll-view-it-moves"></a>

## Прокручиваемое представление, которым она управляет

`scroll` указывает область прокрутки или список, за которыми следует полоса: её `ref`. Полоса находит его, когда оно оказывается на экране, поэтому `ref` может быть задан элементом, расположенным позже в том же рендере; пишите полосу после её области прокрутки (или храните `ref` в сигнале). Отключите собственный индикатор области прокрутки через `scrollbar={false}` и разместите полосу через `class`: поверх края области прокрутки в блоке `relative`, охватывающем обе, или в любом другом месте экрана.

- Длина ползунка соответствует доле содержимого, которую показывает область просмотра, вдоль контентного блока дорожки (внутри её отступов), и не меньше `minThumbLength`. Его положение показывает, насколько прокручено содержимое; пока содержимое отскакивает за пределы края, ползунок остаётся у этого края.
- Перетаскивание ползунка прокручивает содержимое на столько же, на сколько сдвигается ползунок. Нажатие на дорожку переносит середину ползунка под указатель, и перетаскивание продолжается оттуда.
- Вертикальная полоса следует за вертикальной областью прокрутки; `axis="horizontal"` создаёт полосу для горизонтальной (разместите её вдоль нижнего края). Одна полоса на ось.
- Списки работают так же: ползунок следует за полной длиной списка по мере измерения и добавления строк.

<a id="when-it-shows"></a>

## Когда она показывается

| `mode` | Показывается |
|---|---|
| `"scrolling"` (по умолчанию) | Пока содержимое прокручивается, и до `motion.idle` после остановки; указатель на полосе, пока она показана, удерживает её. |
| `"hover"` | Пока указатель находится над полосой (в том месте, где она показывается, даже когда скрыта), а также как в режиме `"scrolling"`. |
| `"always"` | Всегда. |

В любом режиме полоса показывается, пока ползунок перетаскивается, и остаётся `motion.idle` после последней прокрутки, перетаскивания или наведения. Пока содержимое помещается в области просмотра, полоса остаётся скрытой. В скрытом состоянии у полосы `opacity: 0`, и она пропускает нажатия к тому, что находится под ней.

<a id="motion"></a>

## Анимация

`motion` задаёт время затуханий полосы, которые выполняются нативно, как `ScrollbarMotion` у `gpui-base`:

| Поле | По умолчанию | Описание |
|---|---|---|
| `idle` | `2000` | Сколько полоса остаётся после последней прокрутки, перетаскивания или наведения, в миллисекундах. |
| `enter` | `0` | Сколько времени занимает появление: непрозрачность растёт линейно, сдвиг замедляется к концу. |
| `exit` | `0` | Сколько времени занимает затухание после окончания `idle`, с ускорением к концу; при затухании полоса выезжает за пределы своей дорожки. |
| `entrance` | `"fade"` | Как она появляется: `"fade"` на месте или `"slideAndFade"` от края, у которого находится (справа, снизу). |
| `thumbHoverEntrance` | `"fade"` | Как она появляется, когда указатель на скрытом ползунке показывает её (`"hover"`). |

Без длительностей полоса появляется и исчезает мгновенно. Затухание, развёрнутое на полпути, сохраняет скорость: оно длится столько, сколько составляет оставшаяся доля его длительности. Полоса `"always"` никогда не анимируется, как и любая полоса, пока пользователь просит уменьшить движение. Дорожка обрезает то, что сдвигается. Собственная анимация ползунка, например ширина, которая растёт при `hover:`, относится к его стилю (`transition-all` у `thumbClass`).

<a id="props"></a>

## Свойства

| Свойство | Rust | Тип | По умолчанию | Описание |
|---|---|---|---|---|
| `scroll` | `.scroll(..)` | `YNode` | нет | Область прокрутки или список, за которыми следует полоса и которыми она управляет (их `ref`). |
| `axis` | `.axis(..)` | `"horizontal" \| "vertical"` | `"vertical"` | Ось области прокрутки: дорожка идёт вдоль неё. Читается при создании полосы. |
| `mode` | `.mode(..)` | `"scrolling" \| "hover" \| "always"` | `"scrolling"` | Когда полоса показывается. |
| `motion` | `.motion(..)` | `ScrollbarMotion` | мгновенно, `idle` `2000` | Как она появляется и скрывается: [Анимация](#motion). |
| `minThumbLength` | `.min_thumb_length(..)` | `number` | `48` | Минимальная длина ползунка при длинном содержимом, чтобы его было удобно захватить. |
| `thumbClass`, `thumbStyle` | `.thumb_style(\|s\| ..)` | `string`, `StyleProp<ViewStyle>` | нет | Ползунок. |
| `accessibilityLabel` | `.accessibility_label(..)` | `string` | нет | Что программы чтения с экрана произносят о полосе. |
| `class`, `style` | методы стиля | `string`, `StyleProp<ViewStyle>` | нет | Полоса: её дорожка, там, где её размещает приложение. |

<a id="parts"></a>

## Части

| Часть | Находится внутри | Стилизуется с помощью |
|---|---|---|
| Дорожка | там, где её размещает приложение | `class`, `style` |
| Ползунок | контентного блока дорожки | `thumbClass`, `thumbStyle` |

Полоса задаёт размер ползунка вдоль оси и перемещает его; его толщина и положение поперёк дорожки определяются приложением (`w-1.5` и `items-end` у дорожки помещают 6-пунктовый ползунок справа). Отступы дорожки не дают ползунку касаться её концов и сторон.

Состояния, которые показывают части:

- Дорожка: `hover:` под указателем; она является `group`, поэтому ползунок получает от неё `group-hover:` (ползунок, который расширяется, когда указатель приближается).
- Ползунок: `hover:` под указателем, `active:` пока его перетаскивают.

Видимость полосы определяется её непрозрачностью: задавайте прозрачность в цветах фона дорожки или ползунка. Ползунок, который расширяется, использует `transition-all` (она никогда не задерживает положение ползунка, которое полоса задаёт отдельно от его стилей), а `motion-reduce:` оставляет его неподвижным для людей, которые просят уменьшить движение.

<a id="accessibility"></a>

## Доступность

- Область прокрутки остаётся тем, что прокручивают программы чтения с экрана: они двигают её собственными жестами и читают её содержимое.
- Полоса получает метку из `accessibilityLabel` для программ чтения с экрана, которые до неё добираются.

Общие сведения о метках, ролях и действиях см. в разделе [Доступность](https://nsh.dev/translations/ru/docs/accessibility.md).

<a id="platform-notes"></a>

## Заметки по платформам

- Каждая платформа рисует то, что задано стилями приложения, а ползунок следует за смещением, которое платформа сообщает для области прокрутки. Перетаскивание ползунка и нажатия на дорожку работают с мышью, пером или пальцем.
- `"hover"` следует за указателем мыши или тачпада (на десктопах и в вебе); на сенсорном экране полоса показывается, пока содержимое прокручивается, а её скрытая дорожка пропускает касания.
