Timeline Animation and Paginator#
Overview#
The timeline's real-time animation system replaced Vue's TransitionGroup with a lightweight, CSS-only approach . TransitionGroup applied a move CSS class to every item on each add/remove operation — a costly operation for lists with up to 30 items. The new system targets only the element being added or removed, using per-item flags stored directly on the data objects.
Three files form the core of this system:
paginator.ts— manages item state, animation flags, removal timers, and queue capacityMkStreamingTimelineItem.vue— CSS animation wrapper componentMkStreamingNotesTimeline.vue— timeline host that wires paginator state to the animation component
Animation Flags: _shouldAnimateIn_ / _shouldAnimateOut_#
Both flags are optional boolean properties on the MisskeyEntity type. The Paginator sets them directly on item objects — no separate reactive state is needed.
| Flag | Set by | When |
|---|---|---|
_shouldAnimateIn_ | prepend(), releaseQueue() | Item added to top of visible list |
_shouldAnimateOut_ | removeItem() | Item scheduled for removal |
These flags are passed as props to MkStreamingTimelineItem only when the user's animation preference is enabled :
<component
:is="prefer.s.animation ? MkStreamingTimelineItem : 'div'"
v-bind="prefer.s.animation ? { animatingIn: note._shouldAnimateIn_, animatingOut: note._shouldAnimateOut_ } : {}"
...
When animations are disabled, items are wrapped in a plain <div> and no flags are passed.
MkStreamingTimelineItem.vue — CSS Animations#
The component translates animatingIn/animatingOut props into CSS class bindings that trigger @keyframes animations:
- Enter (
.enter): 0.7scubic-bezier(0.23, 1, 0.32, 1), animatesheight: 0 → auto,opacity: 0 → 1,translateY(max(-64px,-100%)) → 0 - Leave (
.leave): 0.2scubic-bezier(0,.5,.5,1), animatesheight: auto → 0,opacity: 1 → 0
The constant ITEM_REMOVAL_MS = 200 is exported and consumed by the timeline to synchronize the paginator's removal delay with the CSS leave animation duration.
Height transition compatibility#
Animating height: auto requires interpolate-size: allow-keywords, a newer CSS feature. For browsers without support, a ResizeObserver measures the inner element's height and writes it to --child-height, which the keyframe then targets as a concrete pixel value instead of auto .
Paginator — Item Limits, Queue, and Removal Timers#
Item limits and constants#
Defined at the top of paginator.ts:
| Constant | Value | Purpose |
|---|---|---|
MAX_ITEMS | 30 | Max items in the visible list |
MAX_QUEUE_ITEMS | 100 | Max items held in the ahead queue |
FIRST_FETCH_LIMIT | 15 | Items fetched on initial load |
SECOND_FETCH_LIMIT | 30 | Items fetched on paginate-older/newer |
trim() enforces MAX_ITEMS by slicing the array; it also sets canFetchOlder = true when the list was at capacity (implying older items exist).
Ahead queue#
enqueue(item) holds items arriving while the user has scrolled away from the top. The queue is capped at MAX_QUEUE_ITEMS = 100; oldest items are dropped on overflow. releaseQueue() sets _shouldAnimateIn_ = true on all queued items before prepending them, then clears the queue.
In MkStreamingNotesTimeline, the queue is released automatically when the user scrolls back to the top or when the tab becomes visible again .
Removal timers#
The itemRemovalDelay constructor option controls deletion behavior:
false(or animations disabled): item is spliced out immediately .number(ms): sets_shouldAnimateOut_ = trueon the item, then schedules asetTimeoutofitemRemovalDelay + 20msto actually splice it — the 20ms buffer ensures the CSS leave animation finishes before DOM removal .
Timers are tracked in a Map<string, number> keyed by item id to prevent double-scheduling. All pending timers are cancelled in clearRemovalTimers(), called during init().
The timeline sets itemRemovalDelay to ITEM_REMOVAL_MS (200ms) when animations are on, or false when off .
Data Flow Summary#
WebSocket / polling
↓
MkStreamingNotesTimeline.prepend(note)
↓
isTop() && !isPausingUpdate?
YES → paginator.prepend() → sets _shouldAnimateIn_ = true, unshifts, trims to 30
NO → paginator.enqueue() → held in aheadQueue (≤100)
↓ (user scrolls to top or tab visible)
paginator.releaseQueue() → sets _shouldAnimateIn_ on all, unshifts, trims to 30
↓
MkStreamingTimelineItem → CSS .enter animation triggers
For deletions (noteDeleted global event → paginator.removeItem(id)): item gets _shouldAnimateOut_ = true, CSS .leave animation plays for 200ms, then item is spliced from the array.
Key Sources#
| File | Purpose |
|---|---|
paginator.ts | Core data class: flags, timers, queue, limits |
MkStreamingTimelineItem.vue | CSS animation wrapper |
MkStreamingNotesTimeline.vue | Timeline host, streaming, queue release logic |
| PR #17708 | Original implementation rationale |