Skip to content

Presence List

Animate items as they enter, leave or reorder in lists and tables.

Use it for lists that change while people watch: notifications, a queue, a filtered list, a table whose rows are added, removed or re-sorted. Each change happens where the eye can follow it. Don't use it when the whole list is replaced at once, like the next page of results. Key the list by page instead, or use Content Swap.

Installation

npm i @youj/motion

import { PresenceItem, PresenceList } from '@youj/motion'

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Usage

import { PresenceItem, PresenceList } from '@youj/motion'

export function Notifications({ items }: { items: Notification[] }) {
  return (
    <PresenceList className="flex flex-col gap-1">
      {items.map((item) => (
        <PresenceItem key={item.id} className="rounded-lg bg-surface px-3 py-2">
          {item.title}
        </PresenceItem>
      ))}
    </PresenceList>
  )
}
  • Every PresenceItem is a direct, keyed child of its PresenceList. The key is how a row is recognized as the same row after a change, so use a stable id, never the index.
  • as picks the elements: ul (the default), ol, tbody or div for the list, and li (the default), tr, div, article or section for items.
  • Items on the first render start settled. Pass initial to animate them in as well.

Examples

Enter, leave, reorder

  • Enter: the item takes its slot and fades down a few pixels, while the items below glide to make room.
  • Leave: the item fades where it stands and keeps its space, so column widths hold, then it's removed and the items below glide up.
  • Reorder: items glide to their new slots.

Movement is a translate-only FLIP, with no scale, so text, borders and cells never distort. Items are measured against the list, so when the list itself moves, because a toolbar appears above it or a card re-centers, the items don't glide for nothing. Each list is its own layout group, so two lists never fight, and memoized rows still animate.

Reference

Propschildren, as, initial… (5)

Props

PresenceList

PresenceList props
PropType
as'div' | 'ol' | 'tbody' | 'ul'

The list element.

default 'ul'

'ul'
initialboolean

Also animate the items present on first render.

default false

false
childrenRequiredReactNode

PresenceItems, keyed by a stable id.

–
Other props pass through: extends HTMLAttributes<HTMLElement>.

PresenceItem

PresenceItem props
PropType
as'div' | 'section' | 'article' | 'li' | 'tr'

The element to render.

default 'li'

'li'
childrenReactNode–
Other props pass through: extends HTMLAttributes<HTMLElement>.
MotionEnter, leave, move · reduced motion

Motion

Motion tokens
MomentTokens
Enterfade and a 6px drop into the slot
duration.baseease.enter

220ms · cubic-bezier(0.16, 1, 0.3, 1)

220mscubic-bezier(0.16, 1, 0.3, 1)
Leavefade in place, keeping the space
duration.fastease.exit

140ms · cubic-bezier(0.4, 0, 1, 1)

140mscubic-bezier(0.4, 0, 1, 1)
Movemaking room, closing gaps, reordering
spring.smooth

0.36s, bounce 0

0.36s, bounce 0

Reduced motion

Items are added, removed and reordered in one frame, with no layout animation at all.

Reduced motion

Every change applies at once: new items appear in their slot, removed ones are gone, and reordered ones are in their new place. The list is the same as with motion. It just doesn't travel.

Accessibilityfocus, announcements

Accessibility

  • The list keeps its semantics: a ul or ol of li, or a real tbody of tr. Screen readers announce the list and its length, and a table stays navigable by row and column.
  • A leaving item is inert from its first frame, so it can't take focus or clicks while it fades.
  • Removing the focused item would drop focus on the page body. When an item is removed with a button inside it, move focus first, to a neighbor's matching button or to the list's own controls. The notifications example does this.
  • The list doesn't announce changes. When a change is news, like a notification arriving, announce it from a live region.