Skip to content

feat: add scoped css#103

Merged
maartenbreddels merged 2 commits intowidgetti:masterfrom
maartenbreddelsai:feat_scoped_css
Feb 10, 2026
Merged

feat: add scoped css#103
maartenbreddels merged 2 commits intowidgetti:masterfrom
maartenbreddelsai:feat_scoped_css

Conversation

@maartenbreddelsai
Copy link

@maartenbreddelsai maartenbreddelsai commented Feb 2, 2026

Summary

  • implement scoped CSS handling for Vue SFC styles and css trait
  • add UI tests for scoped styles
  • expose optional scoped trait to override template styles

Issue

Closes #102

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements scoped CSS support for Vue Single File Components (SFCs) in ipyvue, addressing issue #102 where styles leak between components. The implementation follows the httpVueLoader scoping approach and adds a new scoped trait to allow programmatic control of scoping behavior.

Changes:

  • Implements CSS scoping by transforming selectors to include unique data attributes
  • Adds optional scoped trait to VueTemplate to override template-level scoping
  • Includes UI tests verifying scoped styles work correctly and don't affect unscoped elements

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
ipyvue/VueTemplateWidget.py Adds new scoped Bool trait to control CSS scoping behavior
js/src/VueTemplateModel.js Adds scoped property to the JavaScript model defaults
js/src/VueTemplateRenderer.js Implements core scoping logic: selector transformation, scope ID generation, and lifecycle integration
tests/ui/test_template.py Adds comprehensive tests for both template-based and trait-based scoped CSS
.gitignore Adds ipyvue/static/ to ignored directories for build artifacts

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

scopedSelectors.push(`${segments[1]}${scopeSelector}${segments[2] || ''}`);
}
});
const scopedRule = scopedSelectors.join(',') + rule.cssText.substr(rule.selectorText.length);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The substr method is deprecated in JavaScript. Consider using substring or slice instead. Replace rule.cssText.substr(rule.selectorText.length) with rule.cssText.substring(rule.selectorText.length) for better future compatibility.

Suggested change
const scopedRule = scopedSelectors.join(',') + rule.cssText.substr(rule.selectorText.length);
const scopedRule = scopedSelectors.join(',') + rule.cssText.substring(rule.selectorText.length);

Copilot uses AI. Check for mistakes.
const scopedRule = scopedSelectors.join(',') + rule.cssText.substr(rule.selectorText.length);
deleteRule(i);
insertRule(scopedRule, i);
continue;
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The continue statement after modifying the rule is unnecessary and potentially confusing. When you delete and insert a rule at the same index, the loop counter already moves to the next rule naturally. The continue statement is redundant here and could mislead readers into thinking there's a reason to skip further processing. Consider removing it for clarity.

Suggested change
continue;

Copilot uses AI. Check for mistakes.
Comment on lines 123 to 128
} else {
if (style.innerHTML !== css) {
style.innerHTML = css;
}
style.removeAttribute('data-ipyvue-scope');
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

When multiple component instances share the same cssId but have different scoped values, there's a bug in the CSS processing. For example, if instance A uses scoped CSS and instance B (created later with the same cssId) uses unscoped CSS, instance B will reuse the same style element. At line 124, the check style.innerHTML !== css may fail if the CSS content is the same, causing the code to skip updating the innerHTML. Then at line 127, the scope attribute is removed, but the CSS rules in the style element remain scoped from instance A, breaking instance A's styling. To fix this, the condition at line 124 should also check if the current scoping state differs from the desired state, or the innerHTML should always be reset when transitioning from scoped to unscoped.

Copilot uses AI. Check for mistakes.
ipywidgets_runner, page_session: playwright.sync_api.Page
):
def kernel_code():
from test_template import ScopedStyleTemplate
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The module 'test_template' imports itself.
The module 'tests.ui.test_template' imports itself.

Copilot uses AI. Check for mistakes.
ipywidgets_runner, page_session: playwright.sync_api.Page
):
def kernel_code():
from test_template import ScopedCssTemplate
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The module 'test_template' imports itself.
The module 'tests.ui.test_template' imports itself.

Copilot uses AI. Check for mistakes.
):
def kernel_code():
from test_template import ScopedStyleTemplate
import ipyvue as vue
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

This import of module ipyvue is redundant, as it was previously imported on line 7.

Copilot uses AI. Check for mistakes.
):
def kernel_code():
from test_template import ScopedCssTemplate
import ipyvue as vue
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

This import of module ipyvue is redundant, as it was previously imported on line 7.

Copilot uses AI. Check for mistakes.
@maartenbreddelsai
Copy link
Author

Regarding the review note about redundant ipyvue import: that import is inside kernel_code that runs in the notebook kernel context, so it isn't redundant with the test module import; it ensures ipyvue is available in the kernel. We'll ignore that comment.

@maartenbreddelsai
Copy link
Author

Addressed the remaining review notes in latest push: switched to substring, removed redundant continue, and handled scoped→unscoped reuse by resetting style content when prior scope was set.

@maartenbreddelsai
Copy link
Author

maartenbreddelsai commented Feb 3, 2026

Demo Screenshot

Scoped CSS Demo

Results:

  • Widget A (scoped) - green ✅
  • Widget B (unaffected) - black ✅
  • Widget C (scoped via trait) - blue ✅
  • Widget D (unaffected) - black ✅

Both <style scoped> in templates and the css trait with scoped=True work correctly.

@maartenbreddelsai maartenbreddelsai force-pushed the feat_scoped_css branch 3 times, most recently from dff5cff to 9d60d1e Compare February 10, 2026 10:36
.gitignore Outdated
# Compiled javascript
ipyvue/labextension/
ipyvue/nbextension/
ipyvue/static/
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't seem related.

Maarten Breddels AI added 2 commits February 10, 2026 11:55
- Implement scoped CSS handling for Vue SFC styles
- Add scoped trait for css property
- Rewrite CSS selectors with unique data-v-* attributes
- Add example notebook demonstrating scoped styles
- Add UI tests for scoped CSS functionality
Add scoped_css_support trait to VueTemplate that controls whether
<style scoped> in templates is processed. This is a backwards-compatible
change that defaults to False to avoid breaking existing code.

Configuration options:
- Environment variable: IPYVUE_SCOPED_CSS_SUPPORT=1
- Global setting: ipyvue.scoped_css_support = True
- Per-widget: VueTemplate(..., scoped_css_support=True)

The explicit scoped=True/False trait (used with the css trait) still
works independently of this setting.
@maartenbreddels maartenbreddels merged commit 6fae0f4 into widgetti:master Feb 10, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

<style scoped> in Vue SFCs is not processed, causing CSS leakage between components

3 participants