Skip to content

fix(runtime-core): resolve kebab-case slot names from in-DOM templates#14302

Merged
edison1105 merged 6 commits intomainfrom
edison/fix/inDOMSlotName
Jan 19, 2026
Merged

fix(runtime-core): resolve kebab-case slot names from in-DOM templates#14302
edison1105 merged 6 commits intomainfrom
edison/fix/inDOMSlotName

Conversation

@edison1105
Copy link
Copy Markdown
Member

@edison1105 edison1105 commented Jan 9, 2026

closes #14300

Summary by CodeRabbit

  • Bug Fixes

    • Improved kebab-case slot name resolution so camelCase slot references correctly match kebab-case slot names in browser/dev/prod contexts; exact name matches still take precedence.
  • Tests

    • Added comprehensive tests covering slot name resolution, priority behavior, and scoped rendering across development and production modes.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 9, 2026

📝 Walkthrough

Walkthrough

Centralizes slots proxy behavior, adds kebab-case (hyphenated) slot name lookup for in‑DOM/browser contexts in renderSlot and proxies, updates KeepAlive condition from BROWSER to GLOBAL, and adds tests covering camelCase↔kebab-case slot resolution and exact‑match precedence across dev/prod modes.

Changes

Cohort / File(s) Summary
Tests
packages/runtime-core/__tests__/componentSlots.spec.ts
Adds tests (~+116 lines) for in‑DOM kebab‑case slot lookup, camelCase↔kebab‑case resolution, exact‑match precedence, renderSlot integration, and dev/prod scenarios (uses renderSlot and setCurrentRenderingInstance).
Slots proxy / component
packages/runtime-core/src/component.ts
Replaces getSlotsProxy with createSlotsProxyHandlers and uses a Proxy from that handler in setup context; aligns dev/browser slot proxy behavior and imports hyphenate for kebab‑case resolution.
RenderSlot helper
packages/runtime-core/src/helpers/renderSlot.ts
Extends slot lookup to consider slots[hyphenate(name)] in global/browser builds (in‑DOM template kebab‑case fallback) while preserving SSR and fallback behavior.
KeepAlive behavior
packages/runtime-core/src/components/KeepAlive.ts
Changes a conditional from __BROWSER__ to __GLOBAL__ when attaching the keep‑alive storageContainer under dev/global builds.

Sequence Diagram(s)

sequenceDiagram
  participant Component
  participant renderSlot
  participant SlotsProxy
  participant Hyphenate

  Component->>renderSlot: renderSlot(name, slots, props, ...)
  renderSlot->>SlotsProxy: lookup slots[name]
  alt slot found
    SlotsProxy-->>renderSlot: slot fn
  else not found and __GLOBAL__ (browser/in‑DOM)
    renderSlot->>Hyphenate: hyphenate(name)
    Hyphenate-->>renderSlot: hyphenatedName
    renderSlot->>SlotsProxy: lookup slots[hyphenatedName]
    SlotsProxy-->>renderSlot: slot fn (if exists)
  end
  renderSlot-->>Component: invoke slot fn / render content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nibble hyphens under moonlit code,
Turning camel hums to kebab mode.
Slots once hidden by HTML’s small face,
Now hop back home to their rightful place.
A tiny rabbit fixed a tiny case.

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The KeepAlive.ts change replacing BROWSER with GLOBAL appears unrelated to the slot name resolution objective and falls outside the scope of fixing kebab-case slot names. Clarify whether the KeepAlive.ts condition change is intentional and related to the slot fix, or separate it into a distinct PR with appropriate justification.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the PR: fixing kebab-case slot name resolution from in-DOM templates to resolve the case-sensitivity issue reported in issue #14300.
Linked Issues check ✅ Passed The changes implement the necessary solution for issue #14300 [#14300]: updates to renderSlot.ts add kebab-case lookup via hyphenation, component.ts refactors slot proxy handling to support case-insensitive resolution, and test coverage validates the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d31b716 and d928ed1.

📒 Files selected for processing (3)
  • packages/runtime-core/__tests__/componentSlots.spec.ts
  • packages/runtime-core/src/component.ts
  • packages/runtime-core/src/helpers/renderSlot.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/runtime-core/tests/componentSlots.spec.ts
  • packages/runtime-core/src/component.ts
  • packages/runtime-core/src/helpers/renderSlot.ts
⏰ 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). (1)
  • GitHub Check: test / e2e-test

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jan 9, 2026

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 103 kB (+67 B) 39.1 kB (+19 B) 35.2 kB (-2 B)
vue.global.prod.js 162 kB (+67 B) 59.1 kB (+25 B) 52.6 kB (-18 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 47.1 kB (+58 B) 18.4 kB (+22 B) 16.9 kB (+17 B)
createApp 55.2 kB (+58 B) 21.4 kB (+23 B) 19.6 kB (+19 B)
createSSRApp 59.5 kB (+58 B) 23.2 kB (+23 B) 21.2 kB (+17 B)
defineCustomElement 60.8 kB (+58 B) 23.2 kB (+19 B) 21.1 kB (+20 B)
overall 69.6 kB (+58 B) 26.7 kB (+23 B) 24.4 kB (+108 B)

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jan 9, 2026

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@14302
npm i https://pkg.pr.new/@vue/compiler-core@14302
yarn add https://pkg.pr.new/@vue/compiler-core@14302.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@14302
npm i https://pkg.pr.new/@vue/compiler-dom@14302
yarn add https://pkg.pr.new/@vue/compiler-dom@14302.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@14302
npm i https://pkg.pr.new/@vue/compiler-sfc@14302
yarn add https://pkg.pr.new/@vue/compiler-sfc@14302.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@14302
npm i https://pkg.pr.new/@vue/compiler-ssr@14302
yarn add https://pkg.pr.new/@vue/compiler-ssr@14302.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@14302
npm i https://pkg.pr.new/@vue/reactivity@14302
yarn add https://pkg.pr.new/@vue/reactivity@14302.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@14302
npm i https://pkg.pr.new/@vue/runtime-core@14302
yarn add https://pkg.pr.new/@vue/runtime-core@14302.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@14302
npm i https://pkg.pr.new/@vue/runtime-dom@14302
yarn add https://pkg.pr.new/@vue/runtime-dom@14302.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@14302
npm i https://pkg.pr.new/@vue/server-renderer@14302
yarn add https://pkg.pr.new/@vue/server-renderer@14302.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@14302
npm i https://pkg.pr.new/@vue/shared@14302
yarn add https://pkg.pr.new/@vue/shared@14302.tgz

vue

pnpm add https://pkg.pr.new/vue@14302
npm i https://pkg.pr.new/vue@14302
yarn add https://pkg.pr.new/vue@14302.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@14302
npm i https://pkg.pr.new/@vue/compat@14302
yarn add https://pkg.pr.new/@vue/compat@14302.tgz

commit: bfa00cc

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In @packages/runtime-core/src/component.ts:
- Around line 1114-1124: The Proxy get trap in createSlotsProxyHandlers calls
hyphenate(key) but doesn't guard against symbol keys, causing runtime
exceptions; update the get handler in createSlotsProxyHandlers to first detect
if key is a symbol and in that case return target[key] directly (or use
Reflect.get) without calling hyphenate, otherwise continue existing logic
(including the __BROWSER__ hyphenate fallback for string keys) so symbol
property accesses on InternalSlots are safe.
🧹 Nitpick comments (1)
packages/runtime-core/__tests__/componentSlots.spec.ts (1)

467-579: Improve test isolation: restore previous __BROWSER__/__DEV__ values instead of forcing false/true.
This makes the suite robust to different harness defaults and future refactors.

Proposed patch
 describe('in-DOM template kebab-case slot name resolution', () => {
+  let prevBrowser: boolean
   beforeEach(() => {
+    prevBrowser = __BROWSER__
     __BROWSER__ = true
   })

   afterEach(() => {
-    __BROWSER__ = false
+    __BROWSER__ = prevBrowser
   })

   test('should resolve camelCase slot access to kebab-case via slots (PROD)', () => {
-    __DEV__ = false
+    const prevDev = __DEV__
+    __DEV__ = false
     try {
       const Comp = {
         setup(_: any, { slots }: any) {
           // Access with camelCase, but slot is passed with kebab-case
           return () => slots.dropdownRender()
         },
       }
@@
       createApp(App).mount(root)
       expect(serializeInner(root)).toBe('dropdown content')
     } finally {
-      __DEV__ = true
+      __DEV__ = prevDev
     }
   })
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 623bfb2 and 3188f24.

📒 Files selected for processing (3)
  • packages/runtime-core/__tests__/componentSlots.spec.ts
  • packages/runtime-core/src/component.ts
  • packages/runtime-core/src/helpers/renderSlot.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/runtime-core/src/helpers/renderSlot.ts (1)
packages/shared/src/general.ts (1)
  • hyphenate (118-120)
packages/runtime-core/src/component.ts (2)
packages/runtime-core/src/componentSlots.ts (1)
  • InternalSlots (34-36)
packages/shared/src/general.ts (1)
  • hyphenate (118-120)
🔇 Additional comments (5)
packages/runtime-core/src/helpers/renderSlot.ts (2)

17-17: Good addition: hyphenate import is appropriately scoped to the new lookup.


56-58: Slot resolution behavior looks correct (exact name first, kebab-case fallback only in browser).
This matches in-DOM template constraints while keeping SSR unaffected.

packages/runtime-core/__tests__/componentSlots.spec.ts (1)

14-15: Nice: tests explicitly cover renderSlot + rendering-instance wiring.

packages/runtime-core/src/component.ts (2)

69-70: hyphenate import is a good fit for the intended in-DOM kebab-case behavior.


1166-1173: Wiring looks good: consistent slots proxying in dev, and browser-only in prod to minimize overhead.

Also applies to: 1181-1187

Comment thread packages/runtime-core/src/component.ts
Copy link
Copy Markdown

@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/runtime-core/src/component.ts (1)

1114-1127: Well-structured kebab-case slot resolution with correct priority.

The proxy handler correctly:

  • Tracks slot access in dev mode
  • Prioritizes exact matches over kebab-case fallback
  • Guards hyphenation with __BROWSER__ and string type check
  • Centralizes slot proxy behavior for maintainability
♻️ Optional: Make symbol key handling more explicit

The current logic using || could return false instead of undefined when accessing a non-existent slot with a symbol key (an edge case since slots use string names). For clarity:

 const createSlotsProxyHandlers = (
   instance: ComponentInternalInstance,
 ): ProxyHandler<InternalSlots> => ({
   get(target, key: string | symbol) {
     if (__DEV__) {
       track(instance, TrackOpTypes.GET, '$slots')
     }
-    // in-DOM templates use kebab-case slot names, only relevant in browser
-    return (
-      target[key as string] ||
-      (__BROWSER__ && typeof key === 'string' && target[hyphenate(key)])
-    )
+    // in-DOM templates use kebab-case slot names, only relevant in browser
+    return (
+      target[key as string] ??
+      (__BROWSER__ && typeof key === 'string' ? target[hyphenate(key)] : undefined)
+    )
   },
 })

Using ?? (nullish coalescing) ensures undefined is returned for missing slots rather than false. However, this is unlikely to affect real usage since slots use string keys.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3188f24 and 24e3ce0.

📒 Files selected for processing (1)
  • packages/runtime-core/src/component.ts
🔇 Additional comments (3)
packages/runtime-core/src/component.ts (3)

69-69: LGTM: Import is necessary for kebab-case resolution.

The hyphenate import from @vue/shared is correctly added to support converting camelCase slot names to kebab-case for in-DOM template slot resolution.


1169-1175: LGTM: Dev mode slots proxy correctly uses the new handler.

The lazy initialization pattern is consistent with attrsProxy, and the use of createSlotsProxyHandlers ensures both slot access tracking and kebab-case resolution (when __BROWSER__ is true) work correctly in development mode.


1185-1187: LGTM: Production mode efficiently handles browser-specific slot resolution.

The conditional proxy creation is performance-conscious:

  • Browser contexts get kebab-case resolution via proxy (no tracking overhead since __DEV__ is false)
  • SSR contexts avoid proxy overhead entirely by using plain instance.slots

This aligns perfectly with the fix objective—handling browser HTML attribute lowercasing while keeping SSR efficient.

Copy link
Copy Markdown

@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/runtime-core/src/component.ts (1)

1114-1127: Consider improving type safety in the proxy get handler.

The proxy handler receives key: string | symbol but the first access path target[key as string] doesn't guard against symbols. While InternalSlots is typed as { [name: string]: Slot | undefined } (so symbols would return undefined), it's clearer to be explicit.

Additionally, this logic duplicates the kebab-case resolution from renderSlot.ts. If the logic needs to change in the future, both locations must be updated.

Optional: More explicit type handling
 const createSlotsProxyHandlers = (
   instance: ComponentInternalInstance,
 ): ProxyHandler<InternalSlots> => ({
   get(target, key: string | symbol) {
     if (__DEV__) {
       track(instance, TrackOpTypes.GET, '$slots')
     }
+    if (typeof key !== 'string') {
+      return target[key as any]
+    }
     // in-DOM templates use kebab-case slot names, only relevant in global builds
     return (
-      target[key as string] ||
-      (__GLOBAL__ && typeof key === 'string' && target[hyphenate(key)])
+      target[key] ||
+      (__GLOBAL__ && target[hyphenate(key)])
     )
   },
 })
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24e3ce0 and bcbb196.

📒 Files selected for processing (3)
  • packages/runtime-core/__tests__/componentSlots.spec.ts
  • packages/runtime-core/src/component.ts
  • packages/runtime-core/src/helpers/renderSlot.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime-core/tests/componentSlots.spec.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/runtime-core/src/helpers/renderSlot.ts (2)
packages/runtime-core/src/component.ts (1)
  • slots (1168-1176)
packages/shared/src/general.ts (1)
  • hyphenate (118-120)
packages/runtime-core/src/component.ts (2)
packages/runtime-core/src/componentSlots.ts (1)
  • InternalSlots (34-36)
packages/shared/src/general.ts (1)
  • hyphenate (118-120)
🔇 Additional comments (5)
packages/runtime-core/src/helpers/renderSlot.ts (2)

17-17: LGTM: Import addition is appropriate.

The hyphenate import is necessary for the kebab-case slot name resolution in line 57.


56-57: Edge cases for kebab-case slot name fallback are properly handled.

The code correctly prioritizes exact matches before the kebab-case fallback, and the __GLOBAL__ guard appropriately limits this feature to global builds. The hyphenate() function properly handles all edge cases:

  • Empty strings, special characters, and already-kebab-cased names are handled correctly by the regex pattern /\B([A-Z])/g
  • When both fooBar and foo-bar slots are defined, exact match (fooBar) takes precedence, as confirmed by existing tests
packages/runtime-core/src/component.ts (3)

69-69: LGTM: Import addition is appropriate.

The hyphenate import is required for the new createSlotsProxyHandlers function.


1169-1175: LGTM: Dev mode slots proxy implementation.

The lazy initialization pattern with caching is appropriate for development mode, and using the centralized createSlotsProxyHandlers ensures consistent behavior.


1185-1187: Good optimization: Conditional proxy in production.

The conditional proxy based on __GLOBAL__ is a smart optimization—non-global builds (ESM, bundler) skip the proxy overhead since kebab-case resolution is only needed for in-DOM templates in global/browser builds.

Note: Dev mode always uses the proxy (see lines 1169-1175), but the hyphenation logic is still gated by __GLOBAL__ inside the handler, maintaining consistent behavior across modes.

@edison1105 edison1105 added ready to merge The PR is ready to be merged. 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. labels Jan 9, 2026
Copy link
Copy Markdown
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'm not necessarily opposed to this, but I think the decision to support case conversion for slot names is more than just a minor fix. I'm not sure exactly what ready to merge indicates, but personally I feel this needs more scrutiny from the team before committing to supporting this.

This isn't a new problem. We've definitely discussed this several times on Vue Land, usually when someone asks about the best naming convention for slots. e.g.:

I haven't been able to find any previous discussion of it on GitHub, but I'd be surprised if some form of case conversion or normalisation hasn't been considered before.

Adding partial support for this could be the thin end of the wedge and alternative approaches are available.

Comment thread packages/runtime-core/src/component.ts Outdated
@edison1105 edison1105 added ready for review This PR requires more reviews and removed ready to merge The PR is ready to be merged. labels Jan 10, 2026
@edison1105
Copy link
Copy Markdown
Member Author

@skirtles-code
Thank you for your feedback. To be honest, I'm also quite surprised that this issue has existed for so long without a related issue being filed.

First, to clarify, this problem only occurs in the in-DOM template scenario. As we know, in the browser environment, prop names and v-on event names must use kebab-case—this is an established Vue convention (see the docs). While not explicitly stated, slot names, as part of template attributes, should logically follow the same convention.

Specifically regarding issue #14300, it's essentially the same problem discussed on Discord(you provided above): a-select is a third-party component that internally uses slots.dropdownRender() to render content. This means that no matter how a user writes the slot name in the in-DOM template (dropdownRender or dropdown-render), the component cannot correctly locate the corresponding slot internally. For this situation, I’m not aware of any simpler workaround.

The fix I proposed is actually quite straightforward: when slots[name] is not found, try looking it up again using its kebab-case version. Moreover, this change only affects the Browser Build, so its scope is well-defined. Given the above reasons, I marked it as ready-to-merge.

My intention was not to rush this through, but rather to provide a concrete, low-risk solution for discussion. I'm more than happy to revert the status and assist with a more in-depth exploration.

@KazariEX
Copy link
Copy Markdown
Member

KazariEX commented Jan 10, 2026

Since users are allowed to define slots using camel case, there should at least be a way for them to work correctly in the dom.

Are there any discussions about not converting slot names from kebab case?

@edison1105 edison1105 merged commit 7e554bf into main Jan 19, 2026
24 of 26 checks passed
@edison1105 edison1105 deleted the edison/fix/inDOMSlotName branch January 19, 2026 01:04
@edison1105 edison1105 restored the edison/fix/inDOMSlotName branch January 19, 2026 01:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. ready for review This PR requires more reviews

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slot Name Case Sensitivity Issue in DOM Templates(DOM 模板中插槽名称未转译)

3 participants