PXD

Stick To Bottom

Keep a scroll container pinned to the bottom while content updates, until the user scrolls away. Scrolling back to the bottom restores auto-follow.

Auto-scroll is always instant — smooth scrolling is not supported, so continuous chat-style follow stays stable.

Height required: the container only scrolls when its height is constrained (for example h-40, or a flex child with min-h-0). Without that, it grows with content and never scrolls.

Default

Item 1
Item 2
Item 3
<script setup lang="ts">
import { ref } from 'vue'

const items = ref(['Item 1', 'Item 2', 'Item 3'])

function append() {
  items.value.push(`Item ${items.value.length + 1}`)
}
</script>

<template>
  <div class="flex flex-col gap-2">
    <PButton @click="append">Add item</PButton>

    <PStickToBottom class="h-40 rounded-lg border border-dashed p-2">
      <div v-for="item in items" :key="item" class="p-2 rounded bg-background-100">
        {{ item }}
      </div>
    </PStickToBottom>
  </div>
</template>

Disabled auto-follow

Log 1
<script setup lang="ts">
import { ref } from 'vue'

const items = ref(['Log 1'])

function append() {
  items.value.push(`Log ${items.value.length + 1}`)
}
</script>

<template>
  <div class="flex flex-col gap-2">
    <PButton @click="append">Append log</PButton>

    <PStickToBottom :enabled="false" class="h-36 rounded-lg border border-dashed p-2">
      <div v-for="item in items" :key="item" class="py-1">
        {{ item }}
      </div>
    </PStickToBottom>
  </div>
</template>

Scroll to bottom action

Use the action slot with PBacktop to jump back to the bottom after the user scrolls away. The slot exposes isAtBottom, scrollToBottom, and forceStickToBottom.

Message 1
Message 2
Message 3
Message 4
Message 5
Message 6
Message 7
Message 8
Message 9
Message 10
Message 11
Message 12
<script setup lang="ts">
import { ref } from 'vue'
import ArrowUpIcon from '@gdsicon/vue/arrow-up'

const items = ref(Array.from({ length: 12 }, (_, i) => `Message ${i + 1}`))

function append() {
  items.value.push(`Message ${items.value.length + 1}`)
}
</script>

<template>
  <div class="flex flex-col gap-2">
    <PButton @click="append">Send message</PButton>

    <PStickToBottom class="relative h-40 rounded-lg border border-dashed p-2">
      <div v-for="item in items" :key="item" class="p-2 rounded bg-background-100">
        {{ item }}
      </div>

      <template #action="{ isAtBottom, forceStickToBottom }">
        <PButton
          v-if="!isAtBottom"
          class="mx-auto flex"
          size="sm" shape="rounded" icon
          @click="forceStickToBottom"
        >
          <ArrowUpIcon class="rotate-180" />
        </PButton>
      </template>
    </PStickToBottom>
  </div>
</template>

Props

NameTypeDefaultDescription
thresholdnumber16Distance from bottom considered "at bottom"
enabledbooleantrueWhether updates auto-scroll while at bottom
content-classComponentClass-Extra class on the inner content wrapper
content-styleCSSProperties | string-Extra style on the inner content wrapper

Events

NameTypeDescription
change(isAtBottom: boolean) => voidEmitted when the at-bottom state changes.

Slots

NamePropsDescription
default-Scrollable content
action{ isAtBottom, scrollToBottom, forceStickToBottom }Sticky action area for controls such as a jump-to-bottom button

Methods

NameTypeDescription
containerRefHTMLElementScroll container element exposed for external scroll APIs.
contentRefHTMLElementInner content wrapper element exposed for external measurement.
isAtBottombooleanWhether the container is currently within the bottom threshold.
scrollToBottom() => voidScroll instantly to the bottom, leaving horizontal scroll untouched.
forceStickToBottom() => voidScroll to the bottom and re-enable auto-stick.
stickIfNeeded() => voidScroll to the bottom only when already at the bottom and enabled is true.
update() => voidRe-measure whether the container is at the bottom.

Source

Source