Skip to content

fix(custom-element): component styles are not merged with 2nd argument styles #13792

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

LeeSamFong
Copy link

@LeeSamFong LeeSamFong commented Aug 21, 2025

This pr fixes an issue where component styles are not merged with second argument styles. It also fixes an issue where HMR causes style errors when modifying the contents of a component's <style> tag.

To avoid breaking changes, this pr adds a new extraStyles property to replace the original styles property. I recommend renaming the styles property in documentation to extraStyles, as the styles property has existed for nearly a year.

https://vuejs.org/api/custom-elements.html
image

Summary by CodeRabbit

  • New Features
    • Custom elements can include additional CSS via an extraStyles option; these are added as separate style blocks in the shadow DOM and persist across updates.
  • Public API
    • defineCustomElement accepts an optional options parameter with extraStyles?: string[] to supply additional shadow-root CSS.
  • Tests
    • Updated and added tests validate multiple style blocks, HMR behavior, and prevention of duplicate extraStyles across child updates.

Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

Adds an extraStyles?: string[] option to custom element APIs and defineCustomElement; merges component styles with extraStyles for injection into shadow roots, updates runtime warnings, test cases, and ensures extraStyles persist across renders and HMR while component styles are replaced.

Changes

Cohort / File(s) Summary
Custom element runtime & API
packages/runtime-dom/src/apiCustomElement.ts
Adds extraStyles?: string[] to CustomElementOptions; stores _extraStyles on VueElement; merges styles with extraStyles for injection; updates non-shadowRoot warning to consider extraStyles; adjusts HMR/style replacement logic to preserve extraStyles.
Define helper signature
packages/runtime-dom/src/defineCustomElement.ts
Changes defineCustomElement(def)defineCustomElement(def, options) and threads extraStyles through element definition/public API surface.
Tests
packages/runtime-dom/__tests__/customElement.spec.ts
Updates tests to pass extraStyles, assert multiple <style> blocks on initial render and after HMR, and adds a test ensuring extraStyles are not duplicated by child injections and persist across child HMR.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant App as Application
  participant API as defineCustomElement
  participant CE as VueElement
  participant SR as ShadowRoot

  Dev->>App: import defineCustomElement
  App->>API: defineCustomElement(def, { extraStyles })
  API->>CE: create class storing def + extraStyles
  App->>CE: attach element
  CE->>SR: attach shadowRoot
  CE->>CE: compute fullStyles = styles + extraStyles
  loop append styles
    CE->>SR: append <style> (component or extra)
  end
Loading
sequenceDiagram
  autonumber
  participant HMR as HMR Runtime
  participant CE as VueElement
  participant SR as ShadowRoot

  HMR-->>CE: update component styles (replace)
  CE->>SR: replace component style blocks
  CE->>SR: preserve appended extraStyles blocks
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

:hammer: p3-minor-bug, scope: custom elements

Poem

I stitched a second thread of green,
Beside the red, a tidy scene.
When HMR trades red for blue,
My extra trim remains true.
Hop—styles snug in shadowed light. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 2

🧹 Nitpick comments (1)
packages/runtime-dom/src/apiCustomElement.ts (1)

53-59: API addition looks good; please clarify behavior in docs.

Adding extraStyles?: string[] to CustomElementOptions is a sensible non-breaking way to merge author-supplied CSS with component-emitted styles. Please add a short JSDoc explaining:

  • Merge order relative to component styles (extraStyles appended after the component’s own styles on initial mount).
  • That extraStyles are applied only when shadowRoot !== false.
  • HMR behavior (component styles are replaced on reload; extraStyles persist).

This will help avoid confusion with the existing styles field on component defs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a48ffda and d99c785.

📒 Files selected for processing (2)
  • packages/runtime-dom/__tests__/customElement.spec.ts (2 hunks)
  • packages/runtime-dom/src/apiCustomElement.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/runtime-dom/__tests__/customElement.spec.ts (1)
packages/runtime-dom/src/apiCustomElement.ts (2)
  • defineCustomElement (168-186)
  • VueElement (203-699)
🪛 ast-grep (0.38.6)
packages/runtime-dom/__tests__/customElement.spec.ts

[warning] 858-858: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = <my-el-with-styles></my-el-with-styles>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 858-858: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: container.innerHTML = <my-el-with-styles></my-el-with-styles>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

⏰ 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 (3)
packages/runtime-dom/src/apiCustomElement.ts (2)

224-227: Snapshotting extraStyles across async resolution is correct.

Initializing a private _extraStyles from this._def.extraStyles ensures the extra styles persist when the async wrapper resolves and this._def is replaced with the inner component. This mirrors the existing _nonce pattern and avoids losing extra options during async resolution. LGTM.


396-404: Dev warning now covers extraStyles — good catch.

Extending the warning to trigger when styles || this._extraStyles while shadowRoot: false prevents silent failures when users rely solely on extraStyles. Looks correct.

packages/runtime-dom/__tests__/customElement.spec.ts (1)

855-857: Test covers new API surface.

Passing extraStyles via the second argument to defineCustomElement and asserting presence in shadow DOM verifies the intended merge path. Nice.

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

♻️ Duplicate comments (2)
packages/runtime-dom/src/apiCustomElement.ts (1)

594-603: Fix validated: inject extraStyles only for the host; prevents duplicates and child HMR removal

The new fullStyles construction and the !owner guard correctly ensure:

  • Host receives component styles + extraStyles once.
  • Child injections bring only their own styles.
  • Child HMR no longer wipes host extra CSS.

This addresses the previously raised duplication/removal issue.

Also applies to: 612-616

packages/runtime-dom/__tests__/customElement.spec.ts (1)

877-916: Added guard test: no duplication via child injection; persists across child HMR — LGTM

This directly validates the earlier bugfix: extraStyles appears once, isn’t re-injected by children, and isn’t removed on child HMR. Perfect.

🧹 Nitpick comments (2)
packages/runtime-dom/src/apiCustomElement.ts (1)

53-59: New extraStyles option: please deprecate second-arg styles and guide migration

Great addition. To avoid future confusion where second-argument styles overwrite component-compiled styles, explicitly deprecate the CustomElementOptions.styles field and steer users to extraStyles. This is a doc-only change plus a DEV warning to help migration.

Apply this small annotation so editors/TS hint the preferred option:

 export interface CustomElementOptions {
-  styles?: string[]
+  /**
+   * @deprecated Use `extraStyles` to avoid overriding component-compiled styles.
+   */
+  styles?: string[]
   extraStyles?: string[]
   shadowRoot?: boolean
   nonce?: string
   configureApp?: (app: App) => void
 }

Additionally, consider a DEV-time warning in defineCustomElement when extraOptions.styles is used so users get a clear message at runtime. (Shown here for reference; this change is outside the current hunk.)

// Inside defineCustomElement(...) just after computing Comp
if (__DEV__ && extraOptions && (extraOptions as any).styles) {
  warn(
    'defineCustomElement second-argument "styles" overrides component styles. ' +
      'Use "extraStyles" to append without replacing.',
  )
}

If you want, I can follow up with a PR comment updating the docs page to reflect this deprecation and the new guidance.

packages/runtime-dom/__tests__/customElement.spec.ts (1)

838-846: Optional: add tests for (1) DEV warning with shadowRoot: false + extraStyles and (2) nonce persistence through HMR

Nice coverage in styles suite. Two quick additions would lock in behavior:

  • When shadowRoot: false and extraStyles provided, assert the DEV warning fires (you already warn for styles or extraStyles).
  • After HMR on a host using nonce, assert all injected style tags (including extraStyles) retain the nonce.

I can add these as separate tests if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d99c785 and f79814c.

📒 Files selected for processing (2)
  • packages/runtime-dom/__tests__/customElement.spec.ts (2 hunks)
  • packages/runtime-dom/src/apiCustomElement.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/runtime-dom/__tests__/customElement.spec.ts (1)
packages/runtime-dom/src/apiCustomElement.ts (2)
  • defineCustomElement (168-186)
  • VueElement (203-704)
🪛 ast-grep (0.38.6)
packages/runtime-dom/__tests__/customElement.spec.ts

[warning] 858-858: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = <my-el-with-styles></my-el-with-styles>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 858-858: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: container.innerHTML = <my-el-with-styles></my-el-with-styles>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 895-895: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = <my-el-extra-child></my-el-extra-child>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 895-895: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: container.innerHTML = <my-el-extra-child></my-el-extra-child>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

🔇 Additional comments (4)
packages/runtime-dom/src/apiCustomElement.ts (2)

399-404: DEV warning now considers extraStyles — LGTM

Including this._extraStyles in the shadowRoot: false warning tightens correctness and developer feedback. No action needed.


224-227: No action needed: TS configuration guarantees safe field-initializer order

With "useDefineForClassFields": false and a "target" of "es2016" in your tsconfig, TypeScript down-levels class fields into assignments in the constructor. Parameter properties (like this._def) are assigned immediately after super() and before any of those generated field assignments. Therefore, using

_extraStyles?: string[] = this._def.extraStyles

is safe under the current configuration, and no refactor is required.

packages/runtime-dom/__tests__/customElement.spec.ts (2)

855-862: Tests adopt extraStyles and assert merged ordering — LGTM

Switching to defineCustomElement(def, { extraStyles }) and expecting both blocks verifies the new API and merge behavior. Assertions match the prepend order.


870-875: HMR preserves extraStyles while replacing component styles — LGTM

Good coverage of the host HMR path, ensuring extraStyles persist across reloads.

@edison1105
Copy link
Member

edison1105 commented Aug 22, 2025

What if the user is using extraOptions.styles to override the styles inside the component? At least the current behavior is to remove Comp.styles, and the styles that ultimately take effect are extraOptions.styles. If we choose to merge the two styles, the styles in Comp.styles that are not overridden by extraOptions.styles will still take effect. This would also be a breaking change.

If merging is necessary, it isn’t mandatory to change extraOptions.styles to extraOptions.extraStyles. It should be possible to extract extraOptions.styles and save it to _extraStyles at

if (isPlainObject(Comp)) extend(Comp, extraOptions)

and merge two objects without using extend.

@LeeSamFong
Copy link
Author

In the documentation, the description of extraOptions.styles is "injected" rather than "override", so I assume that when styles was originally designed, it was intended for additional CSS that needed to be added to the shadow root.

In the current PR, extraOptions.styles is preserved to keep the override behavior effective and to avoid a breaking change. A new option, extraOptions.extraStyles, is introduced to handle the injected case.

So I think the documentation might need some additional clarification? Something like:

styles: ... to override component styles ...
extraStyles: an array of inlined CSS strings for providing CSS that should be injected into the element's shadow root.

That said, afte discussion, it feels a bit like this has turned into a feature... whereas my original intention was just to fix a bug...

@LeeSamFong
Copy link
Author

LeeSamFong commented Aug 22, 2025

Also, most users probably don’t need to override styles through the second argument, just like you wouldn’t expect to provide a parameter for overriding styles in createApp().

If there is such a need, it can be achieved in the following way:

defineCustomElement({
  ...App,
  styles: ['css']
})

So would it be better to mark extraOptions.styles as @deprecated?🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants