Skip to content

Conversation

ZKunZhang
Copy link

@ZKunZhang ZKunZhang commented Sep 15, 2025

Summary:

  • Fixed TransitionGroup passing transition-related props to DOM elements incorrectly
  • Added prop filtering to exclude transition-specific and component-specific attributes (tag, moveClass)
  • Resolved W3C validation errors with invalid HTML attributes

Changes:
Filtered props before passing to createVNode, excluding transition props and tag/moveClass while retaining valid HTML attributes.

const filteredProps: Record<string, any> = {}
for (const key in rawProps) {
  if (!(key in TransitionPropsValidators) && key !== 'tag' && key !== 'moveClass') {
    filteredProps[key] = (rawProps as any)[key]
  }
}
return createVNode(tag, filteredProps, children)

Fixes: #13037, W3C validation errors. No breaking changes.

Summary by CodeRabbit

  • Bug Fixes
    • TransitionGroup now strips framework-specific props from its root element to avoid leaking internal attributes into HTML and reduce browser console warnings.
    • Server-side rendering now excludes private/internal props from emitted attributes, preventing unintended attributes in SSR output.
    • Public API and runtime behavior remain unchanged aside from attribute sanitization.

…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.
Copy link

coderabbitai bot commented Sep 15, 2025

Walkthrough

Add prop-sanitization for TransitionGroup at runtime and SSR: runtime filters out Transition-specific props plus 'tag' and 'moveClass' before creating the root vnode; SSR transform excludes private name attribute from props emitted for server-rendered TransitionGroup nodes. No public API changes. (≤50 words)

Changes

Cohort / File(s) Summary of Changes
TransitionGroup runtime prop sanitization
packages/runtime-dom/src/components/TransitionGroup.ts
Import hasOwn, iterate rawProps to build filteredProps by excluding Transition prop validator keys and the keys 'tag' and 'moveClass', and pass filteredProps (or null for Fragment tag) to createVNode for the root element.
SSR transform: exclude private name prop
packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
When computing otherProps for buildProps, filter out the private name attribute (p.name === 'name') in addition to the already-handled tag prop (only for Attribute nodes). Added an inline comment noting the private-props exclusion.

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
Loading
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`)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

scope: transition, :hammer: p3-minor-bug, scope:hydration

Poem

I hop through props with careful grace,
Pluck stray keys from the HTML space.
ULs and lists now wear no shame,
SSR sings clean without the name.
A twitch of whiskers—attributes tame. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Linked Issues Check ❓ Inconclusive The PR implements the core runtime fix by filtering TransitionGroup props before creating the root VNode and updates the SSR transform to exclude the private 'name' prop when the node prop is an Attribute, which directly targets issue #13037's symptom. However, PR discussion and Playground evidence indicate the template-to-SSR compilation path may still emit the 'name' attribute in generated HTML and there are no SSR-template-focused tests or a clear SSR reproduction included that prove the fix across all SSR compilation paths. Given that remaining uncertainty, I cannot conclusively determine the linked issue is fully resolved in all SSR cases. Provide a minimal SSR template reproduction (Playground or test) demonstrating TransitionGroup name+tag output, add targeted SSR-template compiler/unit tests that assert the 'name' prop is not emitted in generated HTML, and adjust the SSR compilation path as needed until those tests pass to fully close issue #13037.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely summarizes the primary fix—filtering TransitionGroup-specific props to prevent invalid HTML attributes—and directly reflects the changes made in runtime-dom and the SSR transform. It is specific, focused on the main issue, and readable for teammates scanning the history.
Out of Scope Changes Check ✅ Passed All modified files are directly related to TransitionGroup prop handling (runtime-dom component and SSR transform) and there are no changes to public/exported signatures or unrelated modules, so the PR content appears in-scope for the linked issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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; consider hasOwn and add a quick guardrail.

  • Use hasOwn instead of in 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

📥 Commits

Reviewing files that changed from the base of the PR and between b555f02 and 92e31b0.

📒 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 属性问题
Copy link
Contributor

@skirtles-code skirtles-code left a 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)
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. 😣

@ZKunZhang
Copy link
Author

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?

Sorry, this does affect SSR after all. I had a misunderstanding of the code, and I'll fix this part again.

@ZKunZhang
Copy link
Author

ZKunZhang commented Sep 17, 2025

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?

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?
P.S. The repository address where I used the aforementioned build artifacts is deploy-preview-13894-ssr-test.git
image

@skirtles-code
Copy link
Contributor

The SSR example you linked appears to be using a render function, so it won't match what happens in the Playground.

For components that use templates, Vue compiles a separate SSR version that bypasses VNodes and just generates the HTML string. You can see that version in the Playground by clicking on the SSR tab.

Playground

It would appear that this is where the spurious name attribute is being added to the output.

…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.
Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between ba3ac8a and d65747a.

📒 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 drops v-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,不该透传)
Copy link
Member

Choose a reason for hiding this comment

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

中文注释不太好,其他贡献者可能看不懂。

Copy link
Author

Choose a reason for hiding this comment

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

sorry 这边发现还有其他属性没有处理
image
我需要晚上回家处理下。

Copy link
Author

Choose a reason for hiding this comment

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

中文注释不太好,其他贡献者可能看不懂。

谢谢您的督促,已经调整好了。

@edison1105 edison1105 added scope: transition 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. wait changes labels Sep 18, 2025
Copy link
Contributor

@skirtles-code skirtles-code left a 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')
Copy link
Contributor

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.

Copy link
Author

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.

image You're absolutely right. I only noticed this issue after fixing the previous one. I'll figure out how to address it—it seems like this isn't the right place to handle it, so I'll try another approach. Thank you for your oversight.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. scope: transition wait changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Invalid HTML with TransitionGroup name="list" tag="ul"
3 participants