-
-
Notifications
You must be signed in to change notification settings - Fork 8.9k
fix(TransitionGroup): filter out transition-specific props to avoid invalid HTML attributes #13894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix(TransitionGroup): filter out transition-specific props to avoid invalid HTML attributes #13894
Conversation
…nvalid HTML attributes - Add props filtering logic to exclude transition-specific and TransitionGroup-specific props - Prevents invalid HTML attributes like 'name' from being applied to DOM elements - Fixes vuejs#13037 where TransitionGroup with tag='ul' was generating invalid HTML The filtering ensures only valid HTML attributes are passed to the rendered element, resolving W3C validation errors when using TransitionGroup with specific tags.
WalkthroughAdd prop-sanitization for TransitionGroup at runtime and SSR: runtime filters out Transition-specific props plus Changes
Sequence Diagram(s)sequenceDiagram
participant C as TransitionGroup (runtime)
participant F as PropsFilter
participant V as createVNode
participant D as DOMRenderer
C->>F: receive rawProps
F-->>C: return filteredProps (exclude transition-only keys, 'tag', 'moveClass')
C->>V: createVNode(tag or Fragment, filteredProps/null, children)
V->>D: mount/update root element with sanitized attributes
Note over D: Root element receives only valid HTML attributes
sequenceDiagram
participant Compiler as SSR Compiler
participant T as ssrTransformTransitionGroup
participant Builder as buildProps
participant Output as SSR Render
Compiler->>T: visit TransitionGroup node with props
T->>Builder: compute otherProps (filter out 'tag' and private 'name' Attribute)
Builder-->>Output: emit props for SSR with private/transition-only props removed
Note over Output: Server-rendered root has valid attributes (no `name`)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal). Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
packages/runtime-dom/src/components/TransitionGroup.ts (2)
133-145
: Prop sanitization is correct; considerhasOwn
and add a quick guardrail.
- Use
hasOwn
instead ofin
to avoid prototype-chain hits and micro‑optimize.- Minor: precompute a blocked Set if this runs hot (optional).
Apply:
- import { extend } from '@vue/shared' + import { extend, hasOwn } from '@vue/shared' ... - if ( - !(key in TransitionPropsValidators) && + if ( + !hasOwn(TransitionPropsValidators, key) && key !== 'tag' && key !== 'moveClass' ) {Please verify that standard fallthrough attrs (class, style, id, aria-, data-, DOM events) still land on the root element across: tag='ul', tag='div', and default Fragment (should warn/ignore as before).
183-183
: Avoid passing props when tag is Fragment to prevent dev warnings.Passing attrs to a Fragment yields extraneous-attrs warnings in dev. Skip props in that case:
- return createVNode(tag, filteredProps, children) + return createVNode(tag, tag === Fragment ? null : filteredProps, children)Confirm no new warnings are emitted for
<TransitionGroup name="x">
(no tag) while attributes on tagged roots (e.g.,<TransitionGroup tag="ul" id="list">
) still render correctly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/runtime-dom/src/components/TransitionGroup.ts
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/runtime-dom/src/components/TransitionGroup.ts (1)
packages/runtime-dom/src/components/Transition.ts (1)
TransitionPropsValidators
(64-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Redirect rules
- GitHub Check: Header rules
- GitHub Check: Pages changed
- 在 TransitionGroup 中引入 hasOwn 函数,替代原有的属性检查方式 - 确保仅有效的 HTML 属性被传递到渲染的元素中,进一步避免无效的 HTML 属性问题
….com/Husky-Yellow/core into fix/transition-group-props-filtering
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this change has fixed the problem.
Here's a Playground using this PR. The name
attribute is still present in the generated HTML:
As far as I'm aware, the original issue only occurs when SSR is enabled. The extra props are already removed during client-side rendering.
Perhaps I'm missing something. Are you able to provide a Playground (using the Netlify preview of this PR), or even better include some test cases that demonstrate the fix is working correctly?
} | ||
|
||
return createVNode(tag, null, children) | ||
return createVNode(tag, tag === Fragment ? null : filteredProps, children) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- A dynamic deletion operation
delete t.props.mode
is executed in the decorate function - This breaks the fallthrough mechanism: Vue's setFullProps function relies on
hasOwn(options, camelKey)
to determine which properties are declared props - 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.
There was a problem hiding this comment.
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. 😣
Sorry, this does affect SSR after all. I had a misunderstanding of the code, and I'll fix this part again. |
During my local testing, I noticed that the actual effect is inconsistent with the display in the Vue SFC Playground. Currently, I’m not quite clear on how to present local effects through the Playground. However, I have integrated the build artifacts from Playground into the project for server-side rendering, and no issues occurred during this process. What confuses me now is: since the same files are used, why are there discrepancies in the final build results? |
…ibutes - Add filtering for `name` prop in SSR transform logic - Prevents `name` from being rendered as DOM attribute (e.g. `<ul name="list">`) - Fixes vuejs#13037 (invalid HTML from TransitionGroup with custom `tag`) Ensures only valid HTML attributes are passed to the rendered element, resolving W3C validation issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts (1)
35-35
: Use English for inline comments to match repo conventions.Replace the Chinese comment with an English equivalent for consistency:
- // 在处理 TransitionGroup 的属性时,过滤掉 name/tag 等私有 props + // Filter out TransitionGroup-only props (e.g. name/tag) so they are not emitted as HTML attrs
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Redirect rules
- GitHub Check: Header rules
- GitHub Check: Pages changed
🔇 Additional comments (1)
packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts (1)
36-45
: Exclude TransitionGroup-only props from SSR-emitted attrs (static + bound)File: packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
Short: Filter all Transition/TransitionGroup props (static and v-bind with a static arg) so they are not emitted as HTML attributes in SSR.Apply this change (broadens the original
name
-only exclusion and also dropsv-bind
with a static arg that matches an excluded key):- const otherProps = node.props.filter(p => { - // 排除 tag(已单独处理)和 name(私有 props,不该透传) - if ( - p === tag || - (p.type === NodeTypes.ATTRIBUTE && p.name === 'name') - ) { - return false - } - return true - }) + const otherProps = node.props.filter(p => { + if (p === tag) return false + // Exclude Transition/TransitionGroup-only props from SSR-emitted attrs + if (p.type === NodeTypes.ATTRIBUTE) { + return !TRANSITION_GROUP_SSR_EXCLUDE_PROPS.has(p.name) + } + // Exclude v-bind with a static arg that matches excluded keys + if ( + p.type === NodeTypes.DIRECTIVE && + p.name === 'bind' && + p.arg && + p.arg.type === NodeTypes.SIMPLE_EXPRESSION && + p.arg.isStatic && + TRANSITION_GROUP_SSR_EXCLUDE_PROPS.has(p.arg.content) + ) { + return false + } + return true + })Add this helper set near the top of the file (outside this hunk):
// Props that belong to Transition/TransitionGroup and must not be emitted as HTML attrs const TRANSITION_GROUP_SSR_EXCLUDE_PROPS = new Set<string>([ 'name', 'css', 'type', 'duration', 'appear', 'moveClass', 'move-class', 'enter-from-class', 'enter-active-class', 'enter-to-class', 'leave-from-class', 'leave-active-class', 'leave-to-class', 'appear-from-class', 'appear-active-class', 'appear-to-class', // 'tag' is handled separately via `findProp`/`p === tag` ])Note: object spreads (
v-bind="obj"
) with transition keys cannot be filtered statically; consider an SSR helper to strip these keys at render time if parity with runtime is required.Tests to add:
- Static props:
name
,appear
,css
,duration
,move-class
, and representative*-class
props.- Bound with static arg:
:name
,:move-class
.- Spread:
v-bind="{ name: 'x', appear: true }"
(document current behavior or add helper).
const otherProps = node.props.filter(p => p !== tag) | ||
// 在处理 TransitionGroup 的属性时,过滤掉 name/tag 等私有 props | ||
const otherProps = node.props.filter(p => { | ||
// 排除 tag(已单独处理)和 name(私有 props,不该透传) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
中文注释不太好,其他贡献者可能看不懂。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
中文注释不太好,其他贡献者可能看不懂。
谢谢您的督促,已经调整好了。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes to ssrTransformTransitionGroup.ts
should be tested. The existing tests in packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts
look like they could be adapted to test for this.
// 排除 tag(已单独处理)和 name(私有 props,不该透传) | ||
if ( | ||
p === tag || | ||
(p.type === NodeTypes.ATTRIBUTE && p.name === 'name') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to only fix the problem for name
, not for other props such as duration
.
Also, checking for a type
of ATTRIBUTE
will only find static attributes. Attributes bound with v-bind
, e.g. :name="n"
, will still be included.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to only fix the problem for
name
, not for other props such asduration
.Also, checking for a
type
ofATTRIBUTE
will only find static attributes. Attributes bound withv-bind
, e.g.:name="n"
, will still be included.

…tionGroup SSR transform This change improves the SSR transform for TransitionGroup by properly filtering out all transition-specific props that should not be passed through to the rendered element. The implementation: 1. Re-creates the transition props validators structure to mirror runtime logic 2. Filters out both static and dynamic transition-specific props 3. Handles both camelCase and kebab-case prop names 4. Excludes TransitionGroup-specific props like moveClass/move-class 5. Adds comprehensive test coverage for prop filtering This ensures that only relevant props are passed to the rendered element in SSR, matching the behavior of client-side rendering.
- Replace TransitionPropsValidators with direct props definition to exclude 'mode' - Combine BaseTransition props with DOM-specific transition props explicitly - Remove manual props filtering logic by properly defining allowed props - Clean up unnecessary hasOwn import since it's no longer used - Simplify vnode creation by removing filteredProps
Summary:
Changes:
Filtered props before passing to createVNode, excluding transition props and tag/moveClass while retaining valid HTML attributes.
Fixes: #13037, W3C validation errors. No breaking changes.
Summary by CodeRabbit