Pinned List

A list whose items glide between a pinned and an unpinned group.

<script setup lang="ts">
import {
  PinnedList,
  PinnedListItems,
  PinnedListLabel,
  PinnedListPinned,
  PinnedListUnpinned,
} from '@animate-ui/vue/primitives/pinned-list'
import { computed, ref } from 'vue'
import Row from './pinned-list-row.vue'

const items = ref([
  { id: 'inbox', name: 'Inbox', pinned: false },
  { id: 'drafts', name: 'Drafts', pinned: true },
  { id: 'projects', name: 'Projects', pinned: true },
  { id: 'archive', name: 'Archive', pinned: false },
])

const pinned = computed(() => items.value.filter((item) => item.pinned))
const unpinned = computed(() => items.value.filter((item) => !item.pinned))

function toggle(id: string) {
  items.value = items.value.map((item) =>
    item.id === id ? { ...item, pinned: !item.pinned } : item,
  )
}
</script>

<template>
  <PinnedList class="flex w-60 flex-col gap-6" @pinned-change="toggle">
    <PinnedListPinned>
      <PinnedListLabel
        :hide="pinned.length === 0"
        class="mb-2 text-xs font-medium text-muted-foreground"
      >
        Pinned
      </PinnedListLabel>
      <PinnedListItems class="flex flex-col gap-2">
        <Row v-for="item in pinned" :key="item.id" :item="item" />
      </PinnedListItems>
    </PinnedListPinned>
    <PinnedListUnpinned>
      <PinnedListLabel
        :hide="unpinned.length === 0"
        class="mb-2 text-xs font-medium text-muted-foreground"
      >
        All
      </PinnedListLabel>
      <PinnedListItems class="flex flex-col gap-2">
        <Row v-for="item in unpinned" :key="item.id" :item="item" />
      </PinnedListItems>
    </PinnedListUnpinned>
  </PinnedList>
</template>

Installation

npm install @animate-ui/vue motion-v
pnpm add @animate-ui/vue motion-v
yarn add @animate-ui/vue motion-v
bun add @animate-ui/vue motion-v

Usage

<script setup lang="ts">
import { computed, ref } from 'vue';
import {
  PinnedList,
  PinnedListItem,
  PinnedListItems,
  PinnedListLabel,
  PinnedListPinned,
  PinnedListTrigger,
  PinnedListUnpinned,
} from '@animate-ui/vue/primitives/pinned-list';

const items = ref(ITEMS);
const toggle = (id: string) =>
  (items.value = items.value.map((i) => (i.id === id ? { ...i, pinned: !i.pinned } : i)));

const pinned = computed(() => items.value.filter((i) => i.pinned));
const unpinned = computed(() => items.value.filter((i) => !i.pinned));
</script>

<template>
  <PinnedList @pinned-change="toggle">
    <PinnedListPinned>
      <PinnedListLabel :hide="pinned.length === 0">Pinned</PinnedListLabel>
      <PinnedListItems>
        <PinnedListItem v-for="item in pinned" :id="item.id" :key="item.id" custom-trigger>
          {{ item.name }}
          <PinnedListTrigger :aria-label="`Unpin ${item.name}`">…</PinnedListTrigger>
        </PinnedListItem>
      </PinnedListItems>
    </PinnedListPinned>
    <PinnedListUnpinned>
      <PinnedListLabel :hide="unpinned.length === 0">All</PinnedListLabel>
      <PinnedListItems>
        <PinnedListItem v-for="item in unpinned" :id="item.id" :key="item.id" custom-trigger>
          {{ item.name }}
          <PinnedListTrigger :aria-label="`Pin ${item.name}`">…</PinnedListTrigger>
        </PinnedListItem>
      </PinnedListItems>
    </PinnedListUnpinned>
  </PinnedList>
</template>

You own the pinned state: @pinned-change tells you which item to move. Without custom-trigger, clicking the whole item toggles it.

Animation

  • The item glides to its new place on a spring (stiffness 320, damping 25, mass 0.8: a shared layout animation with a slight overshoot), above the other items while it moves. The others make room.
  • Labels fade in and out when a group becomes empty or filled (220ms, ease-out), and slide with the items making room, on the items' spring.
  • Triggers, and items without custom-trigger, shrink on press (scale 0.97) on a quick spring (the item's own spring for items). Nothing scales on hover.
  • <AnimateUIProvider :tempo> paces the default transitions.
  • Under reduced motion, items jump to their new place and don't scale.

API Reference

PinnedList

PropTypeDefault
@pinned-change?
(id: string) => void-
#default
slot-
...props?
HTMLMotionProps<'div'>-

PinnedListPinned, PinnedListUnpinned, PinnedListItems

Plain divs that structure the groups. They take div props.

PinnedListLabel

PropTypeDefault
as-child?
booleanfalse
hide?
booleanfalse
transition?
Transition{ duration: 0.22, ease: [0.23, 1, 0.32, 1], layout: { type: 'spring', stiffness: 320, damping: 25, mass: 0.8 } }
...props?
HTMLMotionProps<'p'>-

PinnedListItem

PropTypeDefault
as-child?
booleanfalse
id
string-
custom-trigger?
booleanfalse
transition?
Transition{ type: 'spring', stiffness: 320, damping: 25, mass: 0.8 }
#default
slot-
...props?
HTMLMotionProps<'div'>-

PinnedListTrigger

A button that pins or unpins its item. Give it an accessible label.

PropTypeDefault
as-child?
booleanfalse
...props?
HTMLMotionProps<'button'>-

usePinnedList, usePinnedListItem

usePinnedList returns the movingId ref (the item moving between the lists) and pin(id). usePinnedListItem returns an id() getter.

Credits

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/23/2026