Skip to content

Styles declared via @CssImport should be isolated within exported component #21272

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
vursen opened this issue Apr 10, 2025 · 3 comments
Open
Labels

Comments

@vursen
Copy link
Contributor

vursen commented Apr 10, 2025

Let's consider this example:

/* src/main/java/org/vaadin/example/MyComponent.java */

@CssImport(value = "my-component.css") // <-----------
public class MyComponent extends Div {
    public MyComponent() {
        add(new Button("Click me!"));
    }
}
/* src/main/java/org/vaadin/example/MyComponentExporter.java */

public class MyComponentExporter extends WebComponentExporter<MyComponent> {
    public MyComponentExporter() {
        super("my-component");
    }

    protected void configureInstance(WebComponent<MyComponent> webComponent, MyComponent component) {};
}

In this example, my-component.css will be injected into both the global DOM and the Shadow DOM of all exported components. Currently, the only way to scope a stylesheet exclusively to a specific exported component is by importing it inside a theme that is applied to the exporter class:

/* src/main/frontend/themes/my-theme/styles.css */

@import '../../my-component.css'; /* <-- injected only into the Shadow DOM of `<my-component>` */
/* src/main/java/org/vaadin/example/MyComponentExporter.java */

@Theme("my-theme") // <---------------------------------------
public class MyComponentExporter extends WebComponentExporter<MyComponent> {
    public MyComponentExporter() {
        super("my-component");
    }

    protected void configureInstance(WebComponent<MyComponent> webComponent, MyComponent component) {};
}

However, the current theming system based on the @Theme annotation is going to be deprecated in Vaadin 25. This creates a need for an alternative approach that allows developers to scope styles to an exported component without relying on themes.

A solution would be to change the default behavior of @CssImport so that styles declared with it are injected only into the Shadow DOM of exported components that include the annotated class in their component hierarchy.

For example, if a PhoneField component is used somewhere within the component hierarchy of LoginFormExporter and AccountFormExporter, its @CssImport should be applied only within the Shadow DOM of those two exported components, remaining fully isolated from the rest of the page and other exporters:

/* src/main/java/org/vaadin/example/PhoneField.java */

@CssImport("phone-field.css") // <-- injected only into `<login-form>` and `<account-form>` components
public class PhoneField extends TextField {
    public PhoneField(String label) {
        super(label);
    }
}
/* src/main/java/org/vaadin/example/LoginForm.java */

public class LoginForm extends FormLayout {
    public LoginForm() {
        add(new PhoneField("Phone"));
    }
}

/* src/main/java/org/vaadin/example/LoginFormExporter.java */

public class LoginFormExporter extends WebComponentExporter<LoginForm> {
    public LoginFormExporter() {
        super("login-form");
    }

    protected void configureInstance(WebComponent<LoginForm> webComponent, LoginForm component) {};
}
/* src/main/java/org/vaadin/example/AccountForm.java */

public class AccountForm extends FormLayout {
    public AccountForm() {
        add(new PhoneField("Phone"));
    }
}

/* src/main/java/org/vaadin/example/AccountFormExporter.java */

public class AccountFormExporter extends WebComponentExporter<AccountForm> {
    public AccountFormExporter() {
        super("account-form");
    }

    protected void configureInstance(WebComponent<AccountForm> webComponent, AccountForm component) {};
}

There is an assumption that developers typically have no need to add styles to the global DOM, except for @font-face, which must be defined globally. This specific declaration can be automatically extracted and moved to the global DOM, as proposed in vaadin/platform#7453. This could presumably be implemented as a Vite plugin.

@vursen
Copy link
Contributor Author

vursen commented Apr 11, 2025

@mshabarov, how complex do you think this task would be? Let me know if anything needs clarification.

@mshabarov
Copy link
Contributor

Thanks for the issue @vursen , we only mention of this annotation for web component is in this article, https://vaadin.com/docs/latest/flow/integrations/embedding/theming#cssimport-in-embedded-applications, but it's the opposite - how to apply global styles to the web component.

We need to investigate what's the effort.

@vursen
Copy link
Contributor Author

vursen commented Apr 29, 2025

Prototype: main...proto-embedded-web-component-css-injection

Overview

  • AbstractUpdateImports.java is refactored to delegate the generation of CSS injection code to a new Vite plugin, flowCSSImportPlugin
  • @CssImport styles are now injected into the Shadow DOM of exported web components also without @Theme.
  • Exported web components no longer inject @CssImport styles into the global DOM. Previously, styles were injected globally, which could break component isolation.
  • Vite plugin automatically extracts @font-face rules and external @import declarations from @CssImport styles and injects them into the global DOM when @Theme is not used. This new behavior is designed to replace document.css for those who are migrating from@Theme to plain @CssImport.
  • Vite plugin also fixes hot reloading for @CssImport. However, if the application uses a theme, the plugin falls back to the legacy theme-based mechanism for CSS injection, which still requires a full page reload.

Vite plugin API

?path

When virtual:flow-css-import?path=path/to/file.css is used without any additional query parameters, it simply resolves to path/to/file.css. This allows Vite to apply its built-in CSS injection mechanism in serve mode and include the file in the CSS bundle in build mode.

?include

virtual:flow-css-import?path=path/to/file.css&include={include}

import cssContent from 'path/to/file.css?inline';

const style = document.createElement('style');
style.textContent = cssContent.toString();
style.setAttribute('include', '{include}');
document.head.appendChild(style);

?theme-for

virtual:flow-css-import?path=path/to/file.css&theme-for={themeFor}

import cssContent from 'path/to/file.css?inline';
import { registerStyles, unsafeCSS } from '@vaadin/vaadin-themable-mixin';

registerStyles('{themeFor}', unsafeCSS(cssContent.toString()), {
  moduleId: 'flow_css_mod_{AUTO_GENERATED_ID}'
});

?module-id

virtual:flow-css-import?path=path/to/file.css&module-id={moduleId}

import cssContent from 'path/to/file.css?inline';
import { registerStyles, unsafeCSS } from '@vaadin/vaadin-themable-mixin';

registerStyles('', unsafeCSS(cssContent.toString()), {
  moduleId: '{moduleId}'
});

?exported-web-component (new mechanism)

virtual:flow-css-import?path=path/to/file.css&exported-web-component={exportedWebComponent}

import 'path/to/file.css?global-css-only'; // <--- included in CSS bundle in build mode
import cssContent from 'path/to/file.css?inline'; // <--- included in JS bundle in build mode
import { injectExportedWebComponentCSS } from 'Frontend/generated/jar-resources/flow-css-import.js';

injectExportedWebComponentCSS('{AUTO_GENERATED_ID}', cssContent.toString(), {
  selector: '{exportedWebComponent}'
});

import.meta.hot?.accept(); // <--- enables hot reloading

{exportedWebComponent} is a CSS selector representing web components to which the styles should added. Example values:

  • * (all exported components)
  • exported-component-0, exported-component-1 (a list of component tags)

?exported-web-component + @Theme (theme-compatibility mode)

virtual:flow-css-import?path=path/to/file.css&exported-web-component={exportedWebComponent}

import cssContent from 'path/to/file.css?inline';
import { injectGlobalWebcomponentCss } from 'Frontend/generated/jar-resources/theme-util.js';

injectGlobalWebcomponentCss(cssContent.toString()); // <--- supports only full page hot reloading

{exportedWebComponent} is ignored. All styles are always added to every exported web components.

Next steps

  1. AbstractUpdateImports.java currently adds all @CssImport styles to the Shadow DOM of every exported web component by passing exported-web-component=* to the Vite plugin. While this matches the behavior of @Theme with enabled autoInjectGlobalCssImports, it would be preferable for at least the @CssImport annotations defined directly on WebComponentExporter classes to be included exclusively in their corresponding web components to ensure style isolation between exported web components. This would require AbstractUpdateImports.java to be aware of the class context for each @CssImport in order to add a virtual:flow-css-import... import with a proper web component selector.

  2. @CssImport annotations defined directly on WebComponentExporter classes are currently included not only in generated-flow-webcomponent-imports.js but also in generated-flow-imports.js, which is loaded by the main application. This may lead to unintended side effects, as styles intended only for exported web components may leak into the main application context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: 🪵Product backlog
Development

Successfully merging a pull request may close this issue.

2 participants