# Scrollbars Provider

## Usage

Wrap your app once, and every scrolling Astryx component swaps the browser's native scrollbar for a custom overlay one. It is thin, takes no layout width, hides itself when idle, and follows the light and dark theme.

This is an extra in astryx-vue. The original Astryx does not have it. It is built on [OverlayScrollbars](https://kingsora.github.io/OverlayScrollbars/) (MIT), which ships inside `@astryx-vue/core`, so you install nothing extra.

**It is off by default.** Without a `ScrollbarsProvider`, components behave exactly as before. And without one, neither the OverlayScrollbars JS nor its CSS ends up in your bundle, because only `ScrollbarsProvider` imports them.

```ts
import { ScrollbarsProvider } from '@astryx-vue/core/Scrollbars'
```

Setup takes about 30 seconds. Wrap your app in `App.vue`:

```vue
<script setup lang="ts">
import { ScrollbarsProvider } from '@astryx-vue/core/Scrollbars'
</script>

<template>
  <ScrollbarsProvider>
    <RouterView />
  </ScrollbarsProvider>
</template>
```

The props you will use most:

- `enabled`: the switch, `true` by default. Set it to `false` and the whole subtree goes back to native scrollbars.
- `autoHide`: when the scrollbar hides. `'scroll'` (default) shows it while scrolling, `'leave'` hides it after the pointer leaves, `'move'` shows it while the pointer moves, `'never'` keeps it visible.
- `autoHideDelay`: how many milliseconds to wait before hiding. Default 800.
- `options`: passed straight to OverlayScrollbars and wins over everything else. Examples are below.

## Examples

### Scrollbars Provider — Toggle Comparison

A ScrollableArea with a long list and a CodeBlock that scrolls sideways, both inside one ScrollbarsProvider. Flip the switch to compare the native scrollbars with the custom ones.

```vue
<script setup lang="ts">
import { CodeBlock } from '@astryx-vue/core/Code'
import { ScrollableArea } from '@astryx-vue/core/ScrollableArea'
import { ScrollbarsProvider } from '@astryx-vue/core/Scrollbars'
import { Switch } from '@astryx-vue/core/Switch'
import { Text } from '@astryx-vue/core/Text'
import { shallowRef } from 'vue'

const on = shallowRef(true)

const notes = Array.from({ length: 24 }, (_, index) => `Release note ${index + 1}`)

const code = [
  'const message = "This line is long enough that it has to scroll sideways, so the horizontal scrollbar shows up under the code.";',
  'const another = "Hover the code block, or scroll it, and the thin bar appears without taking any width from the content.";',
  'export { message, another };',
].join('\n')
</script>

<template>
  <div class="scrollbars-provider-preview">
    <Switch v-model="on" label="Custom scrollbars" />
    <ScrollbarsProvider :enabled="on">
      <div class="scrollbars-provider-preview__panes">
        <ScrollableArea label="Release notes" :height="200" :padding="3">
          <div v-for="note in notes" :key="note" class="scrollbars-provider-preview__row">
            <Text type="body">
              {{ note }}
            </Text>
          </div>
        </ScrollableArea>
        <CodeBlock :code="code" language="typescript" title="long-lines.ts" />
      </div>
    </ScrollbarsProvider>
  </div>
</template>
```

### Scrollbars Provider — Your Own Scroll Container

A hand-written overflow: auto div that gets the custom scrollbar through useScrollbars. The look is tuned with CSS variables: a thicker bar in the accent color.

```vue
<script setup lang="ts">
import { ScrollbarsProvider, useScrollbars } from '@astryx-vue/core/Scrollbars'
import { Text } from '@astryx-vue/core/Text'
import { defineComponent, h, shallowRef } from 'vue'

/**
 * `useScrollbars` reads the nearest provider, so the hook lives in a child
 * component that sits inside `ScrollbarsProvider`.
 */
const Feed = defineComponent({
  name: 'DemoFeed',
  setup() {
    const el = shallowRef<HTMLElement | null>(null)
    const { active } = useScrollbars(el)
    const rows = Array.from({ length: 20 }, (_, index) => `Event ${index + 1}`)
    return () => h('div', {
      'ref': el,
      'class': 'scrollbars-provider-preview__feed',
      'data-active': String(active.value),
      'tabindex': 0,
      'role': 'region',
      'aria-label': 'Event feed',
    }, rows.map(row => h('div', { key: row, class: 'scrollbars-provider-preview__row' }, h(Text, { type: 'body' }, () => row))))
  },
})
</script>

<template>
  <ScrollbarsProvider>
    <Feed />
  </ScrollbarsProvider>
</template>
```

## Components that switch automatically

Inside a provider, the scroll areas of these components switch to the custom scrollbar. You do not change any code:

- Scroll containers: `ScrollableArea`, `Layout` (`LayoutContent` and `LayoutPanel` when they scroll), `Card`, `Stack` and `StackItem` (when they scroll)
- Menus and pickers: `DropdownMenu`, `ContextMenu`, `MoreMenu`, `Selector`, `MultiSelector`, `ComplexSelector`, `CommandPalette`
- Suggestion lists: the lists in `Typeahead`, `Tokenizer` and `PowerSearch`, plus `TimeInput` and `DateRangeInput`
- Overlays: `Popover`, `AlertDialog`, `BottomSheet`
- Navigation: `SideNav`, `MobileNav`
- Data display: `Table`, `CodeBlock`
- Conversation: the `Chat` message area and its trigger menu

## What stays native

- The `Chat` input. It is an editable area, and swapping it would get in the way of typing.
- `<textarea>`.
- Horizontal scrolling of tables inside `Markdown`.
- The page's own scrollbar (`body`).

These keep the native scrollbar.

## Your own scroll containers

Reach for [`ScrollableArea`](/en/components/scrollable-area) first. It already comes with an accessible name, keyboard focus and overflow detection, and inside a provider it gets the custom scrollbar for free.

Use `useScrollbars` only when you already have a hand-written scroll container and do not want to move to `ScrollableArea`. Pass it the element's ref:

```vue
<script setup lang="ts">
import { useScrollbars } from '@astryx-vue/core/Scrollbars'
import { shallowRef } from 'vue'

const el = shallowRef<HTMLElement | null>(null)
const { active } = useScrollbars(el)
</script>

<template>
  <div ref="el" class="feed">
    <!-- long content -->
  </div>
</template>

<style scoped>
.feed {
  height: 240px;
  overflow: auto;
}
</style>
```

A few things to know:

- The container itself is the scrolling viewport. Its children are not moved. The hook only appends two extra `div`s at the end of the container for the scrollbars.
- With no `ScrollbarsProvider` above it, `useScrollbars` does nothing and `active` stays `false`.
- The scrollbar follows whatever `overflow` the container has. Making it scrollable is up to you.
- Call `useScrollbars` in a **descendant** of `ScrollbarsProvider`. Calling it in the same component that renders the provider will not find it.

## Turning it off locally

To keep native scrollbars on just one part, pick the scope you need.

**One `ScrollableArea`:**

```vue
<ScrollableArea label="Logs" :height="240" :scrollbars="false">
  ...
</ScrollableArea>
```

**One subtree.** Nest another provider. The inner one wins:

```vue
<ScrollbarsProvider>
  <Main />
  <ScrollbarsProvider :enabled="false">
    <Sidebar />
  </ScrollbarsProvider>
</ScrollbarsProvider>
```

**One of your own containers.** Pass `enabled` to `useScrollbars`. It can be a boolean, a ref or a getter:

```ts
const { active } = useScrollbars(el, { enabled: () => props.fancy })
```

If the outer provider is off, `true` here does not turn it on.

## Changing the look

The look is driven by CSS variables. They all have defaults that follow the theme tokens, so light and dark need no extra work.

| Variable | Default | Description |
| --- | --- | --- |
| `--astryx-scrollbar-size` | `10px` | Scrollbar thickness, including 2px of padding on each side. |
| `--astryx-scrollbar-thumb` | Secondary text color at 45% | Thumb color. |
| `--astryx-scrollbar-thumb-hover` | Secondary text color at 70% | Thumb color on hover and while dragging. |
| `--astryx-scrollbar-track` | `transparent` | Track background. |
| `--astryx-scrollbar-radius` | `9999px` | Corner radius of the track and the thumb. |

To change it site-wide, put the values in the `astryx.overrides` layer:

```css
@layer astryx.overrides {
  :root {
    --astryx-scrollbar-size: 12px;
    --astryx-scrollbar-thumb: color-mix(in srgb, var(--astryx-color-text-primary) 35%, transparent);
    --astryx-scrollbar-radius: 4px;
  }
}
```

You can set these variables on any ancestor, so to change just one area, set them in that area's selector. You can also put them in the variables of a [`Theme`](/en/components/theme) so they switch with it. More on variables in [CSS variables](/en/themes/css-variables).

### Changing behavior with options

`options` goes to OverlayScrollbars as is. It is deep-merged and has the highest priority, so it even overrides `autoHide`.

```vue
<ScrollbarsProvider
  :options="{
    scrollbars: {
      clickScroll: 'instant',
      visibility: 'visible',
    },
  }"
>
  <App />
</ScrollbarsProvider>
```

- `clickScroll: 'instant'`: clicking the track jumps the viewport there. Off by default. The animated variant (`true`) also needs OverlayScrollbars' `ClickScrollPlugin` registered.
- `visibility: 'visible'`: the scrollbar is always shown. The default is `'auto'`, which shows it only when content overflows.

See the [OverlayScrollbars docs](https://kingsora.github.io/OverlayScrollbars/) for the full option list.

## Notes

- **Server rendering**: the first paint shows native scrollbars. They swap to the custom ones after the page mounts, so you may see a brief style change on load.
- **macOS**: the system default is "Show scroll bars: when scrolling", so native scrollbars there are already overlay ones. After the swap, the main difference is a consistent look across platforms.
- **Reduced motion**: when the user's system asks for reduced motion, scrollbar transitions are turned off.
- **Keyboard, touch and screen readers**: no change. The element that scrolls is still the original one. The scrollbar is only drawn on top of it.
- **No provider**: nothing happens, and no extra code is loaded.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | Whether the subtree uses custom scrollbars. When `false`, the subtree is not initialized and any scrollbars already attached are destroyed. In nested providers the inner one wins. |
| `autoHide` | `'never' \| 'scroll' \| 'leave' \| 'move'` | `'scroll'` | When the scrollbar hides. `never` keeps it visible, `scroll` shows it while scrolling, `leave` hides it after the pointer leaves, `move` shows it while the pointer moves. |
| `autoHideDelay` | `number` | `800` | Milliseconds to wait before auto-hiding. |
| `options` | `ScrollbarsOptions` | — | Options passed to OverlayScrollbars (`PartialOptions`). Deep-merged last, so it has the highest priority. |

## Slots

| Slot | Description |
| --- | --- |
| `default` | Content that uses the custom scrollbars. |

## Exports

| Export | Type | Description |
| --- | --- | --- |
| `useScrollbars` | `(target: MaybeRefOrGetter, options?: UseScrollbarsOptions) => UseScrollbarsResult` | Attaches the custom scrollbar to a scroll container you wrote yourself. Does nothing without a `ScrollbarsProvider` above it. The container is the viewport and its children are not moved. Call it in a descendant of the provider. |
| `UseScrollbarsOptions` | `{ enabled?: MaybeRefOrGetter<boolean> }` | Second argument of `useScrollbars`. `enabled` defaults to `true` and controls whether this container uses it. If the outer provider is off, `true` has no effect. |
| `UseScrollbarsResult` | `{ active: Readonly<Ref<boolean>> }` | `active` tells you whether the custom scrollbar is attached to the element right now. |
| `ScrollbarsProviderProps` | Type | The type behind the props table above. |
| `ScrollbarsProviderSlots` | Type | The type behind the slots table above. |
| `ScrollbarsOptions` | `PartialOptions` | OverlayScrollbars option type, used by the `options` prop. |
| `ScrollbarsAutoHide` | `'never' \| 'scroll' \| 'leave' \| 'move'` | Value type of `autoHide`. |
| `ScrollbarsContext` | Type | What the provider gives its subtree: `enabled`, the merged `options`, and `mount`. |
| `ScrollbarsHandle` | `{ update, destroy }` | The handle `mount` returns, used to update options or destroy the scrollbar. |
| `scrollbarsKey` | `InjectionKey` | The injection key the provider uses. |

## Appearance variables

| Variable | Default | Description |
| --- | --- | --- |
| `--astryx-scrollbar-size` | `10px` | Scrollbar thickness. |
| `--astryx-scrollbar-thumb` | Secondary text color at 45% | Thumb color. |
| `--astryx-scrollbar-thumb-hover` | Secondary text color at 70% | Thumb color on hover and drag. |
| `--astryx-scrollbar-track` | `transparent` | Track background. |
| `--astryx-scrollbar-radius` | `9999px` | Corner radius of track and thumb. |

## Native attributes

The component renders no element of its own, so <code>class</code> and <code>style</code> are not forwarded.
