---
title: Scroll Area
desc: >-
  The QScrollArea Vue component offers a way of customizing the scrollbars for
  all desktop browsers.
related:
  - title: Layout Drawer
    path: ../layout/drawer.md
---
The QScrollArea component offers a neat way of customizing the scrollbars by encapsulating your content. Think of it as a DOM element which has `overflow: auto`, but with your own custom styled scrollbar instead of browser's default one and a few nice features on top.

## QScrollArea API

### Props

- `dark` (boolean, optional), default `null`
  Notify the component that the background is a dark color
- `vertical-offset` (any[], optional), default `[0, 0]` *(added v2.17)*
  Adds [top, bottom] offset to vertical thumb
- `horizontal-offset` (any[], optional), default `[0, 0]` *(added v2.17)*
  Adds [left, right] offset to horizontal thumb
- `bar-style` (string | any[] | object, optional)
  Object with CSS properties and values for custom styling the scrollbars (both vertical and horizontal)
  Examples:
    - `{ borderRadius: '5px', background: 'red', opacity: 1 }`
- `vertical-bar-style` (string | any[] | object, optional)
  Object with CSS properties and values for custom styling the vertical scrollbar; Is applied on top of 'bar-style' prop
  Examples:
    - `{ right: '4px', borderRadius: '5px', background: 'red', width: '10px', opacity: 1 }`
- `horizontal-bar-style` (string | any[] | object, optional)
  Object with CSS properties and values for custom styling the horizontal scrollbar; Is applied on top of 'bar-style' prop
  Examples:
    - `{ bottom: '4px', borderRadius: '5px', background: 'red', height: '10px', opacity: 1 }`
- `thumb-style` (object, optional)
  Object with CSS properties and values for custom styling the thumb of scrollbars (both vertical and horizontal)
  Examples:
    - `{ right: '4px', borderRadius: '5px', background: 'red', width: '10px', opacity: 1 }`
- `vertical-thumb-style` (object, optional)
  Object with CSS properties and values for custom styling the thumb of the vertical scrollbar; Is applied on top of 'thumb-style' prop
  Examples:
    - `{ right: '4px', borderRadius: '5px', background: 'red', width: '10px', opacity: 1 }`
- `horizontal-thumb-style` (object, optional)
  Object with CSS properties and values for custom styling the thumb of the horizontal scrollbar; Is applied on top of 'thumb-style' prop
  Examples:
    - `{ bottom: '4px', borderRadius: '5px', background: 'red', height: '10px', opacity: 1 }`
- `content-style` (string | any[] | object, optional)
  Object with CSS properties and values for styling the container of QScrollArea
  Examples: `{ backgroundColor: '#C0C0C0' }`
- `content-active-style` (string | any[] | object, optional)
  Object with CSS properties and values for styling the container of QScrollArea when scroll area becomes active (is mouse hovered)
  Examples: `{ backgroundColor: 'white' }`
- `visible` (boolean, optional), default `null`
  Manually control the visibility of the scrollbar; Overrides default mouse over/leave behavior
- `delay` (number | string, optional), default `1000`
  When content changes, the scrollbar appears; this delay defines the amount of time (in milliseconds) before scrollbars disappear again (if component is not hovered)
- `tabindex` (number | string, optional)
  Tabindex HTML attribute value
  Examples: `100`, `'0'`

### Methods

- `getScrollTarget(): Element`
  Get the scrolling DOM element target
  Returns: `Element` — DOM element upon which scrolling takes place
- `getScroll(): object`
  Get the current scroll information
  Returns: `object` — Scroll information
- `getScrollPosition(): object`
  Get current scroll position
  Returns: `object` — An object containing scroll position information
- `getScrollPercentage(): object`
  Get current scroll position in percentage (0.0 <= x <= 1.0)
  Returns: `object` — An object containing scroll position information in percentage
- `setScrollPosition(axis: string, offset: number, duration?: number): void`
  Set scroll position to an offset; If a duration (in milliseconds) is specified then the scroll is animated
  Params:
    - `axis` (string, required)
      Scroll axis
      Accepts: `'vertical'`, `'horizontal'`
    - `offset` (number, required)
      Scroll position offset from top (in pixels)
    - `duration` (number, optional)
      Duration (in milliseconds) enabling animated scroll
- `setScrollPercentage(axis: string, offset: number, duration?: number): void`
  Set scroll position to a percentage (0.0 <= x <= 1.0) of the total scrolling size; If a duration (in milliseconds) is specified then the scroll is animated
  Params:
    - `axis` (string, required)
      Scroll axis
      Accepts: `'vertical'`, `'horizontal'`
    - `offset` (number, required)
      Scroll percentage (0.0 <= x <= 1.0) of the total scrolling size
    - `duration` (number, optional)
      Duration (in milliseconds) enabling animated scroll

### Events

- `@scroll`
  Emitted when scroll information changes (and listener is configured)
  Params:
    - `info` (object, optional)
      An object containing scroll information
      Object shape:
        - `ref` (ComponentInstance, required)
          Vue reference to the QScrollArea which triggered the event
        - `verticalPosition` (number, required)
          Vertical scroll position (in px)
        - `verticalPercentage` (number, required)
          Vertical scroll percentage (0.0 <= x <= 1.0)
        - `verticalSize` (number, required)
          Vertical scroll size (in px)
        - `verticalContainerSize` (number, required)
          Height of the container (in px)
        - `verticalContainerInnerSize` (number, required) *(added v2.17)*
          Height of the container without the vertical offset (in px)
        - `horizontalPosition` (number, required)
          Horizontal scroll position (in px)
        - `horizontalPercentage` (number, required)
          Horizontal scroll percentage (0.0 <= x <= 1.0)
        - `horizontalSize` (number, required)
          Horizontal scroll size (in px)
        - `horizontalContainerSize` (number, required)
          Width of the container (in px)
        - `horizontalContainerInnerSize` (number, required) *(added v2.17)*
          Width of the container without the horizontal offset (in px)

### Slots

- `#default`
  Default slot in the devland unslotted content of the component

## Usage

The following examples are best seen on desktop as they make too little sense on a mobile device.

> [!TIP]
> You can also take a look at [Layout Drawer](../layout/drawer.md) to see some more examples of it in action.

### Basic

```vue
<template>
  <div class="q-ma-md">
    <q-scroll-area style="height: 200px; max-width: 300px">
      <div v-for="n in 100" :key="n" class="q-py-xs">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </div>
    </q-scroll-area>
  </div>
</template>
```

### Horizontal content

```vue
<template>
  <div class="q-pa-md">
    <q-scroll-area style="height: 230px; max-width: 300px">
      <div class="row no-wrap">
        <div v-for="n in 10" :key="n" style="width: 150px" class="q-pa-sm">
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Architecto
          fuga quae veritatis blanditiis sequi id expedita amet esse aspernatur!
          Iure, doloribus!
        </div>
      </div>
    </q-scroll-area>
  </div>
</template>
```

### Vertical and horizontal content

```vue
<template>
  <div class="q-pa-md">
    <q-scroll-area style="height: 230px; max-width: 300px">
      <div class="row no-wrap" v-for="r in 4" :key="'r' + r">
        <div v-for="n in 10" :key="n" style="width: 150px" class="q-pa-sm">
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Architecto
          fuga quae veritatis blanditiis sequi id expedita amet esse aspernatur!
          Iure, doloribus!
        </div>
      </div>
    </q-scroll-area>
  </div>
</template>
```

### Styled

```vue
<template>
  <div class="q-ma-md">
    <q-scroll-area
      :horizontal-offset="[0, 2]"
      :thumb-style="thumbStyle"
      :bar-style="barStyle"
      style="height: 200px; max-width: 300px"
    >
      <div v-for="n in 100" :key="n" class="q-pa-xs">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </div>
    </q-scroll-area>
  </div>
</template>

<script setup>
const thumbStyle = {
  borderRadius: '5px',
  backgroundColor: '#027be3',
  width: '5px',
  opacity: 0.75
}

const barStyle = {
  borderRadius: '9px',
  backgroundColor: '#027be3',
  width: '9px',
  opacity: 0.2
}
</script>
```

### Styled

```vue
<template>
  <div class="q-ma-md">
    <q-scroll-area
      :horizontal-offset="[0, 2]"
      :thumb-style="thumbStyle"
      :content-style="contentStyle"
      :content-active-style="contentActiveStyle"
      style="height: 200px; max-width: 300px"
    >
      <div v-for="n in 100" :key="n" class="q-pa-xs">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </div>
    </q-scroll-area>
  </div>
</template>

<script setup>
const contentStyle = {
  backgroundColor: 'rgba(0,0,0,0.02)',
  color: '#555'
}

const contentActiveStyle = {
  backgroundColor: '#eee',
  color: 'black'
}

const thumbStyle = {
  borderRadius: '5px',
  backgroundColor: '#027be3',
  width: '5px',
  opacity: '0.75'
}
</script>
```

### Dark design

```vue
<template>
  <div class="q-ma-md">
    <q-scroll-area
      dark
      class="bg-grey-9 text-white rounded-borders"
      style="height: 200px; max-width: 300px"
    >
      <div v-for="n in 100" :key="n" class="q-py-sm q-px-md">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </div>
    </q-scroll-area>
  </div>
</template>
```

### Controlling scrollbar visibility

When using the `visible` Boolean prop, the default mouse over/leave behavior is disabled, leaving you in full control of the scrollbar visibility.

### Controlling scrollbar visibility

```vue
<template>
  <div class="q-ma-md">
    <div>
      <q-toggle v-model="visible" label="Show scrollbar" />
    </div>

    <q-scroll-area :visible="visible" style="height: 200px; max-width: 300px">
      <div v-for="n in 100" :key="n" class="q-py-xs">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </div>
    </q-scroll-area>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const visible = ref(true)
</script>
```

### Delay

When content changes, the scrollbar appears then disappears again. You can set a certain delay (amount of time in milliseconds) before scrollbar disappears again (if component is not hovered):

### Delay

```vue
<template>
  <div class="q-pa-md">
    <q-btn-group class="q-mb-md">
      <q-btn color="primary" @click="less">Less</q-btn>
      <q-btn color="secondary" @click="more">More</q-btn>
    </q-btn-group>

    <q-scroll-area :delay="1200" style="height: 200px; max-width: 300px">
      <div v-for="n in number" :key="n">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </div>
    </q-scroll-area>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const number = ref(4)

function less() {
  if (number.value > 1) {
    number.value--
  }
}

function more() {
  number.value++
}
</script>
```

### Scroll position

```vue
<template>
  <div class="q-pa-md">
    <div class="row q-gutter-md q-mb-md">
      <q-btn
        :label="`Scroll to ${position}px`"
        color="primary"
        @click="scroll"
      />
      <q-btn
        :label="`Animate to ${position}px`"
        color="primary"
        @click="animateScroll"
      />
    </div>

    <q-scroll-area ref="scrollAreaRef" style="height: 150px; max-width: 300px">
      <ol>
        <li v-for="n in 1000" :key="n">
          Lorem ipsum dolor sit amet, consectetur adipisicing elit.
        </li>
      </ol>
    </q-scroll-area>
  </div>
</template>

<script setup>
import { ref, useTemplateRef } from 'vue'

const position = ref(300)
const scrollAreaRef = useTemplateRef('scrollAreaRef')

function scroll() {
  scrollAreaRef.value.setScrollPosition('vertical', position.value)
  position.value = Math.floor(Math.random() * 1001) * 20
}

function animateScroll() {
  scrollAreaRef.value.setScrollPosition('vertical', position.value, 300)
  position.value = Math.floor(Math.random() * 1001) * 20
}
</script>
```

### Scroll event

Below is an example of using the `@scroll` event to synchronize the scrolling between two containers.

### Synchronized

```vue
<template>
  <div class="q-ma-md row no-wrap">
    <q-scroll-area
      visible
      :horizontal-offset="[0, 2]"
      :thumb-style="thumbStyle"
      :bar-style="barStyle"
      style="height: 200px"
      class="col"
      ref="firstRef"
      @scroll="onScrollFirst"
    >
      <div v-for="n in 100" :key="n" class="q-pa-sm">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </div>
    </q-scroll-area>

    <q-scroll-area
      visible
      :horizontal-offset="[0, 2]"
      :thumb-style="thumbStyle"
      :bar-style="barStyle"
      style="height: 200px"
      class="col"
      ref="secondRef"
      @scroll="onScrollSecond"
    >
      <div v-for="n in 100" :key="n" class="q-pa-sm">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </div>
    </q-scroll-area>
  </div>
</template>

<script setup>
import { useTemplateRef } from 'vue'

const firstRef = useTemplateRef('firstRef')
const secondRef = useTemplateRef('secondRef')

let ignoreSource

function scroll(source, position) {
  // if we previously just updated
  // the scroll position, then ignore
  // this update as otherwise we'll flicker
  // the position from one scroll area to
  // the other in an infinite loop
  if (ignoreSource === source) {
    ignoreSource = null
    return
  }

  // we'll now update the other scroll area,
  // which will also trigger a @scroll event...
  // and we need to ignore that one
  ignoreSource = source === 'first' ? 'second' : 'first'

  const areaRef = source === 'first' ? secondRef : firstRef

  areaRef.value.setScrollPosition('vertical', position)
}

const thumbStyle = {
  borderRadius: '7px',
  backgroundColor: '#027be3',
  width: '4px',
  opacity: 0.75
}

const barStyle = {
  borderRadius: '9px',
  backgroundColor: '#027be3',
  width: '8px',
  opacity: 0.2
}

function onScrollFirst({ verticalPosition }) {
  scroll('first', verticalPosition)
}

function onScrollSecond({ verticalPosition }) {
  scroll('second', verticalPosition)
}
</script>
```

## Accessibility *(v2.25+)*

The custom scrollbars and their thumbs are hidden from assistive technology — they are redundant, pointer-only controls over what remains a natively scrollable container.

Whenever the content actually overflows, the scroll container becomes a Tab stop on its own, so the browser's native keyboard scrolling — arrow keys, <kbd>PageUp</kbd>/<kbd>PageDown</kbd>, <kbd>Home</kbd>/<kbd>End</kbd> — works without any setup (WCAG 2.1.1). A QScrollArea whose content fits stays out of the tab order, since there would be nothing to scroll. The `tabindex` prop still overrides both cases — pass `-1` to opt out entirely.
