Frontmatter
| id | 9409 |
| title | CSS Animations for TreeGrid Expand/Collapse |
| state | Open |
| labels | enhancementdesignaigrid |
| assignees | tobiu |
| createdAt | Mar 9, 2026, 12:01 PM |
| updatedAt | Mar 12, 2026, 3:12 PM |
| githubUrl | https://github.com/neomjs/neo/issues/9409 |
| author | tobiu |
| commentsCount | 2 |
| parentIssue | 9404 |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
CSS Animations for TreeGrid Expand/Collapse

Input from Gemini 3.1 Pro:
✦ ### Strategy Analysis & Pivot: Why simple CSS transitions fail in Virtual Row Pooling
We attempted to implement CSS animations for TreeGrid expand/collapse by applying
.neo-tree-row-enteringand.neo-tree-row-leavingclasses during themutateevent of theTreeStore, combined with CSStransition: opacity, transform.The Failure Mode: The UX was broken. The root cause is the architectural conflict between CSS Transitions (which require stable DOM node identities) and Virtual Row Pooling (which intentionally recycles DOM nodes).
- No Fade-In: When expanding a folder, the new rows are actually recycled DOM nodes pulled from the bottom of the virtual pool. When they are placed into their new position and given the
enteringclass (opacity: 0, translateY: -32px), they don't fade in smoothly from their new slot. Because they are the same physical DOM node that was just at the bottom of the grid, the browser attempts to animate them "flying up" across the entire screen fromtranslateY(1500px)totranslateY(150px).- No Fade-Out: When collapsing a folder, the rows are instantly spliced out of the
TreeStoreprojection layer and recycled for other records. They simply vanish; there is no DOM node left in that visual position to apply afade-outtransition to.- Global Transition Chaos: If we apply
transition: transformglobally to grid rows, normal virtual scrolling breaks completely, as every recycled row animates to its new position as you scroll.The "Delayed Permutation" Strategy (The only viable path):
To animate a virtualized TreeGrid, we must separate the visual transition from the data mutation.
beforeMutateEvent:TreeStoremust fire an event beforespliceis called, detailing the intended changes (e.g., "Row 5 is expanding, creating 3 new rows").- Visual Pre-emption (The Gap):
GridBodylistens to this event. It applies a temporarytransform: translateY(3 * rowHeight)to all rows currently rendered below index 5. It turns on CSS transitions only for these specific rows.- The Wait: The grid visually opens a gap over 300ms. Crucially, the
TreeStoredata and the Virtual Scroller's internal math remain completely untouched and stable during this time.- The Commit: After 300ms, the actual
TreeStore.spliceoccurs. The new items are injected.GridBodyremoves the temporary transforms. The new rows drop into the pre-made gap (where they can safely be given a fastopacity: 0 -> 1fade-in).Challenges of this Strategy:
- Concurrency: If a user scrolls during the 300ms delay, the virtual scroller will recycle rows based on the old data, but some of those rows have temporary inline CSS transforms applied. We would need to temporarily lock scrolling (
overflow: hidden) during tree animations.- Rapid Clicks: If a user clicks expand/collapse rapidly on multiple folders, the
TreeStoremust queue these delayed permutations safely.Conclusion: Adding animations to the TreeGrid requires a massive architectural shift in how
TreeStoremutates and howGridBodyintercepts those mutations before they affect the Virtual Scroller. Given the complexity (state management during delays, scrolling conflicts, rapid clicks), we are reverting the initial attempt.This feature requires a dedicated, fresh session focused entirely on the "Delayed Permutation" architecture.

Input from Gemini 3.1 Pro:
✦ ### Architectural Blueprint: "Delayed Permutation" & Plugin Strategy
Based on further analysis, implementing the "Delayed Permutation" strategy requires a clean hybrid architecture splitting responsibilities between the core Data layer and an opt-in View plugin.
1. Core Data Layer (
Neo.data.TreeStore)The core store must gain the ability to temporarily suspend its structural mutations, allowing the UI to react to the intent of a change before the data actually shifts.
- New Config:
deferMutations: Boolean(default:false).- New Lifecycle Event:
beforeMutate. Fired whenexpand()orcollapse()is called, containing the intended delta (e.g.,{ action: 'expand', record, addedCount: 3 }).- New API:
commitMutations(). Executes the queuedspliceoperation and fires the standardmutateevent.Why Core? We cannot safely "monkey-patch" or intercept asynchronous data operations from a plugin without creating fragile code. The data layer must natively support pausing.
2. View Layer Plugin (
Neo.grid.plugin.AnimateTreeNodes)We should not add animation logic to
GridBody.mjsorRow.mjs. It violates the Single Responsibility Principle and bloats the core renderer for a niche feature.
- Activation: When attached, it sets
grid.store.deferMutations = true.- Orchestration: Listens to the
beforeMutateevent.- Visual Pre-emption: Analyzes the
GridBodyVDOM. Calculates which existing rows need to slide to create/close gaps. Applies temporary inlinetransformoverrides and.neo-is-animatingCSS classes directly to those VDOM nodes.- The Bridge: Sets a timeout matching the CSS transition. Once complete, it calls
store.commitMutations(), allowing the grid to perform a standard, instantaneous data update into the pre-animated gaps.3. Addressing the Broken
AnimateRowsPluginThe existing
Neo.grid.plugin.AnimateRowsis currently broken under the Row Pooling architecture because it assumes stable DOM node identities, which pooling destroys.Recommendation: We should combine the goals of
AnimateRowsandAnimateTreeNodesinto a single, comprehensiveNeo.grid.plugin.Animate(or rewriteAnimateRowsentirely).
- The fundamental problem (animating pooled rows) is identical for both sorting/filtering and tree expansion.
- A unified plugin can use the "Delayed Permutation" (or Snapshot & Overlay) techniques to handle all grid animations: row insertion, deletion, sorting, and tree toggling.
- Fixing the old plugin in isolation is wasted effort if the new TreeGrid animation strategy requires fundamentally different mechanics.
This approach keeps the core framework highly performant while cleanly encapsulating complex, stateful animations.
Goal
Provide smooth, CSS-based visual transitions when expanding or collapsing nodes in a Tree Grid, enhancing the user experience.
Details
GridBodyuses row pooling and absolute positioning (transform: translate3d), we cannot rely on simpleheight: 0toheight: 100%transitions on the container.opacity) or slight vertical slide (translateY) for newly rendered rows that enter the visible array during anexpandoperation.collapse, rows might quickly fade out before the view collapses..neo-tree-row-entering,.neo-tree-row-leaving).resources/scss/src/grid/Row.scss.GridBody(orRow) needs to conditionally apply these classes based on theTreeStore's recent mutation events.animateTreeNodes: true(defaulting tofalsefor maximum performance) should govern this behavior.