title: Scroll Island
description: A dynamic and interactive component that displays scroll progress with animated visuals and an expandable area for additional content.
---

```vue
<template>
  <div class="flex items-center justify-center p-8">
    <span class="text-lg text-primary">
      See the scroll island in action on the top of this page
    </span>
    <ScrollIsland title="Scroll Island">
      <div class="my-3 flex flex-col gap-2">
        <a href="#install-using-cli"># Install using CLI</a>
        <a href="#install-manually"># Install Manually</a>
        <a href="#api"># API</a>
        <a href="#features"># Features</a>
        <a href="#credits"># Credits</a>
      </div>
    </ScrollIsland>
  </div>
</template>

```

::alert{type="info"}
**Note:** This component requires `@number-flow/vue` as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="scroll-island" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install @number-flow/vue
```

```bash [pnpm]
pnpm install @number-flow/vue
```

```bash [bun]
bun add @number-flow/vue
```

```bash [yarn]
yarn add @number-flow/vue
```

::

Copy and paste the following code

```vue
<template>
  <MotionConfig
    :transition="{
      duration: 0.7,
      type: 'spring',
      bounce: 0.5,
    }"
  >
    <div
      :class="
        cn(
          'fixed left-1/2 top-12 z-[999] -translate-x-1/2 bg-primary/90 backdrop-blur-lg border-radius',
          $props.class,
        )
      "
      @click="() => (open = !open)"
    >
      <motion.div
        id="motion-id"
        layout
        :initial="{
          height: props.height,
          width: 0,
        }"
        :animate="{
          height: open && isSlotAvailable ? 'auto' : props.height,
          width: open && isSlotAvailable ? 320 : 260,
        }"
        class="bg-natural-900 relative cursor-pointer overflow-hidden text-secondary"
      >
        <header class="gray- flex h-11 cursor-pointer items-center gap-2 px-4">
          <AnimatedCircularProgressBar
            :value="scrollPercentage * 100"
            :min="0"
            :max="100"
            :circle-stroke-width="10"
            class="w-6"
            :show-percentage="false"
            :duration="0.3"
            :gauge-secondary-color="isDark ? '#6b728055' : '#6b728099'"
            :gauge-primary-color="isDark ? 'black' : 'white'"
          />
          <h1 class="grow text-center font-bold">{{ title }}</h1>
          <NumberFlow
            :value="scrollPercentage"
            :format="{ style: 'percent' }"
            locales="en-US"
          />
        </header>
        <motion.div
          v-if="isSlotAvailable"
          class="mb-2 flex h-full max-h-60 flex-col gap-1 overflow-y-auto px-4 text-sm"
        >
          <slot />
        </motion.div>
      </motion.div>
    </div>
  </MotionConfig>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import NumberFlow from "@number-flow/vue";
import { useColorMode } from "@vueuse/core";
import { motion, MotionConfig } from "motion-v";
import { computed, onMounted, onUnmounted, ref, useSlots } from "vue";

interface Props {
  class?: string;
  title?: string;
  height?: number;
}

const props = withDefaults(defineProps<Props>(), {
  class: "",
  title: "Progress",
  height: 44,
});

const open = ref(false);
const slots = useSlots();

const scrollPercentage = ref(0);

const isDark = computed(() => useColorMode().value == "dark");
const isSlotAvailable = computed(() => !!slots.default);
const borderRadius = computed(() => `${props.height / 2}px`);

onMounted(() => {
  if (window === undefined) return;

  window.addEventListener("scroll", updatePageScroll);
  updatePageScroll();
});

function updatePageScroll() {
  scrollPercentage.value = window.scrollY / (document.body.scrollHeight - window.innerHeight);
}

onUnmounted(() => {
  window.removeEventListener("scroll", updatePageScroll);
});
</script>

<style scoped>
.border-radius {
  border-radius: v-bind(borderRadius);
}
</style>

```
::

## API

| Prop Name | Type     | Default      | Description                                     |
| --------- | -------- | ------------ | ----------------------------------------------- |
| `class`   | `string` | `""`         | Additional CSS classes for custom styling.      |
| `title`   | `string` | `"Progress"` | Title displayed in the header of the component. |
| `height`  | `string` | `44`         | Height of the component.                        |

## Features

- **Scroll Progress Tracking**: Dynamically displays the scroll progress of the page as a percentage.
- **Expandable Layout**: Transforms between a circular and a rectangular layout based on user interaction.
- **Animated Circular Progress Bar**: Displays a visually appealing progress bar with smooth transitions.
- **Dynamic Content Slot**: Supports additional content in the expandable section.
- **Dark Mode Support**: Adapts to the dark or light mode of the user's system preferences.

## Credits

- Inspired by the work of [Ali Samadi](https://x.com/alisamadi__/status/1854312982559502556) & [Nitish Khagwal](https://x.com/nitishkmrk)
- [NumberFlow by Maxwell Barvian](https://number-flow.barvian.me/vue) for number formatting and animations.