Skip to content
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions packages/runtime-dom/src/components/TransitionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
useTransitionState,
warn,
} from '@vue/runtime-core'
import { extend } from '@vue/shared'
import { extend, hasOwn } from '@vue/shared'

const positionMap = new WeakMap<VNode, DOMRect>()
const newPositionMap = new WeakMap<VNode, DOMRect>()
Expand Down Expand Up @@ -130,6 +130,19 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
tag = 'span'
}

// Filter out transition-specific props and TransitionGroup-specific props
// to avoid invalid HTML attributes
const filteredProps: Record<string, any> = {}
for (const key in rawProps) {
if (
!hasOwn(TransitionPropsValidators, key) &&
key !== 'tag' &&
key !== 'moveClass'
) {
filteredProps[key] = (rawProps as any)[key]
}
}

prevChildren = []
if (children) {
for (let i = 0; i < children.length; i++) {
Expand Down Expand Up @@ -166,8 +179,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
warn(`<TransitionGroup> children must be keyed.`)
}
}

return createVNode(tag, null, children)
return createVNode(tag, tag === Fragment ? null : filteredProps, children)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused. This seems to change the props from null to filteredProps. If the old value was null then it doesn't seem this is where the spurious props were being applied originally. I'm not sure how passing extra props here would help.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the fix isn't changing from null to filteredProps - it's changing
from passing all props (including invalid HTML attributes like
transition props) to passing only valid HTML attributes.

The condition tag === Fragment ? null : filteredProps means:

  • If rendering a Fragment: pass null (no props needed)
  • If rendering an actual HTML element: pass only the filtered, valid
    HTML props

This prevents invalid HTML attributes like name="fade" or
duration="300" from appearing on the DOM element.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the original code is this:

return createVNode(tag, null, children)

That isn't passing all props, it's passing null. The new code passes more props, not fewer.

I believe the changes to this file are incorrect and should be reverted.

Copy link
Author

@ZKunZhang ZKunZhang Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the original code is this:

return createVNode(tag, null, children)

That isn't passing all props, it's passing null. The new code passes more props, not fewer.

I believe the changes to this file are incorrect and should be reverted.

Thank you for your guidance, and this line of code has awakened me to the issue — I've identified a fundamental flaw in my previous approach to fixing the issue. The core problem is that Vue's automatic fallthrough mechanism fails to properly handle the declared props of TransitionGroup, causing properties that should be filtered by the component to erroneously appear in the final HTML. The runtime fallthrough mechanism malfunctions, resulting in transition-related attributes (such as name="fade") being incorrectly rendered into the HTML. The same issue occurs in SSR environments, generating HTML with invalid attributes. Therefore, my previous method of manually filtering attributes within each component was incorrect.

There is a critical flaw in how TransitionGroup handles props:

  1. A dynamic deletion operation delete t.props.mode is executed in the decorate function
  2. This breaks the fallthrough mechanism: Vue's setFullProps function relies on hasOwn(options, camelKey) to determine which properties are declared props
  3. The end result: The deleted mode and all other transition properties fail to be correctly identified as declared props, causing them to erroneously enter the attrs object

The correct architecture-level fix should be:

  • Rebuild the props definition for TransitionGroup
  • Use extend instead of object spreading (to meet ESBuild requirements)
  • Exclude mode during the definition phase to avoid subsequent deletion operations

It's important to note that during SSR compilation, all attributes are directly compiled into the generated code. Since SSR is processed at compile time rather than runtime, the runtime fallthrough mechanism does not take effect during SSR compilation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the original code is this:

return createVNode(tag, null, children)

That isn't passing all props, it's passing . The new code passes more props, not fewer.null

I believe the changes to this file are incorrect and should be reverted.

I double-checked and realized I had indeed misidentified the root cause of the issue. This led me to fix a file that didn’t need fixing, but I have now reverted that change. 😣

}
},
})
Expand Down