---
name: scroll-area
title: Scroll Area
url: https://ui.frontic.com/components/ui/scroll-area.md
tier: ui
install: npx @frontic/ui add scroll-area
files:
  - components/ui/scroll-area/ScrollArea.vue
  - components/ui/scroll-area/ScrollBar.vue
  - components/ui/scroll-area/index.ts
dependencies:
  - "@vueuse/core"
  - class-variance-authority
  - reka-ui
---

# Scroll Area

Holds content that scrolls inside a fixed area, vertically, horizontally or both, with a slim bar that shows on hover.

## Install

```sh
npx @frontic/ui add scroll-area
```

Used by: [Category Filter](https://ui.frontic.com/components/templates/category-filter.md), [Category Grid](https://ui.frontic.com/components/templates/category-grid.md), [Filter Sheet](https://ui.frontic.com/components/templates/filter-sheet.md), [Filter Sidebar](https://ui.frontic.com/components/templates/filter-sidebar.md), [Filter Toolbar](https://ui.frontic.com/components/templates/filter-toolbar.md)

npm packages: `@vueuse/core`, `class-variance-authority`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { computed, useAttrs } from 'vue'
import { Image } from '@/components/ui/image'
import { Price } from '@/components/ui/price'
import { ScrollArea } from '@/components/ui/scroll-area'

const attrs = useAttrs()
const orientation = computed(() => (attrs.orientation as string | undefined) ?? 'vertical')
const tiles = computed(() => orientation.value !== 'vertical')

const image = (name: string) => `https://static.frontic.com/cdn-cgi/image/width=160,format=auto/ui/products/${name}.jpg`
const eur = (amount: number) => ({ amount, currency: 'EUR' })
const products = [
  { name: 'Frost Escape Parka', price: eur(14900), image: image('frost-escape-parka') },
  { name: 'Winterwonder Parka', price: eur(18900), image: image('winterwonder-parka') },
  { name: 'Blizzard Defender', price: eur(12600), image: image('blizzard-defender-aubergine') },
  { name: 'Polar Light Ski Jacket', price: eur(7900), image: image('polar-light-ski-jacket') },
  { name: 'Snowflake Ski Goggles', price: eur(12700), image: image('snowflake-ski-goggles') },
  { name: 'Mountain View Ski Goggles', price: eur(9900), image: image('mountain-view-ski-goggles') },
  { name: 'TrailMaster 22', price: eur(14100), image: image('trailmaster-22') },
]
</script>

<template>
  <ScrollArea :class="['border border-border', { vertical: 'h-72 w-64', horizontal: 'w-80', both: 'h-72 w-80' }[orientation]]">
    <ul :class="{ vertical: 'flex flex-col gap-3 p-4', horizontal: 'flex w-max gap-4 p-4', both: 'grid w-max grid-cols-4 gap-4 p-4' }[orientation]">
      <li v-for="product in products" :key="product.name" :class="tiles ? 'flex w-28 flex-col gap-2' : 'flex items-center gap-3'">
        <Image :src="product.image" :alt="product.name" :class="tiles ? 'aspect-square w-28' : 'size-12 shrink-0'" />
        <div class="flex flex-col text-sm">
          <span>{{ product.name }}</span>
          <Price :price="product.price" />
        </div>
      </li>
    </ul>
  </ScrollArea>
</template>
```

The example also uses `image` and `price`, which `add scroll-area` leaves out: `npx @frontic/ui add image price`.

## Anatomy

```vue
<ScrollArea>
  <ScrollBar />
</ScrollArea>
```

## API reference

### ScrollArea

Holds content that scrolls within a fixed area.

`data-slot="scroll-area"` · `ScrollArea.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `ScrollType`: `'always' \| 'auto' \| 'scroll' \| 'hover' \| 'glimpse'` | `'hover'` | Describes the nature of scrollbar visibility, similar to how the scrollbar preferences in MacOS control visibility of native scrollbars.<br><br>`auto` - means that scrollbars are visible when content is overflowing on the corresponding orientation.<br><br>`always` - means that scrollbars are always visible regardless of whether the content is overflowing.<br><br>`scroll` - means that scrollbars are visible when the user is scrolling along its corresponding orientation.<br><br>`hover` - when the user is scrolling along its corresponding orientation and when the user is hovering over the scroll area.<br><br>`glimpse` - a hybrid approach that briefly shows scrollbars when the user enters the scroll area, then hides them until further interaction. |
| `dir` | `Direction`: `'ltr' \| 'rtl'` |  | The reading direction of the combobox when applicable.<br><br>If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. |
| `scrollHideDelay` | `number` | `600` | If type is set to either `scroll` or `hover`, this prop determines the length of time, in milliseconds,<br><br>before the scrollbars are hidden after the user stops interacting with scrollbars. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `orientation` | `'vertical' \| 'horizontal' \| 'both'` | `'vertical'` | The direction the content scrolls in, and so the bars it draws. |

| CSS variable | Description |
| --- | --- |
| `--reka-scroll-area-corner-height` | The height of the corner where the two scrollbars meet, in pixels. |
| `--reka-scroll-area-corner-width` | The width of the corner where the two scrollbars meet, in pixels. |

### ScrollBar

Shows the scroll position and lets the user drag to scroll. Renders a `<div>`.

`data-slot="scroll-area-scrollbar"` · `ScrollBar.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | The orientation of the scrollbar |
| `forceMount` | `boolean` | `false` | Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-orientation` | `'vertical' \| 'horizontal'` | Carries the `orientation` prop. |
| `data-scrollbarimpl` |  | Marks the scrollbar element. |
| `data-state` | `'visible' \| 'hidden'` | The state the part is in, one value per state. |

| CSS variable | Description |
| --- | --- |
| `--reka-scroll-area-thumb-height` | The thumb's height on a vertical scrollbar, in pixels. |
| `--reka-scroll-area-thumb-width` | The thumb's width on a horizontal scrollbar, in pixels. |

## Styling

| Part | Recipe |
| --- | --- |
| `viewport` | [`scroll-area-viewport`](https://ui.frontic.com/DESIGN.md#scroll-area-viewport) |
| `scrollbar` | [`scroll-area-scrollbar`](https://ui.frontic.com/DESIGN.md#scroll-area-scrollbar) |
| `thumb` | [`scroll-area-thumb`](https://ui.frontic.com/DESIGN.md#scroll-area-thumb) |

States with a look of their own: focus.

## Source

`components/ui/scroll-area/ScrollArea.vue`

```vue
<script lang="ts">
/** Holds content that scrolls within a fixed area. */
export default {}
</script>

<script setup lang="ts">
import type { ScrollAreaRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport, useForwardProps } from 'reka-ui'
import ScrollBar from './ScrollBar.vue'

const props = withDefaults(
  defineProps<
    ScrollAreaRootProps & {
      class?: HTMLAttributes['class']
      /** The direction the content scrolls in, and so the bars it draws. */
      orientation?: 'vertical' | 'horizontal' | 'both'
    }
  >(),
  { orientation: 'vertical' },
)

const delegatedProps = reactiveOmit(props, 'class', 'orientation')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <ScrollAreaRoot data-slot="scroll-area" v-bind="forwarded" :class="['relative', props.class]">
    <ScrollAreaViewport data-slot="scroll-area-viewport" class="fui-scroll-area-viewport size-full outline-none">
      <slot />
    </ScrollAreaViewport>
    <ScrollBar v-if="props.orientation !== 'horizontal'" />
    <ScrollBar v-if="props.orientation !== 'vertical'" orientation="horizontal" />
    <ScrollAreaCorner />
  </ScrollAreaRoot>
</template>
```

`components/ui/scroll-area/ScrollBar.vue`

```vue
<script lang="ts">
/** Shows the scroll position and lets the user drag to scroll. */
export default {}
</script>

<script setup lang="ts">
import type { ScrollAreaScrollbarProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { cva } from 'class-variance-authority'
import { ScrollAreaScrollbar, ScrollAreaThumb, useForwardProps } from 'reka-ui'

const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(), {
  orientation: 'vertical',
})

const delegatedProps = reactiveOmit(props, 'class')

const scrollbar = cva('fui-scroll-area-scrollbar touch-none transition-colors select-none', {
  variants: {
    orientation: {
      vertical: 'fui-scroll-area-scrollbar-vertical',
      horizontal: 'fui-scroll-area-scrollbar-horizontal',
    },
  },
  defaultVariants: { orientation: 'vertical' },
})
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <ScrollAreaScrollbar data-slot="scroll-area-scrollbar" :data-orientation="props.orientation" v-bind="forwarded" :class="[scrollbar({ orientation: props.orientation }), props.class]">
    <ScrollAreaThumb data-slot="scroll-area-thumb" class="fui-scroll-area-thumb relative flex-1" />
  </ScrollAreaScrollbar>
</template>
```

## API of the items the example uses

### Image

[Full docs](https://ui.frontic.com/components/ui/image.md) · `npx @frontic/ui add image`

#### Image

Shows an image with a loading state and a fallback if it fails to load. Renders a `<span>`.

`data-slot="image"` · `Image.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `src` | `string` |  | URL of the image to show. |
| `alt` | `string` |  | Text alternative read by screen readers; leave empty for a purely decorative image. |
| `loadingEffect` | `LoadingEffect`: `'skeleton' \| 'blur' \| 'none'` | `'skeleton'` | What shows until the image has loaded: a pulsing placeholder, the `placeholderSrc` preview blurred, or nothing. Blur without a `placeholderSrc` shows the pulsing placeholder. |
| `fallbackSrc` | `string` |  | URL tried when `src` fails to load; the fallback slot shows when both fail. |
| `placeholderSrc` | `string` |  | URL of a tiny version of the image, e.g. 24px wide, shown blurred while `src` loads when `loadingEffect` is `blur`. |
| `priority` | `boolean` | `false` | Loads the image first, eagerly and at high fetch priority, instead of when it nears the viewport. Set it on the one image that is the page's largest paint, such as a hero or the first gallery image. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `fallback` |  | Replaces the placeholder shown when the image fails to load and no fallback URL loads either. |

#### ImageFallback

Shows a placeholder in place of an image that failed to load. Renders a `<span>`.

`data-slot="image-fallback"` · `ImageFallback.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### Price

[Full docs](https://ui.frontic.com/components/ui/price.md) · `npx @frontic/ui add price`

#### Price

Shows a price, with the original price and discount percentage when reduced. Renders a `<span>`.

`data-slot="price"` · `Price.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `PriceSize`: `'md' \| 'sm' \| 'lg'` | `'md'` | The text size of the price. |
| `align` | `PriceAlign`: `'start' \| 'end'` | `'start'` | Which side the badge, the prices and the hint line up on. |
| `price` (required) | `PriceType` |  | The price to show: `amount` and `ref` in minor units such as cents, `currency` as an ISO 4217 code. |
| `locale` | `string` |  | The locale that formats the price, e.g. `de-DE`; the ConfigProvider's when left out. |
| `showRef` | `boolean` | `true` | Shows the reference price struck through when it is higher than the amount. |
| `showDiscount` | `boolean` | `true` | Shows the saving as a percentage next to a discounted price. |
| `showHint` | `boolean` | `true` | Shows the hint below the price when the hint slot holds one. |
| `refLabel` | `string` | `'Was'` | Read out before the original price of a reduced product; hidden on screen. |
| `amountLabel` | `string` | `'Now'` | Read out before the current price of a reduced product; hidden on screen. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `hint` |  | A note below the price, such as tax and shipping. |

#### PriceAmount

Shows the current price of the product. Renders a `<span>`.

`data-slot="price-amount"` · `PriceAmount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

#### PriceRef

Shows the original price of a discounted product. Renders a `<span>`.

`data-slot="price-ref"` · `PriceRef.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

#### PriceDiscount

Shows the percentage saved against the original price. Renders a `<span>`.

`data-slot="price-discount"` · `PriceDiscount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ percent: number; }` | Replaces the discount badge; gets the percentage saved. |

#### PriceHint

Holds a note below the price, such as tax and shipping, which may link on. Renders a `<span>`.

`data-slot="price-hint"` · `PriceHint.vue`

Styling: `price-hint`, which extends [`link-foreground`](https://ui.frontic.com/DESIGN.md#link-foreground). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
