Pinned List

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

Pinned

Drafts
Projects

All

Inbox
Archive
"use client"

import * as React from "react"
import {
  PinnedList,
  PinnedListItem,
  PinnedListItems,
  PinnedListLabel,
  PinnedListPinned,
  PinnedListTrigger,
  PinnedListUnpinned,
} from "@animate-ui/react/primitives/pinned-list"
import { PinIcon, PinOffIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"

const ITEMS = [
  { id: "inbox", name: "Inbox", pinned: false },
  { id: "drafts", name: "Drafts", pinned: true },
  { id: "projects", name: "Projects", pinned: true },
  { id: "archive", name: "Archive", pinned: false },
]

type Item = (typeof ITEMS)[number]

function Row({ item }: { item: Item }) {
  return (
    <PinnedListItem
      id={item.id}
      customTrigger
      className="flex h-12 items-center justify-between rounded-xl bg-accent px-4 text-sm select-none"
    >
      {item.name}
      <PinnedListTrigger
        aria-label={item.pinned ? `Unpin ${item.name}` : `Pin ${item.name}`}
        className="-mr-2 flex size-8 items-center justify-center rounded-lg bg-background text-muted-foreground"
      >
        <HugeiconsIcon icon={item.pinned ? PinOffIcon : PinIcon} strokeWidth={2} size={16} />
      </PinnedListTrigger>
    </PinnedListItem>
  )
}

export default function PinnedListDemo() {
  const [items, setItems] = React.useState(ITEMS)

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

  const toggle = (id: string) =>
    setItems((prev) =>
      prev.map((item) =>
        item.id === id ? { ...item, pinned: !item.pinned } : item
      )
    )

  return (
    <PinnedList
      onPinnedChange={toggle}
      className="flex w-60 flex-col gap-6"
    >
      <PinnedListPinned>
        <PinnedListLabel
          hide={pinned.length === 0}
          className="mb-2 text-xs font-medium text-muted-foreground"
        >
          Pinned
        </PinnedListLabel>
        <PinnedListItems className="flex flex-col gap-2">
          {pinned.map((item) => (
            <Row key={item.id} item={item} />
          ))}
        </PinnedListItems>
      </PinnedListPinned>
      <PinnedListUnpinned>
        <PinnedListLabel
          hide={unpinned.length === 0}
          className="mb-2 text-xs font-medium text-muted-foreground"
        >
          All
        </PinnedListLabel>
        <PinnedListItems className="flex flex-col gap-2">
          {unpinned.map((item) => (
            <Row key={item.id} item={item} />
          ))}
        </PinnedListItems>
      </PinnedListUnpinned>
    </PinnedList>
  )
}

Installation

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

Usage

import * as React from 'react';
import {
  PinnedList,
  PinnedListItem,
  PinnedListItems,
  PinnedListLabel,
  PinnedListPinned,
  PinnedListTrigger,
  PinnedListUnpinned,
} from '@animate-ui/react/primitives/pinned-list';

export function Folders() {
  const [items, setItems] = React.useState(ITEMS);
  const toggle = (id: string) =>
    setItems((prev) =>
      prev.map((i) => (i.id === id ? { ...i, pinned: !i.pinned } : i)),
    );

  const pinned = items.filter((i) => i.pinned);
  const unpinned = items.filter((i) => !i.pinned);

  return (
    <PinnedList onPinnedChange={toggle}>
      <PinnedListPinned>
        <PinnedListLabel hide={pinned.length === 0}>Pinned</PinnedListLabel>
        <PinnedListItems>
          {pinned.map((item) => (
            <PinnedListItem key={item.id} id={item.id} customTrigger>
              {item.name}
              <PinnedListTrigger aria-label={`Unpin ${item.name}`}>
                …
              </PinnedListTrigger>
            </PinnedListItem>
          ))}
        </PinnedListItems>
      </PinnedListPinned>
      <PinnedListUnpinned>
        <PinnedListLabel hide={unpinned.length === 0}>All</PinnedListLabel>
        <PinnedListItems>
          {unpinned.map((item) => (
            <PinnedListItem key={item.id} id={item.id} customTrigger>
              {item.name}
              <PinnedListTrigger aria-label={`Pin ${item.name}`}>
                …
              </PinnedListTrigger>
            </PinnedListItem>
          ))}
        </PinnedListItems>
      </PinnedListUnpinned>
    </PinnedList>
  );
}

You own the pinned state: onPinnedChange tells you which item to move. Without customTrigger, 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 customTrigger, 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
onPinnedChange?
(id: string) => void-
children
React.ReactNode-
...props?
HTMLMotionProps<'div'>-

PinnedListPinned, PinnedListUnpinned, PinnedListItems

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

PinnedListLabel

PropTypeDefault
asChild?
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
asChild?
booleanfalse
id
string-
customTrigger?
booleanfalse
transition?
Transition{ type: 'spring', stiffness: 320, damping: 25, mass: 0.8 }
children
React.ReactNode-
...props?
HTMLMotionProps<'div'>-

PinnedListTrigger

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

PropTypeDefault
asChild?
booleanfalse
...props?
HTMLMotionProps<'button'>-

usePinnedList, usePinnedListItem

usePinnedList returns movingId (the item moving between the lists), setMovingId and onPinnedChange. usePinnedListItem returns the item's id.

Credits

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

Last updated: 10/18/2026

On this page